137 lines
4.4 KiB
Swift
137 lines
4.4 KiB
Swift
/// An SQL subquery.
|
|
///
|
|
/// `SQLSubquery` is an opaque representation of an SQL subquery.
|
|
public struct SQLSubquery {
|
|
private var impl: Impl
|
|
|
|
private enum Impl {
|
|
/// A literal SQL query
|
|
case literal(SQL)
|
|
|
|
/// A query interface relation
|
|
case relation(SQLRelation)
|
|
}
|
|
|
|
static func literal(_ sqlLiteral: SQL) -> Self {
|
|
self.init(impl: .literal(sqlLiteral))
|
|
}
|
|
|
|
static func relation(_ relation: SQLRelation) -> Self {
|
|
self.init(impl: .relation(relation))
|
|
}
|
|
}
|
|
|
|
extension SQLSubquery {
|
|
/// The number of columns selected by the subquery.
|
|
///
|
|
/// This method makes it possible to find the columns of a CTE in a request
|
|
/// that includes a CTE association:
|
|
///
|
|
/// // WITH cte AS (SELECT 1 AS a, 2 AS b)
|
|
/// // SELECT player.*, cte.*
|
|
/// // FROM player
|
|
/// // JOIN cte
|
|
/// let cte = CommonTableExpression(named: "cte", sql: "SELECT 1 AS a, 2 AS b")
|
|
/// let request = Player
|
|
/// .with(cte)
|
|
/// .including(required: Player.association(to: cte))
|
|
/// let row = try Row.fetchOne(db, request)!
|
|
///
|
|
/// // We know that "SELECT 1 AS a, 2 AS b" selects two columns,
|
|
/// // so we can find cte columns in the row:
|
|
/// row.scopes["cte"] // [a:1, b:2]
|
|
func columnCount(_ db: Database) throws -> Int {
|
|
switch impl {
|
|
case let .literal(sqlLiteral):
|
|
// Compile request. We can freely use the statement cache because we
|
|
// do not execute the statement or modify its arguments.
|
|
let context = SQLGenerationContext(db)
|
|
let sql = try sqlLiteral.sql(context)
|
|
let statement = try db.cachedStatement(sql: sql)
|
|
return statement.columnCount
|
|
|
|
case let .relation(relation):
|
|
return try SQLQueryGenerator(relation: relation).columnCount(db)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SQLSubquery {
|
|
/// Returns the subquery SQL.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// // SELECT *
|
|
/// // FROM "player"
|
|
/// // WHERE "score" = (SELECT MAX("score") FROM "player")
|
|
/// let maxScore = Player.select(max(Column("score")))
|
|
/// let players = try Player
|
|
/// .filter(Column("score") == maxScore)
|
|
/// .fetchAll(db)
|
|
///
|
|
/// - parameter context: An SQL generation context.
|
|
/// - parameter singleResult: A hint that a single result row will be
|
|
/// consumed. Implementations can optionally use it to optimize the
|
|
/// generated SQL, for example by adding a `LIMIT 1` SQL clause.
|
|
/// - returns: An SQL string.
|
|
func sql(_ context: SQLGenerationContext) throws -> String {
|
|
switch impl {
|
|
case let .literal(sqlLiteral):
|
|
return try sqlLiteral.sql(context)
|
|
|
|
case let .relation(relation):
|
|
return try SQLQueryGenerator(relation: relation).requestSQL(context)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SQLSubqueryable
|
|
|
|
/// A type that can be used as SQL subquery.
|
|
///
|
|
/// Related SQLite documentation <https://www.sqlite.org/syntax/select-stmt.html>
|
|
///
|
|
/// ## Topics
|
|
///
|
|
/// ### Supporting Types
|
|
///
|
|
/// - ``SQLSubquery``
|
|
public protocol SQLSubqueryable: SQLSpecificExpressible {
|
|
var sqlSubquery: SQLSubquery { get }
|
|
}
|
|
|
|
extension SQLSubquery: SQLSubqueryable {
|
|
// Not a real deprecation, just a usage warning
|
|
@available(*, deprecated, message: "Already SQLSubquery")
|
|
public var sqlSubquery: SQLSubquery { self }
|
|
}
|
|
|
|
extension SQLSubqueryable {
|
|
/// Returns a subquery expression.
|
|
public var sqlExpression: SQLExpression {
|
|
.subquery(sqlSubquery)
|
|
}
|
|
}
|
|
|
|
extension SQLSubqueryable {
|
|
/// Returns an expression that checks the inclusion of the expression in
|
|
/// the subquery.
|
|
///
|
|
/// // 1000 IN (SELECT score FROM player)
|
|
/// let request = Player.select(Column("score"), as: Int.self)
|
|
/// let condition = request.contains(1000)
|
|
public func contains(_ element: some SQLExpressible) -> SQLExpression {
|
|
SQLCollection.subquery(sqlSubquery).contains(element.sqlExpression)
|
|
}
|
|
|
|
/// Returns an expression that is true if and only if the subquery would
|
|
/// return one or more rows.
|
|
///
|
|
/// // EXISTS (SELECT * FROM player WHERE name = 'Arthur')
|
|
/// let request = Player.filter(Column("name") == "Arthur")
|
|
/// let condition = request.exists()
|
|
public func exists() -> SQLExpression {
|
|
.exists(sqlSubquery)
|
|
}
|
|
}
|