/// An SQL expression. /// /// `SQLExpression` is an opaque representation of an SQL expression. /// You generally build `SQLExpression` from other expressions. For example: /// /// ```swift /// // Values /// 1000.sqlExpression /// "O'Brien".sqlExpression /// /// // Computed expressions /// Column("score") + Column("bonus") /// (0...1000).contains(Column("score")) /// !Column("isBlue") /// /// // Literal expression /// SQL("IFNULL(name, \(defaultName))").sqlExpression /// /// // Subquery /// Player.select(max(Column("score"))).sqlExpression /// ``` /// /// `SQLExpression` is better used as the return type of a function. For /// function arguments, prefer the ``SQLExpressible`` or /// ``SQLSpecificExpressible`` protocols. For example: /// /// ```swift /// func date(_ value: some SQLSpecificExpressible) -> SQLExpression { /// SQL("DATE(\(value))").sqlExpression /// } /// /// // SELECT * FROM "player" WHERE DATE("createdAt") = '2020-01-23' /// let request = Player.filter(date(Column("createdAt")) == "2020-01-23") /// ``` /// /// Related SQLite documentation: public struct SQLExpression { private var impl: Impl /// The preferred interpretation of the expression in JSON /// building contexts (see `jsonBuilderExpression`). /// /// ```swift /// // Considering: /// // JSON_ARRAY('[1, 2, 3]') → '["[1, 2, 3]"]' /// // JSON_ARRAY(JSON('[1, 2, 3]')) → [[1,2,3]] /// /// // Compare an expression with preferredJSONInterpretation = .unspecified: /// // JSON_ARRAY("info") /// Database.jsonArray([Column("info")]) /// /// // ...with an expression with preferredJSONInterpretation = .jsonValue: /// // JSON_ARRAY(JSON("info")) /// Database.jsonArray([Column("info").jsonValue]) /// Database.jsonArray([JSONColumn("info")]) /// ``` var preferredJSONInterpretation = JSONInterpretation.deferredToSQLite /// The private implementation of the public `SQLExpression`. private enum Impl { /// A column. /// /// id /// name case column(String) /// A qualified column. /// /// player.id /// player.name case qualifiedColumn(String, TableAlias) /// A database value. /// /// NULL /// 42 /// 'Alice' /// 3.14 /// case databaseValue(DatabaseValue) /// A [row value](https://www.sqlite.org/rowvalue.html). /// /// - precondition: expressions.count > 0 case rowValue([SQLExpression]) /// A subquery expression case subquery(SQLSubquery) /// A literal SQL expression case literal(SQL) /// The `CAST(expr AS storage-class)` expression. /// /// See . indirect case cast(SQLExpression, Database.StorageClass) /// The `BETWEEN` and `NOT BETWEEN` operators. /// /// BETWEEN AND indirect case between( expression: SQLExpression, lowerBound: SQLExpression, upperBound: SQLExpression, isNegated: Bool) /// A binary operator. /// /// * /// <= indirect case binary(BinaryOperator, SQLExpression, SQLExpression) /// An escapable binary operator. /// /// LIKE /// LIKE ESCAPE indirect case escapableBinary(EscapableBinaryOperator, SQLExpression, SQLExpression, escape: SQLExpression?) /// An associative binary operator. /// /// AND /// + + /// /// - precondition: expressions.count > 1 case associativeBinary(AssociativeBinaryOperator, [SQLExpression]) /// The `EXISTS` and `NOT EXISTS` operators. /// /// EXISTS () case exists(SQLSubquery, isNegated: Bool) /// The `IN` and `NOT IN` operators. /// /// IN /// NOT IN indirect case `in`(SQLExpression, SQLCollection, isNegated: Bool) /// An unary operator. /// /// - indirect case unary(UnaryOperator, SQLExpression) /// An equality comparison. /// /// = /// <> /// IS /// IS NOT indirect case compare(EqualityOperator, SQLExpression, SQLExpression) /// A table full-text match. /// /// MATCH indirect case tableMatch(TableAlias, SQLExpression) /// A logical `NOT` operator. /// /// NOT indirect case not(SQLExpression) /// A collated expression. /// /// COLLATE indirect case collated(SQLExpression, Database.CollationName) /// The `COUNT(*)` expression. case countAll /// A function call. /// /// (, ...) /// (DISTINCT ) indirect case simpleFunction(SQLSimpleFunctionInvocation) /// An aggregate function call. /// /// (, ...) /// (DISTINCT ) indirect case aggregateFunction(SQLAggregateFunctionInvocation) /// An expression that checks for zero or positive values. /// /// = 0 /// > 0 indirect case isEmpty(SQLExpression, isNegated: Bool) /// An expression that picks the fastest available primary key. /// /// It crashes for WITHOUT ROWID table with a multi-columns primary key. /// Future versions of GRDB may use [row values](https://www.sqlite.org/rowvalue.html). /// /// id /// rowid /// code case fastPrimaryKey /// A qualified "fast primary key" (see `.fastPrimaryKey`). /// /// player.id /// document.rowid /// country.code case qualifiedFastPrimaryKey(TableAlias) /// An expression that is true iff the row exists: /// /// - For tables with a rowid, it is true iff the rowid is not null. /// - For tables WITHOUT ROWID, it is true iff any primary key column is not null. /// - For views, it is true iff any column is not null. /// - For CTEs, it is not implemented yet. case qualifiedExists(TableAlias, isNegated: Bool) /// Returns a qualified expression func qualified(with alias: TableAlias) -> Impl { switch self { case .databaseValue, .qualifiedColumn, .qualifiedFastPrimaryKey, .qualifiedExists, .subquery, .exists: return self case let .column(name): return .qualifiedColumn(name, alias) case let .rowValue(expressions): return .rowValue(expressions.map { $0.qualified(with: alias) }) case let .literal(sqlLiteral): return .literal(sqlLiteral.qualified(with: alias)) case let .cast(expression, storageClass): return .cast(expression.qualified(with: alias), storageClass) case let .between( expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: isNegated): return .between( expression: expression.qualified(with: alias), lowerBound: lowerBound.qualified(with: alias), upperBound: upperBound.qualified(with: alias), isNegated: isNegated) case let .binary(op, lhs, rhs): return .binary(op, lhs.qualified(with: alias), rhs.qualified(with: alias)) case let .escapableBinary(op, lhs, rhs, escape): return .escapableBinary( op, lhs.qualified(with: alias), rhs.qualified(with: alias), escape: escape?.qualified(with: alias)) case let .associativeBinary(op, expressions): return .associativeBinary(op, expressions.map { $0.qualified(with: alias) }) case let .in(expression, collection, isNegated: isNegated): return .in( expression.qualified(with: alias), collection.qualified(with: alias), isNegated: isNegated ) case let .unary(op, expression): return .unary(op, expression.qualified(with: alias)) case let .compare(op, lhs, rhs): return .compare(op, lhs.qualified(with: alias), rhs.qualified(with: alias)) case let .tableMatch(a, expression): return .tableMatch(a, expression.qualified(with: alias)) case let .not(expression): return .not(expression.qualified(with: alias)) case let .collated(expression, collationName): return .collated(expression.qualified(with: alias), collationName) case .countAll: return .countAll 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) case .fastPrimaryKey: return .qualifiedFastPrimaryKey(alias) } } } /// `BooleanTest` supports truthiness tests. /// /// See `SQLExpression.is(_:)` enum BooleanTest { /// Fuels `expression == true` case `true` /// Fuels `expression == false` case `false` /// Fuels `!expression` case falsey } /// An associative binary SQL operator, such as `+`, `*`, `AND`, etc. /// /// Use it with the `joined(operator:)` method. For example: /// /// ```swift /// // SELECT score + bonus + 1000 FROM player /// let values = [ /// scoreColumn, /// bonusColumn, /// 1000.databaseValue] /// let request = Player.select(values.joined(operator: .add)) /// ``` public struct AssociativeBinaryOperator: Hashable, Sendable { /// The SQL operator let sql: String /// The neutral value let neutralValue: DatabaseValue /// If true, (a • b) • c is strictly equal to a • (b • c). /// /// `AND`, `OR`, `||` (concat), `&`, `|` are stricly associative. /// /// `+` and `*` are not stricly associative when applied to floating /// point values. let isStrictlyAssociative: Bool /// If true, (a • b) is a bijective function of a, and a bijective /// function of b. /// /// `||` (concat) is bijective. /// /// `AND`, `OR`, `+` and `*`, `&`, `|` are not. let isBijective: Bool /// Creates a binary operator init(sql: String, neutralValue: DatabaseValue, strictlyAssociative: Bool, bijective: Bool) { self.sql = sql self.neutralValue = neutralValue self.isStrictlyAssociative = strictlyAssociative self.isBijective = bijective } /// The `+` binary SQL operator. /// /// For example: /// /// ```swift /// // score + bonus /// [Column("score"), Column("bonus")].joined(operator: .add) /// ``` public static let add = AssociativeBinaryOperator( sql: "+", neutralValue: 0.databaseValue, strictlyAssociative: false, bijective: false) /// The `*` binary SQL operator. /// /// For example: /// /// ```swift /// // score * factor /// [Column("score"), Column("factor")].joined(operator: .multiply) /// ``` public static let multiply = AssociativeBinaryOperator( sql: "*", neutralValue: 1.databaseValue, strictlyAssociative: false, bijective: false) /// The `AND` binary SQL operator. /// /// For example: /// /// ```swift /// // isBlue AND isTall /// [Column("isBlue"), Column("isTall")].joined(operator: .and) /// ``` public static let and = AssociativeBinaryOperator( sql: "AND", neutralValue: true.databaseValue, strictlyAssociative: true, bijective: false) /// The `OR` binary SQL operator. /// /// For example: /// /// ```swift /// // isBlue OR isTall /// [Column("isBlue"), Column("isTall")].joined(operator: .or) /// ``` public static let or = AssociativeBinaryOperator( sql: "OR", neutralValue: false.databaseValue, strictlyAssociative: true, bijective: false) /// The `||` string concatenation SQL operator. /// /// For example: /// /// ```swift /// // firstName || ' ' || lastName /// [Column("firstName"), " ", Column("lastName")].joined(operator: .concat) /// ``` public static let concat = AssociativeBinaryOperator( sql: "||", neutralValue: "".databaseValue, strictlyAssociative: true, bijective: true) /// The `&` bitwise AND SQL operator. /// /// For example: /// /// ```swift /// // mask & 2 /// [Column("mask"), 2.databaseValue].joined(operator: .bitwiseAnd) /// ``` public static let bitwiseAnd = AssociativeBinaryOperator( sql: "&", neutralValue: (-1).databaseValue, strictlyAssociative: true, bijective: false) /// The `|` bitwise OR SQL operator. /// /// For example: /// /// ```swift /// // mask | 2 /// [Column("mask"), 2.databaseValue].joined(operator: .bitwiseOr) /// ``` public static let bitwiseOr = AssociativeBinaryOperator( sql: "|", neutralValue: 0.databaseValue, strictlyAssociative: true, bijective: false) } /// `BinaryOperator` is an SQLite binary operator, such as `>`, `=`, etc. /// /// See also `AssociativeBinaryOperator`, `EqualityOperator`, /// `EscapableBinaryOperator`. struct BinaryOperator: Hashable { /// The SQL operator let sql: String /// A boolean value indicating if the operator is known to return a /// JSON value. /// /// A false value does not provide any information. let isJSONValue: Bool /// Creates a binary operator. /// /// For example: /// /// ``` /// BinaryOperator("-") /// ``` init(_ sql: String, isJSONValue: Bool = false) { self.sql = sql self.isJSONValue = isJSONValue } /// The `<` binary operator static let lessThan = BinaryOperator("<") /// The `<=` binary operator static let lessThanOrEqual = BinaryOperator("<=") /// The `>` binary operator static let greaterThan = BinaryOperator(">") /// The `>=` binary operator static let greaterThanOrEqual = BinaryOperator(">=") /// The `-` binary operator static let subtract = BinaryOperator("-") /// The `/` binary operator static let divide = BinaryOperator("/") /// The `MATCH` binary operator static let match = BinaryOperator("MATCH") /// The `<<` bitwise left shift operator static let leftShift = BinaryOperator("<<") /// The `>>` bitwise right shift operator static let rightShift = BinaryOperator(">>") #if GRDBCUSTOMSQLITE || GRDBCIPHER /// The `->` SQL operator static let jsonExtractJSON = BinaryOperator("->", isJSONValue: true) /// The `->>` SQL operator static let jsonExtractSQL = BinaryOperator("->>") #else /// The `->` SQL operator @available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+ static let jsonExtractJSON = BinaryOperator("->", isJSONValue: true) /// The `->>` SQL operator @available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+ static let jsonExtractSQL = BinaryOperator("->>") #endif } /// `EscapableBinaryOperator` is an SQLite binary operator that accepts an /// `ESCAPE` clause, such as `LIKE`, etc. /// /// See also `AssociativeBinaryOperator`, `EqualityOperator`, /// `BinaryOperator`. struct EscapableBinaryOperator { /// The SQL operator let sql: String /// The SQL for the negated operator let negatedSQL: String /// Creates a binary operator /// /// BinaryOperator("LIKE", negated: "NOT LIKE") init(_ sql: String, negated: String) { self.sql = sql self.negatedSQL = negated } /// Returns the negated binary operator, if any /// /// let operator = BinaryOperator("LIKE", negated: "NOT LIKE") /// operator.negated!.sql // NOT LIKE var negated: EscapableBinaryOperator { EscapableBinaryOperator(negatedSQL, negated: sql) } /// The `LIKE` escapable binary operator static let like = EscapableBinaryOperator("LIKE", negated: "NOT LIKE") } /// `EqualityOperator` is an SQLite equality operator. enum EqualityOperator: String { case equal = "=" case notEqual = "<>" case `is` = "IS" case isNot = "IS NOT" var negated: EqualityOperator { switch self { case .equal: return .notEqual case .notEqual: return .equal case .is: return .isNot case .isNot: return .is } } } /// `UnaryOperator` is a SQLite unary operator. struct UnaryOperator: Hashable { /// The SQL operator let sql: String /// Creates an unary operator. init(_ sql: String) { self.sql = sql } /// The `-` unary operator static let minus = UnaryOperator("-") /// The `~` unary operator static let bitwiseNot = UnaryOperator("~") } /// Describes the interpretation of an expression in a JSON /// building context. enum JSONInterpretation { /// JSON interpretation is deferred to SQLite: /// /// ```swift /// // JSON_ARRAY('[1, 2, 3]') → '["[1, 2, 3]"]' /// Database.jsonArray(["[1, 2, 3]"]) /// /// // JSON_ARRAY(JSON('[1, 2, 3]')) → '[[1, 2, 3]]' /// Database.jsonArray([Database.json("[1, 2, 3]")]) /// /// // JSON_ARRAY("info") /// Database.jsonArray([Column("info")]) /// ``` case deferredToSQLite /// Expression is interpreted as a JSON value: /// /// ```swift /// // JSON_ARRAY(JSON('[1, 2, 3]')) → '[[1, 2, 3]]' /// Database.jsonArray(["[1, 2, 3]"].jsonValue) /// /// // JSON_ARRAY(JSON("info")) /// Database.jsonArray([Column("info").jsonValue]) /// Database.jsonArray([JSONColumn("info")]) /// ``` case jsonValue } } // MARK: - Creating Expressions extension SQLExpression { // MARK: Basic Expressions /// A column. /// /// id /// name static func column(_ name: String) -> Self { self.init(impl: .column(name)) } /// A qualified column. /// /// player.id /// player.name static func qualifiedColumn(_ name: String, _ alias: TableAlias) -> Self { self.init(impl: .qualifiedColumn(name, alias)) } /// A database value. /// /// NULL /// 42 /// 'Alice' /// 3.14 /// /// /// See also `SQLExpression.null` static func databaseValue(_ dbValue: DatabaseValue) -> Self { self.init(impl: .databaseValue(dbValue)) } /// The `NULL` expression. static let null = SQLExpression.databaseValue(.null) /// A [row value](https://www.sqlite.org/rowvalue.html). /// /// Returns nil if and only if expressions is empty. static func rowValue(_ expressions: [SQLExpression]) -> Self? { guard let expression = expressions.first else { return nil } if expressions.count == 1 { return expression } return self.init(impl: .rowValue(expressions)) } /// A subquery expression. static func subquery(_ subquery: SQLSubquery) -> Self { self.init(impl: .subquery(subquery)) } /// A literal SQL expression. static func literal(_ sqlLiteral: SQL) -> Self { self.init(impl: .literal(sqlLiteral)) } // MARK: Operators /// The `BETWEEN` and `NOT BETWEEN` operators. /// /// BETWEEN AND static func between( expression: SQLExpression, lowerBound: SQLExpression, upperBound: SQLExpression, isNegated: Bool = false) -> Self { if case let .collated(expression, collationName) = expression.impl { // Prefer: expression BETWEEN lowerBound AND upperBound COLLATE collation // over: (expression COLLATE collation) BETWEEN lowerBound AND upperBound // // This transformation was introduced in GRDB v0.42.0, for the first // release of the query interface: // https://github.com/groue/GRDB.swift/blob/3b3cb6bdecdfaac6e3d55bb7ecccf22f2749140f/GRDB/FetchRequest/SQLSupport/Collation.swift#L224-L239 // The commit is a big squash, and we've lost the original intent. // It is likely just an SQL aesthetic preference of mine. // // According to https://www.sqlite.org/datatype3.html#assigning_collating_sequences_from_sql // this rewriting should not have any functional impact. Yet if any // user complains eventually, we should just remove this rewriting // rule without any resistance. return collated(between( expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: isNegated), collationName) } else { return self.init(impl: .between( expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: isNegated)) } } /// A binary operator. /// /// * /// <= /// LIKE static func binary(_ op: BinaryOperator, _ lhs: SQLExpression, _ rhs: SQLExpression) -> Self { // See `between(expression:lowerBound:upperBound:isNegated:)` for some // explanation of these rewriting rules. if case let .collated(lhs, collationName) = lhs.impl { // Prefer: lhs <= rhs COLLATE collation // over: (lhs COLLATE collation) <= rhs return collated(binary(op, lhs, rhs), collationName) } else if case let .collated(rhs, collationName) = rhs.impl { // Prefer: lhs <= rhs COLLATE collation // over: lhs <= (rhs COLLATE collation) return collated(binary(op, lhs, rhs), collationName) } else { return self.init(impl: .binary(op, lhs, rhs)) } } /// An escapable binary operator. /// /// LIKE /// LIKE ESCAPE static func escapableBinary( _ op: EscapableBinaryOperator, _ lhs: SQLExpression, _ rhs: SQLExpression, escape: SQLExpression?) -> Self { self.init(impl: .escapableBinary(op, lhs, rhs, escape: escape)) } /// An associative binary operator. /// /// AND /// + + /// /// When the `expressions` array is empty, returns the neutral value of /// the operator. /// /// When the `expressions` array contains a single expression, returns /// this expression. /// /// When the operator is strictly associative, the expressions /// are flattened: /// /// (a AND b) AND c -> a AND b AND c static func associativeBinary(_ op: AssociativeBinaryOperator, _ expressions: [SQLExpression]) -> Self { // flatten when possible: a • (b • c) = a • b • c var expressions = expressions if op.isStrictlyAssociative { expressions = expressions.flatMap { expression -> [SQLExpression] in if case .associativeBinary(op, let expressions) = expression.impl { return expressions } else { return [expression] } } } guard let expression = expressions.first else { return op.neutralValue.sqlExpression } if expressions.count == 1 { return expression } return self.init(impl: .associativeBinary(op, expressions)) } /// The `EXISTS` operator. /// /// EXISTS () static func exists(_ subquery: SQLSubquery) -> Self { self.init(impl: .exists(subquery, isNegated: false)) } /// The `IN` and `NOT IN` operators. /// /// IN /// NOT IN /// /// See also `SQLCollection.contains(_:)`. static func `in`(_ expression: SQLExpression, _ collection: SQLCollection, isNegated: Bool = false) -> Self { self.init(impl: .in(expression, collection, isNegated: isNegated)) } /// An unary operator. /// /// - static func unary(_ op: UnaryOperator, _ expression: SQLExpression) -> Self { self.init(impl: .unary(op, expression)) } /// An equality comparison. /// /// = /// <> /// IS /// IS NOT /// /// See also `SQLExpression.equal(_:_:)`. static func compare(_ op: EqualityOperator, _ lhs: SQLExpression, _ rhs: SQLExpression) -> Self { // See `between(expression:lowerBound:upperBound:isNegated:)` for some // explanation of these rewriting rules. if case let .collated(lhs, collationName) = lhs.impl { // Prefer: lhs = rhs COLLATE collation // over: (lhs COLLATE collation) = rhs return collated(compare(op, lhs, rhs), collationName) } else if case let .collated(rhs, collationName) = rhs.impl { // Prefer: lhs = rhs COLLATE collation // over: lhs = (rhs COLLATE collation) return collated(compare(op, lhs, rhs), collationName) } else { return self.init(impl: .compare(op, lhs, rhs)) } } /// An equality comparison. Null database values are checked with `IS NULL`. /// /// = /// IS NULL /// IS NULL static func equal(_ lhs: SQLExpression, _ rhs: SQLExpression) -> Self { switch (lhs.impl, rhs.impl) { case let (impl, .databaseValue(.null)), let (.databaseValue(.null), impl): // ... IS NULL return compare(.is, SQLExpression(impl: impl), .null) default: // lhs = rhs return compare(.equal, lhs, rhs) } } /// A table full-text match. /// ///
MATCH static func tableMatch(_ alias: TableAlias, _ expression: SQLExpression) -> Self { self.init(impl: .tableMatch(alias, expression)) } /// A logical `NOT` operator. /// /// NOT static func not(_ expression: SQLExpression) -> Self { self.init(impl: .not(expression)) } /// A collated expression. /// /// COLLATE static func collated(_ expression: SQLExpression, _ collationName: Database.CollationName) -> Self { switch expression.impl { case let .in(expression, collection, isNegated: isNegated): // According to https://www.sqlite.org/datatype3.html#assigning_collating_sequences_from_sql // // > The collating sequence used for expressions of the form // > "x IN (y, z, ...)" is the collating sequence of x. If an // > explicit collating sequence is required on an IN operator it // > should be applied to the left operand, like this: // > "x COLLATE nocase IN (y,z, ...)". // // Indeed: // // $ sqlite3 // SQLite version 3.32.3 2020-06-18 14:16:19 // sqlite> SELECT 'a' IN ('A') COLLATE NOCASE; // 0 // sqlite> SELECT ('a' COLLATE NOCASE) IN ('A'); // 1 // // Conclusion: "x IN (y,z, ...) COLLATE nocase" can not match the // user intent. We could fatal error. Or warn. Or just make it work: // // Prefer: (expression COLLATE collation) IN (...) // over: expression IN (...) COLLATE collation return .in(.collated(expression, collationName), collection, isNegated: isNegated) case let .associativeBinary(op, expressions): // The expression rewrite performed for the `IN` operator above // allows the user to have the following Swift code match the intent: // // // name COLLATE NOCASE IN ('foo', 'bar') // ["foo", "bar"].contains(Column("name")).collating(.nocase) // ["foo", "bar"].contains(Column("name").collating(.nocase)) // // The BETWEEN case is supported as well (see // `between(expression:lowerBound:upperBound:isNegated:)`): // // // name BETWEEN 'foo' AND 'bar' COLLATE NOCASE // ("foo"..."bar").contains(Column("name")).collating(.nocase) // ("foo"..."bar").contains(Column("name").collating(.nocase)) // // We just miss support for non-closed ranges: // // // (name >= 'foo' COLLATE NOCASE) AND (name < 'bar' COLLATE NOCASE) // ("foo"..<"bar").contains(Column("name")).collating(.nocase) // ("foo"..<"bar").contains(Column("name").collating(.nocase)) return .associativeBinary(op, expressions.map { $0.collating(collationName) }) default: return self.init(impl: .collated(expression, collationName)) } } // 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() static func count(_ expression: SQLExpression) -> Self { function("COUNT", [expression]) } /// The `COUNT(DISTINCT)` function. /// /// COUNT(DISTINCT ) static func countDistinct(_ expression: SQLExpression) -> Self { aggregateFunction("COUNT", [expression], isDistinct: true) } /// An expression that checks for zero or positive values. /// /// = 0 /// > 0 static func isEmpty(_ expression: SQLExpression, isNegated: Bool = false) -> Self { self.init(impl: .isEmpty(expression, isNegated: isNegated)) } /// The `CAST(expr AS storage-class)` expression. /// /// See . static func cast(_ expression: SQLExpression, as storageClass: Database.StorageClass) -> Self { self.init(impl: .cast(expression, storageClass)) } // MARK: Deferred // TODO: replace with something that can work for WITHOUT ROWID table with a multi-columns primary key. /// An expression that picks the fastest available primary key. /// /// It crashes for WITHOUT ROWID table with a multi-columns primary key. /// /// id /// rowid /// code static let fastPrimaryKey = SQLExpression(impl: .fastPrimaryKey) // TODO: replace with something that can work for WITHOUT ROWID table with a multi-columns primary key. /// A qualified "fast primary key" (see `SQLExpression.fastPrimaryKey`). /// /// It crashes for WITHOUT ROWID table with a multi-columns primary key. /// /// player.id /// document.rowid /// country.code static func qualifiedFastPrimaryKey(_ alias: TableAlias) -> Self { self.init(impl: .qualifiedFastPrimaryKey(alias)) } /// An expression that is true iff the row exists: /// /// - For tables with a rowid, it is true iff the rowid is not null. /// - For tables WITHOUT ROWID, it is true iff any primary key column is not null. /// - For views, it is true iff any column is not null. /// - For CTEs, it is not implemented yet. static func qualifiedExists(_ alias: TableAlias) -> Self { self.init(impl: .qualifiedExists(alias, isNegated: false)) } } // MARK: - Deriving Expressions extension SQLExpression { /// Returns a qualified expression func qualified(with alias: TableAlias) -> Self { .init(impl: impl.qualified(with: alias), preferredJSONInterpretation: preferredJSONInterpretation) } func withPreferredJSONInterpretation(_ interpretation: JSONInterpretation) -> Self { .init(impl: impl, preferredJSONInterpretation: interpretation) } } // MARK: - Expressions Information extension SQLExpression { /// The expression as a quoted SQL literal (not public in order to avoid abuses) /// /// try "foo'bar".databaseValue.quotedSQL(db) // "'foo''bar'"" func quotedSQL(_ db: Database) throws -> String { let context = SQLGenerationContext(db, argumentsSink: .literalValues) return try sql(context) } /// If this expression is a table column, returns the name of this column. /// /// When in doubt, returns nil. /// /// This method makes it possible to avoid inserting `LIMIT 1` to the SQL /// of some requests: /// /// // SELECT * FROM "player" WHERE "id" = 1 /// try Player.fetchOne(db, key: 1) /// try Player.filter(Column("id") == 1).fetchOne(db) /// /// // SELECT * FROM "player" WHERE "name" = 'Arthur' LIMIT 1 /// try Player.filter(Column("name") == "Arthur").fetchOne(db) /// /// This method makes it possible to track individual rows identified by /// their row ids, and ignore modifications to other rows: /// /// // Track rows 1, 2, 3 only /// let request = Player.filter(keys: [1, 2, 3]) /// let regionObservation = DatabaseRegionObservation(tracking: request) /// let valueObservation = ValueObservation.tracking(request.fetchAll) /// /// - parameter acceptsBijection: If true, expressions that define a /// bijection on a column return this column. For example: `-score` /// returns `score`. func column(_ db: Database, for alias: TableAlias, acceptsBijection: Bool = false) throws -> String? { switch impl { case let .qualifiedColumn(name, a): if alias == a { return name } else { return nil } case let .associativeBinary(op, expressions): assert(expressions.count > 1) guard acceptsBijection && op.isBijective else { return nil } let nonConstants = expressions.filter { $0.isConstantInRequest == false } if nonConstants.count == 1 { return try nonConstants[0].column(db, for: alias, acceptsBijection: acceptsBijection) } return nil case let .unary(op, expression): if acceptsBijection && op == .minus { return try expression.column(db, for: alias, acceptsBijection: acceptsBijection) } return nil case let .collated(expression, _): return try expression.column(db, for: alias, acceptsBijection: acceptsBijection) case let .simpleFunction(invocation) where acceptsBijection: return try invocation.column(db, for: alias) case let .qualifiedFastPrimaryKey(a): if alias == a { return try db.primaryKey(alias.tableName).fastPrimaryKeyColumn } return nil default: return nil } } /// Returns an SQL string that represents the expression. /// /// - parameter context: An SQL generation context which accepts /// statement arguments. /// - parameter wrappedInParenthesis: If true, the returned SQL should be /// wrapped inside parenthesis. func sql(_ context: SQLGenerationContext, wrappedInParenthesis: Bool = false) throws -> String { switch impl { case let .column(name): return name.quotedDatabaseIdentifier case let .qualifiedColumn(name, alias): if let qualifier = context.qualifier(for: alias) { return qualifier.quotedDatabaseIdentifier + "." + name.quotedDatabaseIdentifier } return name.quotedDatabaseIdentifier case let .databaseValue(dbValue): if dbValue.isNull { // fast path for NULL return "NULL" } else if context.append(arguments: [dbValue]) { // Use statement arguments return "?" } else { // Quoting needed: just use SQLite, which knows better. return try String.fetchOne(context.db, sql: "SELECT QUOTE(?)", arguments: [dbValue])! } case let .rowValue(expressions): assert(!expressions.isEmpty) let values = try expressions.map { try $0.sql(context) } return "(" + values.joined(separator: ", ") + ")" case let .subquery(subquery): return try "(" + subquery.sql(context) + ")" case let .literal(sqlLiteral): var resultSQL = try sqlLiteral.sql(context) if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .cast(expression, storageClass): return try "CAST(\(expression.sql(context, wrappedInParenthesis: false)) AS \(storageClass.rawValue))" case let .between(expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: isNegated): var resultSQL = try """ \(expression.sql(context, wrappedInParenthesis: true)) \ \(isNegated ? "NOT BETWEEN" : "BETWEEN") \ \(lowerBound.sql(context, wrappedInParenthesis: true)) \ AND \ \(upperBound.sql(context, wrappedInParenthesis: true)) """ if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .binary(op, lhs, rhs): var resultSQL = try """ \(lhs.sql(context, wrappedInParenthesis: true)) \ \(op.sql) \ \(rhs.sql(context, wrappedInParenthesis: true)) """ if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .escapableBinary(op, lhs, rhs, escape): var resultSQL = try """ \(lhs.sql(context, wrappedInParenthesis: true)) \ \(op.sql) \ \(rhs.sql(context, wrappedInParenthesis: true)) """ if let escape { resultSQL += try " ESCAPE \(escape.sql(context, wrappedInParenthesis: true))" } if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .associativeBinary(op, expressions): assert(expressions.count > 1) let expressionSQLs = try expressions.map { try $0.sql(context, wrappedInParenthesis: true) } let joiner = " \(op.sql) " var resultSQL = expressionSQLs.joined(separator: joiner) if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .exists(subquery, isNegated: isNegated): var resultSQL = try """ \(isNegated ? "NOT EXISTS" : "EXISTS") \ (\(subquery.sql(context))) """ if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .in(expression, collection, isNegated: isNegated): var resultSQL = try """ \(expression.sql(context, wrappedInParenthesis: true)) \ \(isNegated ? "NOT IN" : "IN") \ \(collection.sql(context)) """ if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .unary(op, expression): var resultSQL = try op.sql + expression.sql(context, wrappedInParenthesis: true) if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .compare(op, lhs, rhs): var resultSQL = try """ \(lhs.sql(context, wrappedInParenthesis: true)) \ \(op.rawValue) \ \(rhs.sql(context, wrappedInParenthesis: true)) """ if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .tableMatch(alias, expression): var resultSQL = try """ \(context.resolvedName(for: alias).quotedDatabaseIdentifier) \ MATCH \ \(expression.sql(context, wrappedInParenthesis: true)) """ if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .not(expression): var resultSQL = try "NOT \(expression.sql(context, wrappedInParenthesis: true))" if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case let .collated(expression, collationName): var resultSQL = try """ \(expression.sql(context)) \ COLLATE \ \(collationName.rawValue) """ if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case .countAll: return "COUNT(*)" 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 """ \(expression.sql(context, wrappedInParenthesis: true)) \ \(isNegated ? "> 0" : "= 0") """ if wrappedInParenthesis { resultSQL = "(\(resultSQL))" } return resultSQL case .fastPrimaryKey: // Likely a GRDB bug: how comes this expression is used before it // has been qualified? fatalError("SQLExpression.fastPrimaryKey is not qualified.") case let .qualifiedFastPrimaryKey(alias): let primaryKey = try context.db.primaryKey(alias.tableName) guard let column = primaryKey.fastPrimaryKeyColumn else { fatalError("Not implemented for WITHOUT ROWID table with a multi-columns primary key") } return try SQLExpression .qualifiedColumn(column, alias) .sql(context, wrappedInParenthesis: wrappedInParenthesis) case let .qualifiedExists(alias, isNegated: isNegated): // Works with tables and views. // TODO: add support for CTEs eventually. let existenceCheckColumns = try context.db.existenceCheckColumns(in: alias.tableName) if isNegated { return try existenceCheckColumns .map { SQLExpression.qualifiedColumn($0, alias) == nil } .joined(operator: .and) .sql(context, wrappedInParenthesis: wrappedInParenthesis) } else { return try existenceCheckColumns .map { SQLExpression.qualifiedColumn($0, alias) != nil } .joined(operator: .or) .sql(context, wrappedInParenthesis: wrappedInParenthesis) } } } /// Returns the columns that identify a unique row in the request /// /// When in doubt, returns an empty set. /// /// WHERE 0 -- [] /// WHERE a -- [] /// WHERE a = b -- [] /// WHERE a = 1 -- ["a"] /// WHERE a = 1 AND b = 2 -- ["a", "b"] /// WHERE a = 1 AND b = 2 AND c > 0 -- ["a", "b"] /// WHERE a = 1 OR a = 2 -- [] /// WHERE a > 1 -- [] /// /// This method makes it possible to avoid inserting `LIMIT 1` to the SQL /// of some requests: /// /// // SELECT * FROM "player" WHERE "id" = 1 /// try Player.fetchOne(db, key: 1) /// try Player.filter(Column("id") == 1).fetchOne(db) /// /// // SELECT * FROM "player" WHERE "name" = 'Arthur' LIMIT 1 /// try Player.filter(Column("name") == "Arthur").fetchOne(db) func identifyingColums(_ db: Database, for alias: TableAlias) throws -> Set { switch impl { case let .rowValue(expressions): assert(!expressions.isEmpty) return try expressions.reduce(into: []) { try $0.formUnion($1.identifyingColums(db, for: alias)) } case let .associativeBinary(op, expressions): assert(expressions.count > 1) if op == .and { return try expressions.reduce(into: []) { try $0.formUnion($1.identifyingColums(db, for: alias)) } } else if op == .or { return [] } else { return [] } case let .compare(op, lhs, rhs): switch op { case .equal, .is: if let column = try lhs.column(db, for: alias, acceptsBijection: true), rhs.isConstantInRequest { return [column] } if let column = try rhs.column(db, for: alias, acceptsBijection: true), lhs.isConstantInRequest { return [column] } return [] case .notEqual, .isNot: return [] } case let .collated(expression, _): return try expression.identifyingColums(db, for: alias) default: return [] } } /// Returns the rowIds that identify rows in the request. A nil result means /// an unbounded list. /// /// When in doubt, returns nil. /// /// WHERE 1 -- nil /// WHERE 0 -- [] /// WHERE NULL -- [] /// WHERE id IS NULL -- [] /// WHERE id = 1 -- [1] /// WHERE id = 1 AND b = 2 -- [1] /// WHERE id = 1 OR id = 2 -- [1, 2] /// WHERE id IN (1, 2, 3) -- [1, 2, 3] /// WHERE id IN (1, 2) OR rowid IN (2, 3) -- [1, 2, 3] /// WHERE id > 1 -- nil /// /// This method makes it possible to track individual rows identified by /// their row ids, and ignore modifications to other rows: /// /// // Track rows 1, 2, 3 only /// let request = Player.filter(keys: [1, 2, 3]) /// let regionObservation = DatabaseRegionObservation(tracking: request) /// let valueObservation = ValueObservation.tracking(request.fetchAll) func identifyingRowIDs(_ db: Database, for alias: TableAlias) throws -> Set? { switch impl { case let .databaseValue(dbValue): if dbValue.isNull || dbValue == false.databaseValue { // Those requests select no row: // - WHERE NULL // - WHERE 0 return [] } return nil case let .associativeBinary(op, expressions): assert(expressions.count > 1) if op == .and { var result: Set? = nil for expression in expressions { if let expressionRowIDs = try expression.identifyingRowIDs(db, for: alias) { if var rowIDs = result { rowIDs.formIntersection(expressionRowIDs) result = rowIDs if rowIDs.isEmpty { break } } else { result = expressionRowIDs } } } return result } else if op == .or { var result: Set = [] for expression in expressions { if let expressionRowIDs = try expression.identifyingRowIDs(db, for: alias) { result.formUnion(expressionRowIDs) } else { return nil } } return result } else { return nil } case let .in(expression, collection, isNegated: false): if let expressions = collection.collectionExpressions, let column = try expression.column(db, for: alias), try db.columnIsRowID(column, of: alias.tableName) { return Set(expressions.compactMap { expression in if case let .databaseValue(dbValue) = expression.impl { return Int64.fromDatabaseValue(dbValue) } else { return nil } }) } else { return nil } case let .compare(op, lhs, rhs): switch op { case .equal, .is: if let column = try lhs.column(db, for: alias), try db.columnIsRowID(column, of: alias.tableName), case .databaseValue(let dbValue) = rhs.impl { if let rowID = Int64.fromDatabaseValue(dbValue) { return [rowID] } else { // We miss `rowid = '1'` here, because SQLite would interpret the '1' string as a number return [] } } if let column = try rhs.column(db, for: alias), try db.columnIsRowID(column, of: alias.tableName), case .databaseValue(let dbValue) = lhs.impl { if let rowID = Int64.fromDatabaseValue(dbValue) { return [rowID] } else { // We miss `rowid = '1'` here, because SQLite would interpret the '1' string as a number return [] } } return nil case .notEqual, .isNot: return nil } case let .collated(expression, _): return try expression.identifyingRowIDs(db, for: alias) default: return nil } } /// Performs a boolean test. /// /// We generally distinguish four boolean values: /// /// 1. truthy: `filter(expression)` /// 2. falsey: `filter(!expression)` /// 3. true: `filter(expression == true)` /// 4. false: `filter(expression == false)` /// /// They generally produce the following SQL: /// /// 1. truthy: `WHERE expression` /// 2. falsey: `WHERE NOT expression` /// 3. true: `WHERE expression = 1` /// 4. false: `WHERE expression = 0` /// /// The `= 1` and `= 0` tests allow the SQLite query planner to /// optimize queries with indices on boolean columns and expressions. /// See /// /// Some specific expressions can produce idiomatic SQL. /// /// For example, the `like(_)` expression: /// /// - `column.like(pattern)` -> `column LIKE pattern` /// - `!(column.like(pattern))` -> `column NOT LIKE pattern` /// - `column.like(pattern) == true` -> `(column LIKE pattern) = 1` /// - `column.like(pattern) == false` -> `(column LIKE pattern) = 0` /// /// Another example, the `isEmpty` association aggregate: /// /// - `association.isEmpty` -> `COUNT(child.id) = 0` /// - `!association.isEmpty` -> `COUNT(child.id) > 0` /// - `association.isEmpty == true` -> `COUNT(child.id) = 0` /// - `association.isEmpty == false` -> `COUNT(child.id) > 0` func `is`(_ test: BooleanTest) -> SQLExpression { switch impl { case let .databaseValue(dbValue): switch dbValue.storage { case .null: return .null case .int64(let int64) where int64 == 0 || int64 == 1: switch test { case .true: return (int64 == 1).sqlExpression case .false, .falsey: return (int64 == 0).sqlExpression } default: switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: return .not(self) } } case let .between(expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: isNegated): switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: return .between( expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: !isNegated) } case let .escapableBinary(op, lhs, rhs, escape): switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: return .escapableBinary(op.negated, lhs, rhs, escape: escape) } case let .exists(subquery, isNegated: isNegated): switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: return SQLExpression(impl: .exists(subquery, isNegated: !isNegated)) } case let .in(expression, collection, isNegated: isNegated): switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: return .in(expression, collection, isNegated: !isNegated) } case let .compare(op, lhs, rhs): switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: return .compare(op.negated, lhs, rhs) } case .not: switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: // Support `NOT (NOT expression)` as a technique to build 0 or 1 return .not(self) } case let .collated(expression, collationName): return .collated(expression.is(test), collationName) case let .isEmpty(expression, isNegated: isNegated): switch test { case .true: return self case .false, .falsey: return .isEmpty(expression, isNegated: !isNegated) } case let .qualifiedExists(alias, isNegated: isNegated): switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: return SQLExpression(impl: .qualifiedExists(alias, isNegated: !isNegated)) } default: switch test { case .true: return .compare(.equal, self, true.sqlExpression) case .false: return .compare(.equal, self, false.sqlExpression) case .falsey: return .not(self) } } } /// Returns true if the expression has a unique value when SQLite runs /// a request. /// /// When in doubt, returns false. /// /// 1 -- true /// 1 + 2 -- true /// score -- false /// /// This property supports `identifyingColums(_:for:)` var isConstantInRequest: Bool { switch impl { case .databaseValue: return true case let .rowValue(expressions), let .associativeBinary(_, expressions): return expressions.allSatisfy(\.isConstantInRequest) case let .cast(expression, _): return expression.isConstantInRequest case let .between(expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: _): return expression.isConstantInRequest && lowerBound.isConstantInRequest && upperBound.isConstantInRequest case let .binary(_, lhs, rhs), let .compare(_, lhs, rhs): return lhs.isConstantInRequest && rhs.isConstantInRequest case let .in(expression, collection, isNegated: _): guard let expressions = collection.collectionExpressions else { return false } return expression.isConstantInRequest && expressions.allSatisfy(\.isConstantInRequest) case let .unary(_, expression), let .not(expression), let .isEmpty(expression, isNegated: _), let .collated(expression, _): return expression.isConstantInRequest case let .simpleFunction(invocation): return invocation.isConstantInRequest default: return false } } /// Returns true if the expression is an aggregate. /// /// When in doubt, returns false. /// /// SELECT score -- false /// SELECT COUNT(*) -- true /// SELECT MAX(score) -- true /// SELECT MAX(score) + 1 -- true /// /// This method makes it possible to avoid inserting `LIMIT 1` to the SQL /// of some requests: /// /// // SELECT MAX("score") FROM "player" /// try Player.select(max(Column("score")), as: Int.self).fetchOne(db) /// /// // SELECT "score" FROM "player" LIMIT 1 /// try Player.select(Column("score"), as: Int.self).fetchOne(db) var isAggregate: Bool { switch impl { case let .rowValue(expressions), let .associativeBinary(_, expressions): return expressions.contains(where: \.isAggregate) case let .between(expression: expression, lowerBound: _, upperBound: _, isNegated: _), let .unary(_, expression), let .not(expression), let .collated(expression, _), let .isEmpty(expression, isNegated: _): return expression.isAggregate case let .binary(_, lhs, rhs), let .compare(_, lhs, rhs): return lhs.isAggregate || rhs.isAggregate case let .in(expression, collection, isNegated: _): if expression.isAggregate { // SELECT aggregate IN (...) return true } if let expressions = collection.collectionExpressions, expressions.contains(where: \.isAggregate) { // SELECT expr IN (aggregate, ...) return true } return false case .countAll: return true case .aggregateFunction: return true default: return false } } } /// 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: Bool /// A boolean value indicating if a function is known to return a /// JSON value. /// /// A false value does not provide any information. 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 } } 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: Bool 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) } func sql(_ context: SQLGenerationContext, wrappedInParenthesis: Bool) throws -> String { var sql = name if isDistinct { sql += "(DISTINCT " } else { sql += "(" } sql += try arguments .map { try $0.sql(context) } .joined(separator: ", ") if let ordering { sql += try " ORDER BY \(ordering.sql(context))" } sql += ")" if let filter { sql += try " FILTER (WHERE \(filter.sql(context)))" } if wrappedInParenthesis && filter != nil { return "(\(sql))" } else { return sql } } } // MARK: - JSON extension SQLExpression { /// A boolean value indicating if the expression is known to be a /// JSON value. /// /// A false value does not provide any information. /// /// For examples: /// /// ```swift /// // isJSONValue is true: /// // /// // NULL /// // JSON('[1, 2, 3]') /// // info -> 'address' /// DatabaseValue.null /// Database.json("[1, 2, 3]") /// JSONColumn("info").jsonRepresentation(forKey: "address") /// /// // isJSONValue is false /// // /// // '[1, 2, 3]' /// // info /// // info ->> 'address' /// [1, 2, 3].databaseValue /// JSONColumn("info") /// JSONColumn("info")["address"] /// ``` var isJSONValue: Bool { switch impl { case .databaseValue(.null): return true case let .binary(op, _, _): return op.isJSONValue case let .collated(expression, _): return expression.isJSONValue case let .simpleFunction(invocation): return invocation.isJSONValue case let .aggregateFunction(invocation): return invocation.isJSONValue default: return false } } #if GRDBCUSTOMSQLITE || GRDBCIPHER /// Returns an expression suitable in JSON building contexts. var jsonBuilderExpression: SQLExpression { switch preferredJSONInterpretation { case .deferredToSQLite: return self case .jsonValue: if isJSONValue { return self } else { // Needs explicit call to JSON() return .function("JSON", [self]) } } } #else @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS /// Returns an expression suitable in JSON building contexts. var jsonBuilderExpression: SQLExpression { switch preferredJSONInterpretation { case .deferredToSQLite: return self case .jsonValue: if isJSONValue { return self } else { // Needs explicit call to JSON() return .function("JSON", [self]) } } } #endif } // MARK: - SQLExpressible /// A type that can be used as an SQL expression. /// /// Related SQLite documentation public protocol SQLExpressible { /// Returns an SQL expression. var sqlExpression: SQLExpression { get } } extension SQLExpressible where Self == Column { /// The hidden rowID column public static var rowID: Self { Column.rowID } } /// A database-specific type that can be used as an SQL expression. /// /// `SQLSpecificExpressible` is the protocol for all database-specific types /// that can be turned into an SQL expression. Types whose existence is not purely /// dedicated to the database should adopt the ``SQLExpressible`` /// protocol instead. /// /// For example, ``Column`` is a type that only exists to help you build /// requests, and it adopts `SQLSpecificExpressible`. /// /// On the other side, `Int` adopts ``SQLExpressible``. /// /// ## Topics /// /// ### Applying a Collation /// /// - ``collating(_:)-2mr78`` /// - ``collating(_:)-10dk1`` /// /// ### SQL Functions & Operators /// /// See also JSON functions in . /// /// - ``abs(_:)-5l6xp`` /// - ``average(_:)`` /// - ``average(_:filter:)`` /// - ``capitalized`` /// - ``cast(_:as:)-1dmu3`` /// - ``count(_:)`` /// - ``count(distinct:)`` /// - ``dateTime(_:_:)`` /// - ``julianDay(_:_:)`` /// - ``length(_:)-41me0`` /// - ``like(_:escape:)`` /// - ``localizedCapitalized`` /// - ``localizedLowercased`` /// - ``localizedUppercased`` /// - ``lowercased`` /// - ``min(_:)`` /// - ``min(_:filter:)`` /// - ``max(_:)`` /// - ``max(_:filter:)`` /// - ``sum(_:)`` /// - ``sum(_:filter:)`` /// - ``total(_:)`` /// - ``total(_:filter:)`` /// - ``uppercased`` /// - ``SQLDateModifier`` /// /// ### Interpreting an expression as JSON /// /// - ``asJSON`` /// /// ### Creating Ordering Terms /// /// - ``asc`` /// - ``ascNullsLast`` /// - ``desc`` /// - ``descNullsFirst`` /// /// ### Creating Result Columns /// /// - ``forKey(_:)-3xk0`` /// - ``forKey(_:)-3egx6`` public protocol SQLSpecificExpressible: SQLExpressible, SQLSelectable, SQLOrderingTerm { // SQLExpressible can be adopted by Swift standard types, and user // types, through the DatabaseValueConvertible protocol which inherits // from SQLExpressible. // // For example, Int adopts SQLExpressible through // DatabaseValueConvertible. // // SQLSpecificExpressible, on the other side, is not adopted by any // Swift standard type or any user type. It is only adopted by GRDB types, // such as Column and SQLExpression. // // This separation lets us define functions and operators that do not // spill out. The three declarations below have no chance overloading a // Swift-defined operator, or a user-defined operator: // // - ==(some SQLExpressible, some SQLSpecificExpressible) // - ==(some SQLSpecificExpressible, some SQLExpressible) // - ==(some SQLSpecificExpressible, some SQLSpecificExpressible) } extension SQLSpecificExpressible { public var sqlSelection: SQLSelection { .expression(sqlExpression) } public var sqlOrdering: SQLOrdering { .expression(sqlExpression) } } extension SQLExpression: SQLSpecificExpressible { // Not a real deprecation, just a usage warning @available(*, deprecated, message: "Already SQLExpression") public var sqlExpression: SQLExpression { self } } extension Sequence where Element: SQLSpecificExpressible { /// Returns an expression by joining all elements with an associative SQL /// binary operator. /// /// For example: /// /// ```swift /// // SELECT * FROM player /// // WHERE (registered /// // AND (score >= 1000) /// // AND (name IS NOT NULL)) /// let conditions = [ /// Column("registered"), /// Column("score") >= 1000, /// Column("name") != nil] /// Player.filter(conditions.joined(operator: .and)) /// ``` /// /// When the sequence is empty, `joined(operator:)` returns the neutral /// value of the operator. It is: /// /// - `0` for ``SQLExpression/AssociativeBinaryOperator/add`` /// - `1` for ``SQLExpression/AssociativeBinaryOperator/multiply`` /// - `false` for ``SQLExpression/AssociativeBinaryOperator/or`` /// - `true` for ``SQLExpression/AssociativeBinaryOperator/and`` /// - `""` for ``SQLExpression/AssociativeBinaryOperator/concat`` public func joined(operator: SQLExpression.AssociativeBinaryOperator) -> SQLExpression { .associativeBinary(`operator`, map(\.sqlExpression)) } } extension Sequence where Element == any SQLSpecificExpressible { /// Returns an expression by joining all elements with an associative SQL /// binary operator. /// /// For example: /// /// ``` /// // SELECT * FROM player /// // WHERE (registered /// // AND (score >= 1000) /// // AND (name IS NOT NULL)) /// let conditions = [ /// Column("registered"), /// Column("score") >= 1000, /// Column("name") != nil] /// Player.filter(conditions.joined(operator: .and)) /// ``` /// /// When the sequence is empty, `joined(operator:)` returns the neutral /// value of the operator. It is: /// /// - `0` for ``SQLExpression/AssociativeBinaryOperator/add`` /// - `1` for ``SQLExpression/AssociativeBinaryOperator/multiply`` /// - `false` for ``SQLExpression/AssociativeBinaryOperator/or`` /// - `true` for ``SQLExpression/AssociativeBinaryOperator/and`` /// - `""` for ``SQLExpression/AssociativeBinaryOperator/concat`` public func joined(operator: SQLExpression.AssociativeBinaryOperator) -> SQLExpression { .associativeBinary(`operator`, map(\.sqlExpression)) } } // MARK: - SQL Ordering Support extension SQLSpecificExpressible { /// An ordering term for ascending order (nulls first). public var asc: SQLOrdering { .asc(sqlExpression) } /// An ordering term for descending order (nulls last). public var desc: SQLOrdering { .desc(sqlExpression) } #if GRDBCUSTOMSQLITE /// An ordering term for ascending order (nulls last). public var ascNullsLast: SQLOrdering { .ascNullsLast(sqlExpression) } /// An ordering term for descending order (nulls first). public var descNullsFirst: SQLOrdering { .descNullsFirst(sqlExpression) } #elseif !GRDBCIPHER /// An ordering term for ascending order (nulls last). @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ public var ascNullsLast: SQLOrdering { .ascNullsLast(sqlExpression) } /// An ordering term for descending order (nulls first). @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ public var descNullsFirst: SQLOrdering { .descNullsFirst(sqlExpression) } #endif } // MARK: - SQL Selection Support extension SQLSpecificExpressible { /// Returns an aliased result column. /// /// For example: /// /// ```swift /// // SELECT (score + bonus) AS totalScore FROM player /// let totalScore = (Column("score") * Column("bonus")).forKey("totalScore") /// let request = Player.select(totalScore) /// ``` /// /// If you need to refer to the aliased column in another part of a request, /// use ``ColumnExpression/detached``. For example: /// /// ```swift /// // SELECT (score + bonus) AS totalScore /// // FROM player /// // ORDER BY totalScore /// let request = Player /// .select(totalScore) /// .order(Column("totalScore").detached) /// ``` public func forKey(_ key: String) -> SQLSelection { .aliasedExpression(sqlExpression, key) } /// Returns an aliased column with the same name as the coding key. /// /// See . public func forKey(_ key: some CodingKey) -> SQLSelection { forKey(key.stringValue) } } // MARK: - SQL Collations Support extension SQLSpecificExpressible { /// Returns a collated expression. /// /// For example: /// /// ```swift /// // SELECT * FROM player /// // WHERE email = 'contact@example.com' COLLATE NOCASE /// Player.filter(Column("email").collating(.nocase) == "contact@example.com") /// ``` public func collating(_ collation: Database.CollationName) -> SQLExpression { .collated(sqlExpression, collation) } /// Returns a collated expression. /// /// For example: /// /// ```swift /// Player.order(Column("name").collating(.localizedStandardCompare)) /// ``` public func collating(_ collation: DatabaseCollation) -> SQLExpression { .collated(sqlExpression, Database.CollationName(rawValue: collation.name)) } }