2409 lines
82 KiB
Swift
2409 lines
82 KiB
Swift
/// 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: <https://www.sqlite.org/lang_expr.html>
|
|
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
|
|
/// <data>
|
|
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 <https://www.sqlite.org/lang_expr.html#castexpr>.
|
|
indirect case cast(SQLExpression, Database.StorageClass)
|
|
|
|
/// The `BETWEEN` and `NOT BETWEEN` operators.
|
|
///
|
|
/// <expression> BETWEEN <lowerBound> AND <upperBound>
|
|
indirect case between(
|
|
expression: SQLExpression,
|
|
lowerBound: SQLExpression,
|
|
upperBound: SQLExpression,
|
|
isNegated: Bool)
|
|
|
|
/// A binary operator.
|
|
///
|
|
/// <lhs> * <rhs>
|
|
/// <lhs> <= <rhs>
|
|
indirect case binary(BinaryOperator, SQLExpression, SQLExpression)
|
|
|
|
/// An escapable binary operator.
|
|
///
|
|
/// <lhs> LIKE <rhs>
|
|
/// <lhs> LIKE <rhs> ESCAPE <escape>
|
|
indirect case escapableBinary(EscapableBinaryOperator, SQLExpression, SQLExpression, escape: SQLExpression?)
|
|
|
|
/// An associative binary operator.
|
|
///
|
|
/// <expr1> AND <expr2>
|
|
/// <expr1> + <expr2> + <expr3>
|
|
///
|
|
/// - precondition: expressions.count > 1
|
|
case associativeBinary(AssociativeBinaryOperator, [SQLExpression])
|
|
|
|
/// The `EXISTS` and `NOT EXISTS` operators.
|
|
///
|
|
/// EXISTS (<subquery>)
|
|
case exists(SQLSubquery, isNegated: Bool)
|
|
|
|
/// The `IN` and `NOT IN` operators.
|
|
///
|
|
/// <expression> IN <collection>
|
|
/// <expression> NOT IN <collection>
|
|
indirect case `in`(SQLExpression, SQLCollection, isNegated: Bool)
|
|
|
|
/// An unary operator.
|
|
///
|
|
/// -<expression>
|
|
indirect case unary(UnaryOperator, SQLExpression)
|
|
|
|
/// An equality comparison.
|
|
///
|
|
/// <lhs> = <rhs>
|
|
/// <lhs> <> <rhs>
|
|
/// <lhs> IS <rhs>
|
|
/// <lhs> IS NOT <rhs>
|
|
indirect case compare(EqualityOperator, SQLExpression, SQLExpression)
|
|
|
|
/// A table full-text match.
|
|
///
|
|
/// <table> MATCH <pattern>
|
|
indirect case tableMatch(TableAlias, SQLExpression)
|
|
|
|
/// A logical `NOT` operator.
|
|
///
|
|
/// NOT <expression>
|
|
indirect case not(SQLExpression)
|
|
|
|
/// A collated expression.
|
|
///
|
|
/// <expression> COLLATE <collation>
|
|
indirect case collated(SQLExpression, Database.CollationName)
|
|
|
|
/// The `COUNT(*)` expression.
|
|
case countAll
|
|
|
|
/// A function call.
|
|
///
|
|
/// <function>(<argument>, ...)
|
|
/// <function>(DISTINCT <argument>)
|
|
indirect case simpleFunction(SQLSimpleFunctionInvocation)
|
|
|
|
/// An aggregate function call.
|
|
///
|
|
/// <function>(<argument>, ...)
|
|
/// <function>(DISTINCT <argument>)
|
|
indirect case aggregateFunction(SQLAggregateFunctionInvocation)
|
|
|
|
/// An expression that checks for zero or positive values.
|
|
///
|
|
/// <expression> = 0
|
|
/// <expression> > 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
|
|
/// <data>
|
|
///
|
|
/// 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.
|
|
///
|
|
/// <expression> BETWEEN <lowerBound> AND <upperBound>
|
|
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.
|
|
///
|
|
/// <lhs> * <rhs>
|
|
/// <lhs> <= <rhs>
|
|
/// <lhs> LIKE <rhs>
|
|
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.
|
|
///
|
|
/// <lhs> LIKE <rhs>
|
|
/// <lhs> LIKE <rhs> ESCAPE <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.
|
|
///
|
|
/// <expr1> AND <expr2>
|
|
/// <expr1> + <expr2> + <expr3>
|
|
///
|
|
/// 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 (<subquery>)
|
|
static func exists(_ subquery: SQLSubquery) -> Self {
|
|
self.init(impl: .exists(subquery, isNegated: false))
|
|
}
|
|
|
|
/// The `IN` and `NOT IN` operators.
|
|
///
|
|
/// <expression> IN <collection>
|
|
/// <expression> NOT IN <collection>
|
|
///
|
|
/// 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.
|
|
///
|
|
/// -<expression>
|
|
static func unary(_ op: UnaryOperator, _ expression: SQLExpression) -> Self {
|
|
self.init(impl: .unary(op, expression))
|
|
}
|
|
|
|
/// An equality comparison.
|
|
///
|
|
/// <lhs> = <rhs>
|
|
/// <lhs> <> <rhs>
|
|
/// <lhs> IS <rhs>
|
|
/// <lhs> IS NOT <rhs>
|
|
///
|
|
/// 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`.
|
|
///
|
|
/// <lhs> = <rhs>
|
|
/// <lhs> IS NULL
|
|
/// <rhs> 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.
|
|
///
|
|
/// <table> MATCH <pattern>
|
|
static func tableMatch(_ alias: TableAlias, _ expression: SQLExpression) -> Self {
|
|
self.init(impl: .tableMatch(alias, expression))
|
|
}
|
|
|
|
/// A logical `NOT` operator.
|
|
///
|
|
/// NOT <expression>
|
|
static func not(_ expression: SQLExpression) -> Self {
|
|
self.init(impl: .not(expression))
|
|
}
|
|
|
|
/// A collated expression.
|
|
///
|
|
/// <expression> COLLATE <collation>
|
|
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<String> = [
|
|
"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<String> = [
|
|
"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<String> = [
|
|
"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(<expression>)
|
|
static func count(_ expression: SQLExpression) -> Self {
|
|
function("COUNT", [expression])
|
|
}
|
|
|
|
/// The `COUNT(DISTINCT)` function.
|
|
///
|
|
/// COUNT(DISTINCT <expression>)
|
|
static func countDistinct(_ expression: SQLExpression) -> Self {
|
|
aggregateFunction("COUNT", [expression], isDistinct: true)
|
|
}
|
|
|
|
/// An expression that checks for zero or positive values.
|
|
///
|
|
/// <expression> = 0
|
|
/// <expression> > 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 <https://www.sqlite.org/lang_expr.html#castexpr>.
|
|
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<String> {
|
|
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<Int64>? {
|
|
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<Int64>? = 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<Int64> = []
|
|
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 <https://github.com/groue/GRDB.swift/issues/816>
|
|
///
|
|
/// 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 <https://www.sqlite.org/syntax/expr.html>
|
|
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 <doc:JSON>.
|
|
///
|
|
/// - ``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 <doc:SQLSpecificExpressible/forKey(_:)-3xk0>.
|
|
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))
|
|
}
|
|
}
|