120 lines
3.6 KiB
Swift
120 lines
3.6 KiB
Swift
/// The right-hand side of the `IN` or `NOT IN` SQL operators
|
|
///
|
|
/// See <https://sqlite.org/lang_expr.html#the_in_and_not_in_operators>
|
|
struct SQLCollection {
|
|
private var impl: Impl
|
|
|
|
private enum Impl {
|
|
/// An array collection
|
|
///
|
|
/// id IN (1, 2, 3)
|
|
/// ~~~~~~~~~
|
|
case array([SQLExpression])
|
|
|
|
/// A subquery
|
|
///
|
|
/// score IN (SELECT ...)
|
|
/// ~~~~~~~~~~~~
|
|
case subquery(SQLSubquery)
|
|
|
|
/// A table
|
|
///
|
|
/// score IN table
|
|
/// ~~~~~
|
|
case table(String)
|
|
}
|
|
|
|
static func array(_ expressions: [SQLExpression]) -> Self {
|
|
self.init(impl: .array(expressions))
|
|
}
|
|
|
|
static func subquery(_ subquery: SQLSubquery) -> Self {
|
|
self.init(impl: .subquery(subquery))
|
|
}
|
|
|
|
static func table(_ tableName: String) -> Self {
|
|
self.init(impl: .table(tableName))
|
|
}
|
|
}
|
|
|
|
extension SQLCollection {
|
|
/// Returns a qualified collection.
|
|
func qualified(with alias: TableAlias) -> SQLCollection {
|
|
switch impl {
|
|
case .subquery,
|
|
.table:
|
|
return self
|
|
|
|
case let .array(expressions):
|
|
return .array(expressions.map { $0.qualified(with: alias) })
|
|
}
|
|
}
|
|
|
|
/// The expressions in the collection, if known.
|
|
///
|
|
/// This property 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)
|
|
var collectionExpressions: [SQLExpression]? {
|
|
switch impl {
|
|
case .subquery,
|
|
.table:
|
|
return nil
|
|
|
|
case let .array(expressions):
|
|
return expressions
|
|
}
|
|
}
|
|
|
|
/// Returns an SQL string that represents the collection.
|
|
///
|
|
/// - parameter context: An SQL generation context which accepts
|
|
/// statement arguments.
|
|
func sql(_ context: SQLGenerationContext) throws -> String {
|
|
switch impl {
|
|
case let .array(expressions):
|
|
return try "("
|
|
+ expressions.map { try $0.sql(context) }.joined(separator: ", ")
|
|
+ ")"
|
|
|
|
case let .subquery(subquery):
|
|
return try "("
|
|
+ subquery.sql(context)
|
|
+ ")"
|
|
|
|
case let .table(tableName):
|
|
return tableName.quotedDatabaseIdentifier
|
|
}
|
|
}
|
|
|
|
/// Returns an expression that check whether the collection contains
|
|
/// the expression.
|
|
func contains(_ value: SQLExpression) -> SQLExpression {
|
|
switch impl {
|
|
case .subquery,
|
|
.table:
|
|
return .in(value, self)
|
|
|
|
case let .array(expressions):
|
|
guard let expression = expressions.first else {
|
|
// [].contains(...) -> false
|
|
return false.sqlExpression
|
|
}
|
|
|
|
if expressions.count == 1 {
|
|
// Output `value = expression` instead of `value IN (expression)`,
|
|
// because it looks nicer. Force the equal `=` operator, so that
|
|
// the result evaluates just as `value IN (expression)`, even
|
|
// if expression is NULL.
|
|
return .compare(.equal, value, expression)
|
|
}
|
|
|
|
return .in(value, self)
|
|
}
|
|
}
|
|
}
|