From f7156b94903786cdd27b812df585c466ab19cf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 1 Nov 2023 18:47:11 +0100 Subject: [PATCH 1/5] Custom SQLite 3.44.0 --- Documentation/CustomSQLiteBuilds.md | 2 +- SQLiteCustom/src | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/CustomSQLiteBuilds.md b/Documentation/CustomSQLiteBuilds.md index 2aa955f5ba..c49092d476 100644 --- a/Documentation/CustomSQLiteBuilds.md +++ b/Documentation/CustomSQLiteBuilds.md @@ -3,7 +3,7 @@ Custom SQLite Builds By default, GRDB uses the version of SQLite that ships with the target operating system. -**You can build GRDB with a custom build of [SQLite 3.42.0](https://www.sqlite.org/changes.html).** +**You can build GRDB with a custom build of [SQLite 3.44.0](https://www.sqlite.org/changes.html).** A custom SQLite build can activate extra SQLite features, and extra GRDB features as well, such as support for the [FTS5 full-text search engine](../../../#full-text-search), and [SQLite Pre-Update Hooks](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/transactionobserver). diff --git a/SQLiteCustom/src b/SQLiteCustom/src index 59f4dc65f0..31e6aa6618 160000 --- a/SQLiteCustom/src +++ b/SQLiteCustom/src @@ -1 +1 @@ -Subproject commit 59f4dc65f053482934ad184a88f2394a7dfc2f7b +Subproject commit 31e6aa66188e59616f062df77329d6ee9ee45929 From 2603b6ea0314b4cd57bac21b81278e0a32732890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 1 Nov 2023 19:16:50 +0100 Subject: [PATCH 2/5] Support for ORDER BY and FILTER in aggregate functions --- GRDB/Core/DatabaseFunction.swift | 32 +- GRDB/JSON/SQLJSONFunctions.swift | 48 ++- GRDB/QueryInterface/SQL/SQLExpression.swift | 406 +++++++++++------- GRDB/QueryInterface/SQL/SQLFunctions.swift | 207 ++++++++- Tests/GRDBTests/JSONExpressionsTests.swift | 93 ++++ .../QueryInterfaceExpressionsTests.swift | 157 +++++++ 6 files changed, 762 insertions(+), 181 deletions(-) diff --git a/GRDB/Core/DatabaseFunction.swift b/GRDB/Core/DatabaseFunction.swift index e74b22b730..4b13c39c27 100644 --- a/GRDB/Core/DatabaseFunction.swift +++ b/GRDB/Core/DatabaseFunction.swift @@ -34,19 +34,6 @@ public final class DatabaseFunction: Hashable { private let kind: Kind private var eTextRep: CInt { (SQLITE_UTF8 | (isPure ? SQLITE_DETERMINISTIC : 0)) } - var functionFlags: SQLFunctionFlags { - var flags = SQLFunctionFlags(isPure: isPure) - - switch kind { - case .function: - break - case .aggregate: - flags.isAggregate = true - } - - return flags - } - /// Creates an SQL function. /// /// For example: @@ -183,9 +170,24 @@ public final class DatabaseFunction: Hashable { /// } /// ``` public func callAsFunction(_ arguments: any SQLExpressible...) -> SQLExpression { - .function(name, arguments.map(\.sqlExpression), flags: functionFlags) + switch kind { + case .function: + return .simpleFunction( + name, + arguments.map(\.sqlExpression), + isPure: isPure, + isJSONValue: false) + case .aggregate: + return .aggregateFunction( + name, + arguments.map(\.sqlExpression), + isDistinct: false, + ordering: nil, + filter: nil, + isJSONValue: false) + } } - + /// Calls sqlite3_create_function_v2 /// See func install(in db: Database) { diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift index 41c87c9ba3..2fd86aae22 100644 --- a/GRDB/JSON/SQLJSONFunctions.swift +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -374,15 +374,32 @@ extension Database { /// The `JSON_GROUP_ARRAY` SQL function. /// /// Related SQLite documentation: - public static func jsonGroupArray(_ value: some SQLExpressible) -> SQLExpression { - .function("JSON_GROUP_ARRAY", [value.sqlExpression.jsonBuilderExpression]) + public static func jsonGroupArray( + _ value: some SQLExpressible, + orderBy ordering: (any SQLOrderingTerm)? = nil, + filter: (any SQLSpecificExpressible)? = nil) + -> SQLExpression { + .aggregateFunction( + "JSON_GROUP_ARRAY", + [value.sqlExpression.jsonBuilderExpression], + ordering: ordering?.sqlOrdering, + filter: filter?.sqlExpression, + isJSONValue: true) } /// The `JSON_GROUP_OBJECT` SQL function. /// /// Related SQLite documentation: - public static func jsonGroupObject(key: some SQLExpressible, value: some SQLExpressible) -> SQLExpression { - .function("JSON_GROUP_OBJECT", [key.sqlExpression, value.sqlExpression.jsonBuilderExpression]) + public static func jsonGroupObject( + key: some SQLExpressible, + value: some SQLExpressible, + filter: (any SQLSpecificExpressible)? = nil + ) -> SQLExpression { + .aggregateFunction( + "JSON_GROUP_OBJECT", + [key.sqlExpression, value.sqlExpression.jsonBuilderExpression], + filter: filter?.sqlExpression, + isJSONValue: true) } } #else @@ -781,16 +798,31 @@ extension Database { /// /// Related SQLite documentation: @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonGroupArray(_ value: some SQLExpressible) -> SQLExpression { - .function("JSON_GROUP_ARRAY", [value.sqlExpression.jsonBuilderExpression]) + public static func jsonGroupArray( + _ value: some SQLExpressible, + filter: (any SQLSpecificExpressible)? = nil) + -> SQLExpression { + .aggregateFunction( + "JSON_GROUP_ARRAY", + [value.sqlExpression.jsonBuilderExpression], + filter: filter?.sqlExpression, + isJSONValue: true) } /// The `JSON_GROUP_OBJECT` SQL function. /// /// Related SQLite documentation: @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonGroupObject(key: some SQLExpressible, value: some SQLExpressible) -> SQLExpression { - .function("JSON_GROUP_OBJECT", [key.sqlExpression, value.sqlExpression.jsonBuilderExpression]) + public static func jsonGroupObject( + key: some SQLExpressible, + value: some SQLExpressible, + filter: (any SQLSpecificExpressible)? = nil + ) -> SQLExpression { + .aggregateFunction( + "JSON_GROUP_OBJECT", + [key.sqlExpression, value.sqlExpression.jsonBuilderExpression], + filter: filter?.sqlExpression, + isJSONValue: true) } } #endif diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index 781f091fe4..e7579b4735 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -165,8 +165,14 @@ public struct SQLExpression { /// /// (, ...) /// (DISTINCT ) - case function(String, flags: SQLFunctionFlags, arguments: [SQLExpression]) + indirect case simpleFunction(SQLSimpleFunctionInvocation) + /// An aggregate function call. + /// + /// (, ...) + /// (DISTINCT ) + indirect case aggregateFunction(SQLAggregateFunctionInvocation) + /// An expression that checks for zero or positive values. /// /// = 0 @@ -268,11 +274,11 @@ public struct SQLExpression { case .countAll: return .countAll - case let .function(name, flags: flags, arguments: arguments): - return .function( - name, - flags: flags, - arguments: arguments.map { $0.qualified(with: alias) }) + case let .simpleFunction(invocation): + return .simpleFunction(invocation.qualified(with: alias)) + + case let .aggregateFunction(invocation): + return .aggregateFunction(invocation.qualified(with: alias)) case let .isEmpty(expression, isNegated: isNegated): return .isEmpty(expression.qualified(with: alias), isNegated: isNegated) @@ -916,9 +922,154 @@ extension SQLExpression { // MARK: Functions + // TODO: add missing pure functions: + // https://www.sqlite.org/lang_aggfunc.html + // https://www.sqlite.org/lang_datefunc.html + // https://www.sqlite.org/lang_mathfunc.html + private static let knownPureFunctions: Set = [ + "ABS", + "CHAR", + "COALESCE", + "GLOB", + "HEX", + "IFNULL", + "IIF", + "INSTR", + "JSON", + "JSON_ARRAY", + "JSON_GROUP_ARRAY", + "JSON_GROUP_OBJECT", + "JSON_INSERT", + "JSON_OBJECT", + "JSON_PATCH", + "JSON_REMOVE", + "JSON_REPLACE", + "JSON_SET", + "JSON_QUOTE", + "LENGTH", + "LIKE", + "LIKELIHOOD", + "LIKELY", + "LOAD_EXTENSION", + "LOWER", + "LTRIM", + "NULLIF", + "PRINTF", + "QUOTE", + "REPLACE", + "ROUND", + "RTRIM", + "SOUNDEX", + "SQLITE_COMPILEOPTION_GET", + "SQLITE_COMPILEOPTION_USED", + "SQLITE_SOURCE_ID", + "SQLITE_VERSION", + "SUBSTR", + "TRIM", + "TYPEOF", + "UNICODE", + "UNLIKELY", + "UPPER", + "ZEROBLOB", + ] + + private static let knownAggregateFunctions: Set = [ + "AVG", + "COUNT", + "GROUP_CONCAT", + "JSON_GROUP_ARRAY", + "JSON_GROUP_OBJECT", + "MAX", // when single argument + "MIN", // when single argument + "SUM", + "TOTAL", + ] + + private static let knownFunctionsReturningJSONValue: Set = [ + "JSON", + "JSON_ARRAY", + "JSON_GROUP_ARRAY", + "JSON_GROUP_OBJECT", + "JSON_INSERT", + "JSON_OBJECT", + "JSON_PATCH", + "JSON_REMOVE", + "JSON_REPLACE", + "JSON_SET", + "JSON_QUOTE", + ] + /// The `COUNT(*)` expression. static let countAll = SQLExpression(impl: .countAll) + /// A function call. + static func function(_ functionName: String, _ arguments: [SQLExpression]) -> Self { + let name = functionName.uppercased() + + if (name == "MAX" || name == "MIN") && arguments.count > 1 { + return .simpleFunction( + functionName, + arguments, + isPure: true, + isJSONValue: false) + + } else if Self.knownAggregateFunctions.contains(name) { + return .aggregateFunction( + functionName, + arguments, + isJSONValue: Self.knownFunctionsReturningJSONValue.contains(name)) + + } else { + let isJSONValue: Bool + if name == "JSON_EXTRACT" && arguments.count > 2 { + isJSONValue = true + } else { + isJSONValue = Self.knownFunctionsReturningJSONValue.contains(name) + } + + return .simpleFunction( + functionName, arguments, + isPure: Self.knownPureFunctions.contains(name), + isJSONValue: isJSONValue) + } + } + + /// A simple function call. + /// + /// - warning: Don't use this method for aggregate functions! + static func simpleFunction( + _ name: String, + _ arguments: [SQLExpression], + isPure: Bool = false, + isJSONValue: Bool = false) + -> Self + { + .init(impl: .simpleFunction(SQLSimpleFunctionInvocation( + name: name, + arguments: arguments, + isPure: isPure, + isJSONValue: isJSONValue))) + } + + /// An aggregate function call. + static func aggregateFunction( + _ name: String, + _ arguments: [SQLExpression], + isDistinct: Bool = false, + ordering: SQLOrdering? = nil, + filter: SQLExpression? = nil, + isJSONValue: Bool = false) + -> Self + { + .init(impl: .aggregateFunction(.init( + name: name, + arguments: arguments, + isDistinct: isDistinct, + ordering: ordering, + filter: filter, + isJSONValue: isJSONValue))) + } + /// The `COUNT` function. /// /// COUNT() @@ -930,24 +1081,7 @@ extension SQLExpression { /// /// COUNT(DISTINCT ) static func countDistinct(_ expression: SQLExpression) -> Self { - function("COUNT", [expression], flags: .init(isAggregate: true, isDistinct: true)) - } - - /// A function call. - /// - /// (, ...) - static func function(_ name: String, _ arguments: [SQLExpression]) -> Self { - .init(impl: .function( - name, - flags: .defaultFlags(for: name, argumentCount: arguments.count), - arguments: arguments)) - } - - /// A function call with explicit flags. - /// - /// (, ...) - static func function(_ name: String, _ arguments: [SQLExpression], flags: SQLFunctionFlags) -> Self { - .init(impl: .function(name, flags: flags, arguments: arguments)) + aggregateFunction("COUNT", [expression], isDistinct: true) } /// An expression that checks for zero or positive values. @@ -1071,18 +1205,8 @@ extension SQLExpression { case let .collated(expression, _): return try expression.column(db, for: alias, acceptsBijection: acceptsBijection) - case let .function(name, flags: flags, arguments: arguments) where !flags.isAggregate: - guard acceptsBijection else { - return nil - } - let name = name.uppercased() - if ["HEX", "QUOTE"].contains(name) && arguments.count == 1 { - return try arguments[0].column(db, for: alias, acceptsBijection: acceptsBijection) - } else if name == "IFNULL" && arguments.count == 2 && arguments[1].isConstantInRequest { - return try arguments[0].column(db, for: alias, acceptsBijection: acceptsBijection) - } else { - return nil - } + case let .simpleFunction(invocation) where acceptsBijection: + return try invocation.column(db, for: alias) case let .qualifiedFastPrimaryKey(a): if alias == a { @@ -1266,13 +1390,11 @@ extension SQLExpression { case .countAll: return "COUNT(*)" - case let .function(name, flags: flags, arguments: arguments): - assert(!flags.isDistinct || flags.isAggregate, "distinct requires aggregate") - assert(!flags.isDistinct || arguments.count == 1, "distinct requires a single argument") - return try name - + (flags.isDistinct ? "(DISTINCT " : "(") - + arguments.map { try $0.sql(context) }.joined(separator: ", ") - + ")" + case let .simpleFunction(invocation): + return try invocation.sql(context) + + case let .aggregateFunction(invocation): + return try invocation.sql(context, wrappedInParenthesis: wrappedInParenthesis) case let .isEmpty(expression, isNegated: isNegated): var resultSQL = try """ @@ -1721,10 +1843,8 @@ extension SQLExpression { let .collated(expression, _): return expression.isConstantInRequest - case let .function(_, flags: flags, arguments: arguments) - where flags.isPure && !flags.isAggregate: - - return arguments.allSatisfy(\.isConstantInRequest) + case let .simpleFunction(invocation): + return invocation.isConstantInRequest default: return false @@ -1783,7 +1903,7 @@ extension SQLExpression { case .countAll: return true - case let .function(_, flags: flags, arguments: _) where flags.isAggregate: + case .aggregateFunction: return true default: @@ -1792,131 +1912,108 @@ extension SQLExpression { } } -// MARK: - Function Flags - -/// Information about a function -struct SQLFunctionFlags { +/// https://www.sqlite.org/syntax/simple-function-invocation.html +struct SQLSimpleFunctionInvocation { + var name: String + var arguments: [SQLExpression] + /// A boolean value indicating if a function is known to be pure. /// /// A false value does not provide any information. - var isPure = false + var isPure: Bool - /// A boolean value indicating if a function is known to be - /// an aggregate. + /// A boolean value indicating if a function is known to return a + /// JSON value. /// /// A false value does not provide any information. - var isAggregate = false + var isJSONValue: Bool + + var isConstantInRequest: Bool { + isPure && arguments.allSatisfy(\.isConstantInRequest) + } + + func qualified(with alias: TableAlias) -> Self { + SQLSimpleFunctionInvocation( + name: name, + arguments: arguments.map { $0.qualified(with: alias) }, + isPure: isPure, + isJSONValue: isJSONValue) + } + + func column(_ db: Database, for alias: TableAlias) throws -> String? { + let name = name.uppercased() + if ["HEX", "QUOTE"].contains(name) && arguments.count == 1 { + return try arguments[0].column(db, for: alias, acceptsBijection: true) + } else if name == "IFNULL" && arguments.count == 2 && arguments[1].isConstantInRequest { + return try arguments[0].column(db, for: alias, acceptsBijection: true) + } else { + return nil + } + } - /// A boolean value indicating if the function should have `DISTINCT` - /// in its SQL generation (as in `COUNT(DISTINCT ...)`). + func sql(_ context: SQLGenerationContext) throws -> String { + var sql = name + sql += "(" + sql += try arguments + .map { try $0.sql(context) } + .joined(separator: ", ") + sql += ")" + return sql + } +} + +/// https://www.sqlite.org/syntax/aggregate-function-invocation.html +struct SQLAggregateFunctionInvocation { + var name: String + var arguments: [SQLExpression] var isDistinct = false + var ordering: SQLOrdering? = nil // SQLite 3.44.0+ + var filter: SQLExpression? = nil // @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) SQLite 3.30+ /// A boolean value indicating if a function is known to return a /// JSON value. /// /// A false value does not provide any information. - var isJSONValue = false -} - -extension SQLFunctionFlags { - // TODO: add missing pure functions: - // https://www.sqlite.org/lang_aggfunc.html - // https://www.sqlite.org/lang_datefunc.html - // https://www.sqlite.org/lang_mathfunc.html - private static let knownPureFunctions: Set = [ - "ABS", - "CHAR", - "COALESCE", - "GLOB", - "HEX", - "IFNULL", - "IIF", - "INSTR", - "JSON", - "JSON_ARRAY", - "JSON_GROUP_ARRAY", - "JSON_GROUP_OBJECT", - "JSON_INSERT", - "JSON_OBJECT", - "JSON_PATCH", - "JSON_REMOVE", - "JSON_REPLACE", - "JSON_SET", - "JSON_QUOTE", - "LENGTH", - "LIKE", - "LIKELIHOOD", - "LIKELY", - "LOAD_EXTENSION", - "LOWER", - "LTRIM", - "NULLIF", - "PRINTF", - "QUOTE", - "REPLACE", - "ROUND", - "RTRIM", - "SOUNDEX", - "SQLITE_COMPILEOPTION_GET", - "SQLITE_COMPILEOPTION_USED", - "SQLITE_SOURCE_ID", - "SQLITE_VERSION", - "SUBSTR", - "TRIM", - "TYPEOF", - "UNICODE", - "UNLIKELY", - "UPPER", - "ZEROBLOB", - ] - - private static let knownAggregateFunctions: Set = [ - "AVG", - "COUNT", - "GROUP_CONCAT", - "JSON_GROUP_ARRAY", - "JSON_GROUP_OBJECT", - "MAX", // when single argument - "MIN", // when single argument - "SUM", - "TOTAL", - ] + var isJSONValue: Bool - private static let knownFunctionsReturningJSONValue: Set = [ - "JSON", - "JSON_ARRAY", - "JSON_GROUP_ARRAY", - "JSON_GROUP_OBJECT", - "JSON_INSERT", - "JSON_OBJECT", - "JSON_PATCH", - "JSON_REMOVE", - "JSON_REPLACE", - "JSON_SET", - "JSON_QUOTE", - ] + func qualified(with alias: TableAlias) -> Self { + SQLAggregateFunctionInvocation( + name: name, + arguments: arguments.map { $0.qualified(with: alias) }, + isDistinct: isDistinct, + ordering: ordering?.qualified(with: alias), + filter: filter?.qualified(with: alias), + isJSONValue: isJSONValue) + } - /// Infers flags from the function name and number of arguments. - static func defaultFlags(for functionName: String, argumentCount: Int) -> Self { - var flags = SQLFunctionFlags() + func sql(_ context: SQLGenerationContext, wrappedInParenthesis: Bool) throws -> String { + var sql = name - let name = functionName.uppercased() + if isDistinct { + sql += "(DISTINCT " + } else { + sql += "(" + } - flags.isPure = Self.knownPureFunctions.contains(name) + sql += try arguments + .map { try $0.sql(context) } + .joined(separator: ", ") - if (name == "MAX" || name == "MIN") && argumentCount > 1 { - flags.isPure = true - } else { - flags.isAggregate = Self.knownAggregateFunctions.contains(name.uppercased()) + if let ordering { + sql += try " ORDER BY \(ordering.sql(context))" } - if name == "JSON_EXTRACT" && argumentCount > 2 { - flags.isJSONValue = true - } else { - flags.isJSONValue = Self.knownFunctionsReturningJSONValue.contains(name) + sql += ")" + + if let filter { + sql += try " FILTER (WHERE \(filter.sql(context)))" } - return flags + if wrappedInParenthesis && filter != nil { + return "(\(sql))" + } else { + return sql + } } } @@ -1960,8 +2057,11 @@ extension SQLExpression { case let .collated(expression, _): return expression.isJSONValue - case let .function(_, flags: flags, arguments: _): - return flags.isJSONValue + case let .simpleFunction(invocation): + return invocation.isJSONValue + + case let .aggregateFunction(invocation): + return invocation.isJSONValue default: return false diff --git a/GRDB/QueryInterface/SQL/SQLFunctions.swift b/GRDB/QueryInterface/SQL/SQLFunctions.swift index a1eb3c0f27..a6938dab5b 100644 --- a/GRDB/QueryInterface/SQL/SQLFunctions.swift +++ b/GRDB/QueryInterface/SQL/SQLFunctions.swift @@ -10,6 +10,40 @@ public func abs(_ value: some SQLSpecificExpressible) -> SQLExpression { .function("ABS", [value.sqlExpression]) } +#if GRDBCUSTOMSQLITE || GRDBCIPHER +/// The `AVG` SQL function. +/// +/// For example: +/// +/// ```swift +/// // AVG(length) +/// average(Column("length")) +/// ``` +public func average( + _ value: some SQLSpecificExpressible, + filter: (any SQLSpecificExpressible)? = nil) +-> SQLExpression { + .aggregateFunction("AVG", [value.sqlExpression], filter: filter?.sqlExpression) +} +#else +/// The `AVG` SQL function. +/// +/// For example: +/// +/// ```swift +/// // AVG(length) FILTER (WHERE length > 0) +/// average(Column("length"), filter: Column("length") > 0) +/// ``` +@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +public func average( + _ value: some SQLSpecificExpressible, + filter: some SQLSpecificExpressible) +-> SQLExpression { + .aggregateFunction( + "AVG", [value.sqlExpression], + filter: filter.sqlExpression) +} + /// The `AVG` SQL function. /// /// For example: @@ -19,8 +53,9 @@ public func abs(_ value: some SQLSpecificExpressible) -> SQLExpression { /// average(Column("length")) /// ``` public func average(_ value: some SQLSpecificExpressible) -> SQLExpression { - .function("AVG", [value.sqlExpression]) + .aggregateFunction("AVG", [value.sqlExpression]) } +#endif /// The `COUNT` SQL function. /// @@ -72,6 +107,38 @@ public func length(_ value: some SQLSpecificExpressible) -> SQLExpression { .function("LENGTH", [value.sqlExpression]) } +#if GRDBCUSTOMSQLITE || GRDBCIPHER +/// The `MAX` SQL function. +/// +/// For example: +/// +/// ```swift +/// // MAX(score) +/// max(Column("score")) +/// ``` +public func max( + _ value: some SQLSpecificExpressible, + filter: (any SQLSpecificExpressible)? = nil) +-> SQLExpression { + .aggregateFunction("MAX", [value.sqlExpression], filter: filter?.sqlExpression) +} +#else +/// The `MAX` SQL function. +/// +/// For example: +/// +/// ```swift +/// // MAX(score) FILTER (WHERE score < 0) +/// max(Column("score"), filter: Column("score") < 0) +/// ``` +@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +public func max( + _ value: some SQLSpecificExpressible, + filter: some SQLSpecificExpressible) +-> SQLExpression { + .aggregateFunction("MAX", [value.sqlExpression], filter: filter.sqlExpression) +} + /// The `MAX` SQL function. /// /// For example: @@ -81,7 +148,40 @@ public func length(_ value: some SQLSpecificExpressible) -> SQLExpression { /// max(Column("score")) /// ``` public func max(_ value: some SQLSpecificExpressible) -> SQLExpression { - .function("MAX", [value.sqlExpression]) + .aggregateFunction("MAX", [value.sqlExpression]) +} +#endif + +#if GRDBCUSTOMSQLITE || GRDBCIPHER +/// The `MIN` SQL function. +/// +/// For example: +/// +/// ```swift +/// // MIN(score) +/// min(Column("score")) +/// ``` +public func min( + _ value: some SQLSpecificExpressible, + filter: (any SQLSpecificExpressible)? = nil) +-> SQLExpression { + .aggregateFunction("MIN", [value.sqlExpression], filter: filter?.sqlExpression) +} +#else +/// The `MIN` SQL function. +/// +/// For example: +/// +/// ```swift +/// // MIN(score) FILTER (WHERE score > 0) +/// min(Column("score"), filter: Column("score") > 0) +/// ``` +@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +public func min( + _ value: some SQLSpecificExpressible, + filter: some SQLSpecificExpressible) +-> SQLExpression { + .aggregateFunction("MIN", [value.sqlExpression], filter: filter.sqlExpression) } /// The `MIN` SQL function. @@ -93,7 +193,55 @@ public func max(_ value: some SQLSpecificExpressible) -> SQLExpression { /// min(Column("score")) /// ``` public func min(_ value: some SQLSpecificExpressible) -> SQLExpression { - .function("MIN", [value.sqlExpression]) + .aggregateFunction("MIN", [value.sqlExpression]) +} +#endif + +#if GRDBCUSTOMSQLITE || GRDBCIPHER +/// The `SUM` SQL function. +/// +/// For example: +/// +/// ```swift +/// // SUM(amount) +/// sum(Column("amount")) +/// ``` +/// +/// See also ``total(_:)``. +/// +/// Related SQLite documentation: . +public func sum( + _ value: some SQLSpecificExpressible, + orderBy ordering: (any SQLOrderingTerm)? = nil, + filter: (any SQLSpecificExpressible)? = nil) +-> SQLExpression +{ + .aggregateFunction( + "SUM", [value.sqlExpression], + ordering: ordering?.sqlOrdering, + filter: filter?.sqlExpression) +} +#else +/// The `SUM` SQL function. +/// +/// For example: +/// +/// ```swift +/// // SUM(amount) FILTER (WHERE amount > 0) +/// sum(Column("amount"), filter: Column("amount") > 0) +/// ``` +/// +/// See also ``total(_:)``. +/// +/// Related SQLite documentation: . +@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +public func sum( + _ value: some SQLSpecificExpressible, + filter: some SQLSpecificExpressible) +-> SQLExpression { + .aggregateFunction( + "SUM", [value.sqlExpression], + filter: filter.sqlExpression) } /// The `SUM` SQL function. @@ -109,7 +257,55 @@ public func min(_ value: some SQLSpecificExpressible) -> SQLExpression { /// /// Related SQLite documentation: . public func sum(_ value: some SQLSpecificExpressible) -> SQLExpression { - .function("SUM", [value.sqlExpression]) + .aggregateFunction("SUM", [value.sqlExpression]) +} +#endif + +#if GRDBCUSTOMSQLITE || GRDBCIPHER +/// The `TOTAL` SQL function. +/// +/// For example: +/// +/// ```swift +/// // TOTAL(amount) +/// total(Column("amount")) +/// ``` +/// +/// See also ``sum(_:)``. +/// +/// Related SQLite documentation: . +public func total( + _ value: some SQLSpecificExpressible, + orderBy ordering: (any SQLOrderingTerm)? = nil, + filter: (any SQLSpecificExpressible)? = nil) +-> SQLExpression +{ + .aggregateFunction( + "TOTAL", [value.sqlExpression], + ordering: ordering?.sqlOrdering, + filter: filter?.sqlExpression) +} +#else +/// The `TOTAL` SQL function. +/// +/// For example: +/// +/// ```swift +/// // TOTAL(amount) FILTER (WHERE amount > 0) +/// total(Column("amount"), filter: Column("amount") > 0) +/// ``` +/// +/// See also ``total(_:)``. +/// +/// Related SQLite documentation: . +@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +public func total( + _ value: some SQLSpecificExpressible, + filter: some SQLSpecificExpressible) +-> SQLExpression { + .aggregateFunction( + "TOTAL", [value.sqlExpression], + filter: filter.sqlExpression) } /// The `TOTAL` SQL function. @@ -125,8 +321,9 @@ public func sum(_ value: some SQLSpecificExpressible) -> SQLExpression { /// /// Related SQLite documentation: . public func total(_ value: some SQLSpecificExpressible) -> SQLExpression { - .function("TOTAL", [value.sqlExpression]) + .aggregateFunction("TOTAL", [value.sqlExpression]) } +#endif // MARK: - String functions diff --git a/Tests/GRDBTests/JSONExpressionsTests.swift b/Tests/GRDBTests/JSONExpressionsTests.swift index 5c91ea3c8d..5758037abf 100644 --- a/Tests/GRDBTests/JSONExpressionsTests.swift +++ b/Tests/GRDBTests/JSONExpressionsTests.swift @@ -1211,6 +1211,72 @@ final class JSONExpressionsTests: GRDBTestCase { } } + func test_Database_jsonGroupArray_filter() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, player.select(Database.jsonGroupArray(nameColumn, filter: length(nameColumn) > 0)), """ + SELECT JSON_GROUP_ARRAY("name") FILTER (WHERE LENGTH("name") > 0) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonGroupArray(infoColumn, filter: length(nameColumn) > 0)), """ + SELECT JSON_GROUP_ARRAY(JSON("info")) FILTER (WHERE LENGTH("name") > 0) FROM "player" + """) + } + } + +#if GRDBCUSTOMSQLITE || GRDBCIPHER + func test_Database_jsonGroupArray_order() throws { + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, player.select(Database.jsonGroupArray(nameColumn, orderBy: nameColumn)), """ + SELECT JSON_GROUP_ARRAY("name" ORDER BY "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonGroupArray(infoColumn, orderBy: nameColumn.desc)), """ + SELECT JSON_GROUP_ARRAY(JSON("info") ORDER BY "name" DESC) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonGroupArray(nameColumn, orderBy: nameColumn, filter: length(nameColumn) > 0)), """ + SELECT JSON_GROUP_ARRAY("name" ORDER BY "name") FILTER (WHERE LENGTH("name") > 0) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonGroupArray(infoColumn, orderBy: nameColumn.desc, filter: length(nameColumn) > 0)), """ + SELECT JSON_GROUP_ARRAY(JSON("info") ORDER BY "name" DESC) FILTER (WHERE LENGTH("name") > 0) FROM "player" + """) + } + } +#endif + func test_Database_jsonGroupObject() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures @@ -1238,6 +1304,33 @@ final class JSONExpressionsTests: GRDBTestCase { } } + func test_Database_jsonGroupObject_filter() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("key", .text) + t.column("value", .jsonText) + } + let player = Table("player") + let keyColumn = Column("key") + let valueColumn = JSONColumn("value") + + try assertEqualSQL(db, player.select(Database.jsonGroupObject(key: keyColumn, value: valueColumn, filter: length(valueColumn) > 0)), """ + SELECT JSON_GROUP_OBJECT("key", JSON("value")) FILTER (WHERE LENGTH("value") > 0) FROM "player" + """) + } + } + func test_index_and_generated_columns() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures diff --git a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift index c4b0c0046e..67c33c4d9b 100644 --- a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift +++ b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift @@ -1505,6 +1505,28 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { "SELECT AVG(\"age\" / 2) FROM \"readers\"") } + func testAvgExpression_filter() throws { + #if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3030000 else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #else + guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #endif + + let dbQueue = try makeDatabaseQueue() + + XCTAssertEqual( + sql(dbQueue, tableRequest.select(average(Col.age, filter: Col.age > 0))), + "SELECT AVG(\"age\") FILTER (WHERE \"age\" > 0) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(average(Col.age / 2, filter: Col.age > 0))), + "SELECT AVG(\"age\" / 2) FILTER (WHERE \"age\" > 0) FROM \"readers\"") + } + func testLengthExpression() throws { let dbQueue = try makeDatabaseQueue() @@ -1524,6 +1546,28 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { "SELECT MIN(\"age\" / 2) FROM \"readers\"") } + func testMinExpression_filter() throws { + #if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3030000 else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #else + guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #endif + + let dbQueue = try makeDatabaseQueue() + + XCTAssertEqual( + sql(dbQueue, tableRequest.select(min(Col.age, filter: Col.age > 0))), + "SELECT MIN(\"age\") FILTER (WHERE \"age\" > 0) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(min(Col.age / 2, filter: Col.age > 0))), + "SELECT MIN(\"age\" / 2) FILTER (WHERE \"age\" > 0) FROM \"readers\"") + } + func testMaxExpression() throws { let dbQueue = try makeDatabaseQueue() @@ -1535,6 +1579,28 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { "SELECT MAX(\"age\" / 2) FROM \"readers\"") } + func testMaxExpression_filter() throws { + #if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3030000 else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #else + guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #endif + + let dbQueue = try makeDatabaseQueue() + + XCTAssertEqual( + sql(dbQueue, tableRequest.select(max(Col.age, filter: Col.age < 0))), + "SELECT MAX(\"age\") FILTER (WHERE \"age\" < 0) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(max(Col.age / 2, filter: Col.age < 0))), + "SELECT MAX(\"age\" / 2) FILTER (WHERE \"age\" < 0) FROM \"readers\"") + } + func testSumExpression() throws { let dbQueue = try makeDatabaseQueue() @@ -1546,6 +1612,52 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { "SELECT SUM(\"age\" / 2) FROM \"readers\"") } + func testSumExpression_filter() throws { + #if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3030000 else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #else + guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #endif + + let dbQueue = try makeDatabaseQueue() + + XCTAssertEqual( + sql(dbQueue, tableRequest.select(sum(Col.age, filter: Col.age > 0))), + "SELECT SUM(\"age\") FILTER (WHERE \"age\" > 0) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(sum(Col.age / 2, filter: Col.age > 0))), + "SELECT SUM(\"age\" / 2) FILTER (WHERE \"age\" > 0) FROM \"readers\"") + } + +#if GRDBCUSTOMSQLITE || GRDBCIPHER + func testSumExpression_order() throws { + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3044000 else { + throw XCTSkip("ORDER BY clause on aggregate functions is not available") + } + + let dbQueue = try makeDatabaseQueue() + + XCTAssertEqual( + sql(dbQueue, tableRequest.select(sum(Col.age, orderBy: Col.age))), + "SELECT SUM(\"age\" ORDER BY \"age\") FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(sum(Col.age / 2, orderBy: Col.age.desc))), + "SELECT SUM(\"age\" / 2 ORDER BY \"age\" DESC) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(sum(Col.age, orderBy: Col.age, filter: Col.age > 0))), + "SELECT SUM(\"age\" ORDER BY \"age\") FILTER (WHERE \"age\" > 0) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(sum(Col.age / 2, orderBy: Col.age.desc, filter: Col.age > 0))), + "SELECT SUM(\"age\" / 2 ORDER BY \"age\" DESC) FILTER (WHERE \"age\" > 0) FROM \"readers\"") + } +#endif + func testTotalExpression() throws { let dbQueue = try makeDatabaseQueue() @@ -1557,6 +1669,51 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { "SELECT TOTAL(\"age\" / 2) FROM \"readers\"") } + func testTotalExpression_filter() throws { + #if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3030000 else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #else + guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + throw XCTSkip("FILTER clause on aggregate functions is not available") + } + #endif + + let dbQueue = try makeDatabaseQueue() + + XCTAssertEqual( + sql(dbQueue, tableRequest.select(total(Col.age, filter: Col.age > 0))), + "SELECT TOTAL(\"age\") FILTER (WHERE \"age\" > 0) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(total(Col.age / 2, filter: Col.age > 0))), + "SELECT TOTAL(\"age\" / 2) FILTER (WHERE \"age\" > 0) FROM \"readers\"") + } + +#if GRDBCUSTOMSQLITE || GRDBCIPHER + func testTotalExpression_order() throws { + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3044000 else { + throw XCTSkip("ORDER BY clause on aggregate functions is not available") + } + + let dbQueue = try makeDatabaseQueue() + + XCTAssertEqual( + sql(dbQueue, tableRequest.select(total(Col.age, orderBy: Col.age))), + "SELECT TOTAL(\"age\" ORDER BY \"age\") FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(total(Col.age / 2, orderBy: Col.age.desc))), + "SELECT TOTAL(\"age\" / 2 ORDER BY \"age\" DESC) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(total(Col.age, orderBy: Col.age, filter: Col.age > 0))), + "SELECT TOTAL(\"age\" ORDER BY \"age\") FILTER (WHERE \"age\" > 0) FROM \"readers\"") + XCTAssertEqual( + sql(dbQueue, tableRequest.select(total(Col.age / 2, orderBy: Col.age.desc, filter: Col.age > 0))), + "SELECT TOTAL(\"age\" / 2 ORDER BY \"age\" DESC) FILTER (WHERE \"age\" > 0) FROM \"readers\"") + } +#endif // MARK: - LIKE operator From 46ad19d2e359609570401ee0ae1b1d32d7e80764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 2 Nov 2023 13:24:46 +0100 Subject: [PATCH 3/5] Documentation --- GRDB/Documentation.docc/JSON.md | 4 +- GRDB/JSON/SQLJSONFunctions.swift | 53 +++++++++++++++++++++ GRDB/QueryInterface/SQL/SQLExpression.swift | 5 ++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/GRDB/Documentation.docc/JSON.md b/GRDB/Documentation.docc/JSON.md index 49eec990f0..581561b67d 100644 --- a/GRDB/Documentation.docc/JSON.md +++ b/GRDB/Documentation.docc/JSON.md @@ -133,8 +133,8 @@ The `->` and `->>` SQL operators are available on the ``SQLJSONExpressible`` pro - ``Database/jsonArray(_:)-469db`` - ``Database/jsonObject(_:)`` - ``Database/jsonQuote(_:)`` -- ``Database/jsonGroupArray(_:)`` -- ``Database/jsonGroupObject(key:value:)`` +- ``Database/jsonGroupArray(_:filter:)`` +- ``Database/jsonGroupObject(key:value:filter:)`` ### Modify JSON values at the SQL level diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift index 2fd86aae22..0972c2509c 100644 --- a/GRDB/JSON/SQLJSONFunctions.swift +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -373,6 +373,19 @@ extension Database { /// The `JSON_GROUP_ARRAY` SQL function. /// + /// For example: + /// + /// ```swift + /// // SELECT JSON_GROUP_ARRAY(name) FROM player + /// Player.select(Database.jsonGroupArray(Column("name"))) + /// + /// // SELECT JSON_GROUP_ARRAY(name) FILTER (WHERE score > 0) FROM player + /// Player.select(Database.jsonGroupArray(Column("name"), filter: Column("score") > 0)) + /// + /// // SELECT JSON_GROUP_ARRAY(name ORDER BY name) FROM player + /// Player.select(Database.jsonGroupArray(Column("name"), orderBy: Column("name"))) + /// ``` + /// /// Related SQLite documentation: public static func jsonGroupArray( _ value: some SQLExpressible, @@ -389,6 +402,21 @@ extension Database { /// The `JSON_GROUP_OBJECT` SQL function. /// + /// For example: + /// + /// ```swift + /// // SELECT JSON_GROUP_OBJECT(name, score) FROM player + /// Player.select(Database.jsonGroupObject( + /// key: Column("name"), + /// value: Column("score"))) + /// + /// // SELECT JSON_GROUP_OBJECT(name, score) FILTER (WHERE score > 0) FROM player + /// Player.select(Database.jsonGroupObject( + /// key: Column("name"), + /// value: Column("score"), + /// filter: Column("score") > 0)) + /// ``` + /// /// Related SQLite documentation: public static func jsonGroupObject( key: some SQLExpressible, @@ -796,6 +824,16 @@ extension Database { /// The `JSON_GROUP_ARRAY` SQL function. /// + /// For example: + /// + /// ```swift + /// // SELECT JSON_GROUP_ARRAY(name) FROM player + /// Player.select(Database.jsonGroupArray(Column("name"))) + /// + /// // SELECT JSON_GROUP_ARRAY(name) FILTER (WHERE score > 0) FROM player + /// Player.select(Database.jsonGroupArray(Column("name"), filter: Column("score") > 0)) + /// ``` + /// /// Related SQLite documentation: @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonGroupArray( @@ -811,6 +849,21 @@ extension Database { /// The `JSON_GROUP_OBJECT` SQL function. /// + /// For example: + /// + /// ```swift + /// // SELECT JSON_GROUP_OBJECT(name, score) FROM player + /// Player.select(Database.jsonGroupObject( + /// key: Column("name"), + /// value: Column("score"))) + /// + /// // SELECT JSON_GROUP_OBJECT(name, score) FILTER (WHERE score > 0) FROM player + /// Player.select(Database.jsonGroupObject( + /// key: Column("name"), + /// value: Column("score"), + /// filter: Column("score") > 0)) + /// ``` + /// /// Related SQLite documentation: @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonGroupObject( diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index e7579b4735..3613bdb16c 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -2144,6 +2144,7 @@ extension SQLExpressible where Self == Column { /// /// - ``abs(_:)-5l6xp`` /// - ``average(_:)`` +/// - ``average(_:filter:)`` /// - ``capitalized`` /// - ``count(_:)`` /// - ``count(distinct:)`` @@ -2156,9 +2157,13 @@ extension SQLExpressible where Self == Column { /// - ``localizedUppercased`` /// - ``lowercased`` /// - ``min(_:)`` +/// - ``min(_:filter:)`` /// - ``max(_:)`` +/// - ``max(_:filter:)`` /// - ``sum(_:)`` +/// - ``sum(_:filter:)`` /// - ``total(_:)`` +/// - ``total(_:filter:)`` /// - ``uppercased`` /// - ``SQLDateModifier`` /// From 225d7a8c2d52ebc16df568833af4939fffef0e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 2 Nov 2023 14:01:48 +0100 Subject: [PATCH 4/5] Fix test for SQLCipher --- Tests/GRDBTests/JSONExpressionsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/GRDBTests/JSONExpressionsTests.swift b/Tests/GRDBTests/JSONExpressionsTests.swift index 5758037abf..666cdde8b1 100644 --- a/Tests/GRDBTests/JSONExpressionsTests.swift +++ b/Tests/GRDBTests/JSONExpressionsTests.swift @@ -1245,7 +1245,7 @@ final class JSONExpressionsTests: GRDBTestCase { #if GRDBCUSTOMSQLITE || GRDBCIPHER func test_Database_jsonGroupArray_order() throws { // Prevent SQLCipher failures - guard sqlite3_libversion_number() >= 3038000 else { + guard sqlite3_libversion_number() >= 3044000 else { throw XCTSkip("JSON support is not available") } From f0fe5ff017b16fac8792142302dae02bde0b0a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 2 Nov 2023 14:08:15 +0100 Subject: [PATCH 5/5] TODO GRDB7 --- GRDB/Core/DatabaseFunction.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/GRDB/Core/DatabaseFunction.swift b/GRDB/Core/DatabaseFunction.swift index 4b13c39c27..81fe825748 100644 --- a/GRDB/Core/DatabaseFunction.swift +++ b/GRDB/Core/DatabaseFunction.swift @@ -144,6 +144,7 @@ public final class DatabaseFunction: Hashable { self.kind = .aggregate { Aggregate() } } + // TODO: GRDB7 -> expose ORDER BY and FILTER when we have distinct types for simple functions and aggregates. /// Returns an SQL expression that applies the function. /// /// You can use a `DatabaseFunction` as a regular Swift function. It returns