Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/QueryInterface/Request/RequestProtocols.swift
T
2025-01-28 12:28:03 +08:00

1571 lines
52 KiB
Swift

import Foundation
// MARK: - TypedRequest
/// A request that knows how to decode database rows.
public protocol TypedRequest<RowDecoder> {
/// The type that can decode database rows.
///
/// For example, it is `Player` in the request below:
///
/// ```swift
/// let request = Player.all()
/// ```
associatedtype RowDecoder
}
// MARK: - SelectionRequest
/// A request that can define the selected columns.
///
/// ## Topics
///
/// ### The SELECT Clause
///
/// - ``annotated(with:)-4qcem``
/// - ``annotated(with:)-6ehs4``
/// - ``annotatedWhenConnected(with:)``
/// - ``select(_:)-30yzl``
/// - ``select(_:)-7e2y5``
/// - ``select(literal:)``
/// - ``select(sql:arguments:)``
/// - ``selectWhenConnected(_:)``
public protocol SelectionRequest {
/// Defines the result columns.
///
/// The `selection` parameter is a closure that accepts a database
/// connection and returns an array of result columns. It is evaluated when
/// the request has an access to the database, and can perform database
/// requests in order to build its result.
///
/// For example:
///
/// ```swift
/// // SELECT id, name FROM player
/// let request = Player.all().selectWhenConnected { db in
/// [Column("id"), Column("name")]
/// }
/// ```
///
/// Any previous selection is discarded:
///
/// ```swift
/// // SELECT name FROM player
/// let request = Player.all()
/// .selectWhenConnected { db in [Column("id")] }
/// .selectWhenConnected { db in [Column("name")] }
/// ```
///
/// - parameter selection: A closure that accepts a database connection and
/// returns an array of result columns.
func selectWhenConnected(_ selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self
/// Appends result columns to the selected columns.
///
/// The `selection` parameter is a closure that accepts a database
/// connection and returns an array of result columns. It is evaluated when
/// the request has an access to the database, and can perform database
/// requests in order to build its result.
///
/// For example:
///
/// ```swift
/// // SELECT *, score + bonus AS totalScore FROM player
/// let request = Player.all().annotatedWhenConnected { db in
/// [(Column("score") + Column("bonus")).forKey("totalScore")]
/// }
/// ```
///
/// - parameter selection: A closure that accepts a database connection and
/// returns an array of result columns.
func annotatedWhenConnected(with selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self
}
extension SelectionRequest {
/// Defines the result columns.
///
/// For example:
///
/// ```swift
/// // SELECT id, score FROM player
/// let request = Player.all().select([Column("id"), Column("score")])
/// ```
///
/// Any previous selection is replaced:
///
/// ```swift
/// // SELECT score FROM player
/// let request = Player.all()
/// .select([Column("id")])
/// .select([Column("score")])
/// ```
public func select(_ selection: [any SQLSelectable]) -> Self {
selectWhenConnected { _ in selection }
}
/// Defines the result columns.
///
/// For example:
///
/// ```swift
/// // SELECT id, score FROM player
/// let request = Player.all().select(Column("id"), Column("score"))
/// ```
///
/// Any previous selection is discarded:
///
/// ```swift
/// // SELECT score FROM player
/// let request = Player.all()
/// .select(Column("id"))
/// .select(Column("score"))
/// ```
public func select(_ selection: any SQLSelectable...) -> Self {
select(selection)
}
/// Defines the result columns with an SQL string.
///
/// For example:
///
/// ```swift
/// // SELECT id, name FROM player
/// let request = Player.all()
/// .select(sql: "id, name")
///
/// // SELECT id, IFNULL(name, 'Anonymous') FROM player
/// let defaultName = "Anonymous"
/// let request = Player.all()
/// .select(sql: "id, IFNULL(name, ?)", arguments: [defaultName])
/// ```
///
/// Any previous selection is discarded:
///
/// ```swift
/// // SELECT score FROM player
/// let request = Player.all()
/// .select(sql: "id")
/// .select(sql: "name")
/// ```
public func select(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
select(SQL(sql: sql, arguments: arguments))
}
/// Defines the result columns with an ``SQL`` literal.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// // SELECT id, IFNULL(name, 'Anonymous') FROM player
/// let defaultName = "Anonymous"
/// let request = Player.all()
/// .select(literal: "id, IFNULL(name, \(defaultName))")
/// ```
///
/// Any previous selection is discarded:
///
/// ```swift
/// // SELECT IFNULL(name, 'Anonymous') FROM player
/// let request = Player.all()
/// .select(literal: "id")
/// .select(literal: "IFNULL(name, \(defaultName))")
/// ```
public func select(literal sqlLiteral: SQL) -> Self {
// NOT TESTED
select(sqlLiteral)
}
/// Appends result columns to the selected columns.
///
/// For example:
///
/// ```swift
/// // SELECT *, score + bonus AS totalScore FROM player
/// let totalScore = (Column("score") + Column("bonus")).forKey("totalScore")
/// let request = Player.all().annotated(with: [totalScore])
/// ```
public func annotated(with selection: [any SQLSelectable]) -> Self {
annotatedWhenConnected(with: { _ in selection })
}
/// Appends result columns to the selected columns.
///
/// For example:
///
/// ```swift
/// // SELECT *, score + bonus AS totalScore FROM player
/// let totalScore = (Column("score") + Column("bonus")).forKey("totalScore")
/// let request = Player.all().annotated(with: totalScore)
/// ```
public func annotated(with selection: any SQLSelectable...) -> Self {
annotated(with: selection)
}
}
// MARK: - FilteredRequest
/// A request that can filter database rows.
///
/// The filter applies to the `WHERE` clause, or to the `ON` clause of
/// an SQL join.
///
/// ## Topics
///
/// ### The WHERE and JOIN ON Clauses
///
/// - ``all()``
/// - ``filter(_:)``
/// - ``filter(literal:)``
/// - ``filter(sql:arguments:)``
/// - ``filterWhenConnected(_:)``
/// - ``none()``
public protocol FilteredRequest {
/// Filters the fetched rows with a boolean SQL expression.
///
/// The `predicate` parameter is a closure that accepts a database
/// connection and returns a boolean SQL expression. It is evaluated when
/// the request has an access to the database, and can perform database
/// requests in order to build its result.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player WHERE name = 'O''Brien'
/// let name = "O'Brien"
/// let request = Player.all().filterWhenConnected { db in
/// Column("name") == name
/// }
/// ```
///
/// - parameter predicate: A closure that accepts a database connection and
/// returns a boolean SQL expression.
func filterWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self
}
extension FilteredRequest {
// Accept SQLSpecificExpressible instead of SQLExpressible, so that we
// prevent the `Player.filter(42)` misuse.
// See https://github.com/groue/GRDB.swift/pull/864
/// Filters the fetched rows with a boolean SQL expression.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player WHERE name = 'O''Brien'
/// let name = "O'Brien"
/// let request = Player.all().filter(Column("name") == name)
/// ```
public func filter(_ predicate: some SQLSpecificExpressible) -> Self {
filterWhenConnected { _ in predicate }
}
/// Filters the fetched rows with an SQL string.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player WHERE name = 'O''Brien'
/// let name = "O'Brien"
/// let request = Player.all().filter(sql: "name = ?", arguments: [name])
/// ```
public func filter(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
filter(SQL(sql: sql, arguments: arguments))
}
/// Filters the fetched rows with an ``SQL`` literal.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// // SELECT * FROM player WHERE name = 'O''Brien'
/// let name = "O'Brien"
/// let request = Player.all().filter(literal: "name = \(name)")
/// ```
public func filter(literal sqlLiteral: SQL) -> Self {
// NOT TESTED
filter(sqlLiteral)
}
/// Returns an empty request that fetches no row.
public func none() -> Self {
filterWhenConnected { _ in false }
}
/// Returns `self`: a request that fetches all rows from this request.
///
/// This method, which does nothing, exists in order to match ``none()``.
public func all() -> Self {
self
}
}
// MARK: - TableRequest
/// A request that feeds from a database table
///
/// ## Topics
///
/// ## The Database Table
///
/// - ``databaseTableName``
///
/// ### Instance Methods
///
/// - ``aliased(_:)``
/// - ``TableAlias``
///
/// ### The WHERE Clause
///
/// - ``filter(id:)``
/// - ``filter(ids:)``
/// - ``filter(key:)-1p9sq``
/// - ``filter(key:)-2te6v``
/// - ``filter(keys:)-6ggt1``
/// - ``filter(keys:)-8fbn9``
/// - ``matching(_:)-3s3zr``
/// - ``matching(_:)-7c1e8``
///
/// ### The GROUP BY and HAVING Clauses
///
/// - ``groupByPrimaryKey()``
///
/// ### The ORDER BY Clause
///
/// - ``orderByPrimaryKey()``
public protocol TableRequest {
/// The name of the database table
var databaseTableName: String { get }
/// Returns a request that can be referred to with the provided alias.
///
/// Use this method when you need to refer to this request from
/// another request.
///
/// The first example fetches posthumous books:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord { }
/// struct Book: TableRecord, FetchableRecord {
/// static let author = belongsTo(Author.self)
/// }
///
/// // SELECT book.*
/// // FROM book
/// // JOIN author ON author.id = book.authorId
/// // WHERE book.publishDate >= author.deathDate
/// let authorAlias = TableAlias()
/// let posthumousBooks = try Book
/// .joining(required: Book.author.aliased(authorAlias))
/// .filter(Column("publishDate") >= authorAlias[Column("deathDate")])
/// .fetchAll(db)
/// ```
///
/// The second example sorts books by author name first, and then by title:
///
/// ```swift
/// // SELECT book.*
/// // FROM book
/// // JOIN author ON author.id = book.authorId
/// // ORDER BY author.name, book.title
/// let authorAlias = TableAlias()
/// let books = try Book
/// .joining(required: Book.author.aliased(authorAlias))
/// .order(authorAlias[Column("name")], Column("title"))
/// .fetchAll(db)
/// ```
///
/// The third example uses named ``TableAlias`` so that SQL snippets can
/// refer to SQL tables with those names:
///
/// ```swift
/// // SELECT b.*
/// // FROM book b
/// // JOIN author a ON a.id = b.authorId
/// // AND a.countryCode = 'FR'
/// // WHERE b.publishDate >= a.deathDate
/// let bookAlias = TableAlias(name: "b")
/// let authorAlias = TableAlias(name: "a")
/// let posthumousFrenchBooks = try Book.aliased(bookAlias)
/// .joining(required: Book.author.aliased(authorAlias)
/// .filter(sql: "a.countryCode = ?", arguments: ["FR"]))
/// .filter(sql: "b.publishDate >= a.deathDate")
/// .fetchAll(db)
/// ```
func aliased(_ alias: TableAlias) -> Self
}
extension TableRequest where Self: FilteredRequest, Self: TypedRequest {
/// Filters by primary key.
///
/// All single-column primary keys are supported:
///
/// ```swift
/// // SELECT * FROM player WHERE id = 1
/// let request = Player.all().filter(key: 1)
///
/// // SELECT * FROM country WHERE code = 'FR'
/// let request = Country.all().filter(key: "FR")
/// ```
///
/// - parameter key: A primary key
public func filter(key: some DatabaseValueConvertible) -> Self {
if key.databaseValue.isNull {
return none()
}
return filter(keys: [key])
}
/// Filters by primary key.
///
/// All single-column primary keys are supported:
///
/// ```swift
/// // SELECT * FROM player WHERE id = IN (1, 2, 3)
/// let request = Player.all().filter(keys: [1, 2, 3])
///
/// // SELECT * FROM country WHERE code = IN ('FR', 'US')
/// let request = Country.all().filter(keys: ["FR", "US"])
/// ```
///
/// - parameter keys: A collection of primary keys
public func filter<Sequence: Swift.Sequence>(keys: Sequence)
-> Self
where Sequence.Element: DatabaseValueConvertible
{
// In order to encode keys in the database, we perform a runtime check
// for EncodableRecord, and look for a customized encoding strategy.
// Such dynamic dispatch is unusual in GRDB, but static dispatch
// (customizing TableRequest where RowDecoder: EncodableRecord) would
// make it impractical to define `filter(id:)`, `fetchOne(_:key:)`,
// `deleteAll(_:ids:)` etc.
if let recordType = RowDecoder.self as? any EncodableRecord.Type {
if Sequence.Element.self == Data.self || Sequence.Element.self == Optional<Data>.self {
let strategy = recordType.databaseDataEncodingStrategy
let keys = keys.compactMap { ($0 as! Data?).flatMap(strategy.encode)?.databaseValue }
return filter(rawKeys: keys)
} else if Sequence.Element.self == Date.self || Sequence.Element.self == Optional<Date>.self {
let strategy = recordType.databaseDateEncodingStrategy
let keys = keys.compactMap { ($0 as! Date?).flatMap(strategy.encode)?.databaseValue }
return filter(rawKeys: keys)
} else if Sequence.Element.self == UUID.self || Sequence.Element.self == Optional<UUID>.self {
let strategy = recordType.databaseUUIDEncodingStrategy
let keys = keys.map { ($0 as! UUID?).map(strategy.encode)?.databaseValue }
return filter(rawKeys: keys)
}
}
return filter(rawKeys: keys)
}
/// Creates a request filtered by primary key.
///
/// // SELECT * FROM player WHERE ... id IN (1, 2, 3)
/// let request = try Player...filter(rawKeys: [1, 2, 3])
///
/// - parameter keys: A collection of primary keys
func filter<Keys>(rawKeys: Keys) -> Self
where Keys: Sequence, Keys.Element: DatabaseValueConvertible
{
// Don't bother removing NULLs. We'd lose CPU cycles, and this does not
// change the SQLite results anyway.
let expressions = rawKeys.map {
$0.databaseValue.sqlExpression
}
if expressions.isEmpty {
// Don't hit the database
return none()
}
let databaseTableName = self.databaseTableName
return filterWhenConnected { db in
let primaryKey = try db.primaryKey(databaseTableName)
GRDBPrecondition(
primaryKey.columns.count == 1,
"Requesting by key requires a single-column primary key in the table \(databaseTableName)")
return SQLCollection.array(expressions).contains(Column(primaryKey.columns[0]).sqlExpression)
}
}
/// Filters by primary or unique key.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player WHERE id = 1
/// let request = Player.all().filter(key: ["id": 1])
///
/// // SELECT * FROM player WHERE email = 'arthur@example.com'
/// let request = Player.all().filter(key: ["email": "arthur@example.com"])
///
/// // SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
/// let request = Citizenship.all().filter(key: [
/// "citizenId": 1,
/// "countryCode": "FR",
/// ])
/// ```
///
/// When executed, this request raises a fatal error if no unique index
/// exists on a subset of the key columns.
///
/// - parameter key: A key dictionary.
public func filter(key: [String: (any DatabaseValueConvertible)?]?) -> Self {
guard let key else {
return none()
}
return filter(keys: [key])
}
/// Filters by primary or unique key.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player WHERE id = 1
/// let request = Player.all().filter(keys: [["id": 1]])
///
/// // SELECT * FROM player WHERE email = 'arthur@example.com'
/// let request = Player.all().filter(keys: [["email": "arthur@example.com"]])
///
/// // SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
/// let request = Citizenship.all().filter(keys: [
/// ["citizenId": 1, "countryCode": "FR"],
/// ])
/// ```
///
/// When executed, this request raises a fatal error if no unique index
/// exists on a subset of the key columns.
///
/// - parameter keys: An array of key dictionaries.
public func filter(keys: [[String: (any DatabaseValueConvertible)?]]) -> Self {
if keys.isEmpty {
return none()
}
let databaseTableName = self.databaseTableName
return filterWhenConnected { db in
try keys
.map { key in
// Prevent filter(keys: [["foo": 1, "bar": 2]]) where
// ("foo", "bar") do not contain a unique key (primary key
// or unique index).
guard let columns = try db.columnsForUniqueKey(key.keys, in: databaseTableName) else {
fatalError("""
table \(databaseTableName) has no unique key on column(s) \
\(key.keys.sorted().joined(separator: ", "))
""")
}
let lowercaseColumns = columns.map { $0.lowercased() }
return key
// Preserve ordering of columns in the unique index
.sorted { (kv1, kv2) in
guard let index1 = lowercaseColumns.firstIndex(of: kv1.key.lowercased()) else {
// We allow extra columns which are not in the unique key
// Put them last in the query
return false
}
guard let index2 = lowercaseColumns.firstIndex(of: kv2.key.lowercased()) else {
// We allow extra columns which are not in the unique key
// Put them last in the query
return true
}
return index1 < index2
}
.map { (column, value) in Column(column) == value }
.joined(operator: .and)
}
.joined(operator: .or)
}
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
extension TableRequest
where Self: FilteredRequest,
Self: TypedRequest,
RowDecoder: Identifiable,
RowDecoder.ID: DatabaseValueConvertible
{
/// Filters by primary key.
///
/// All single-column primary keys are supported:
///
/// ```swift
/// // SELECT * FROM player WHERE id = 1
/// let request = Player.all().filter(id: 1)
///
/// // SELECT * FROM country WHERE code = 'FR'
/// let request = Country.all().filter(id: "FR")
/// ```
///
/// - parameter id: A primary key
public func filter(id: RowDecoder.ID) -> Self {
filter(key: id)
}
/// Filters by primary key.
///
/// All single-column primary keys are supported:
///
/// ```swift
/// // SELECT * FROM player WHERE id = IN (1, 2, 3)
/// let request = Player.all().filter(ids: [1, 2, 3])
///
/// // SELECT * FROM country WHERE code = IN ('FR', 'US')
/// let request = Country.all().filter(ids: ["FR", "US"])
/// ```
///
/// - parameter ids: A collection of primary keys
public func filter<IDS>(ids: IDS) -> Self
where IDS: Collection, IDS.Element == RowDecoder.ID
{
filter(keys: ids)
}
}
extension TableRequest where Self: OrderedRequest {
/// Sorts the fetched rows according to the primary key.
///
/// All primary keys are supported:
///
/// ```swift
/// // SELECT * FROM player ORDER BY id
/// let request = Player.all().orderByPrimaryKey()
///
/// // SELECT * FROM country ORDER BY code
/// let request = Country.all().orderByPrimaryKey()
///
/// // SELECT * FROM citizenship ORDER BY citizenId, countryCode
/// let request = Citizenship.all().orderByPrimaryKey()
/// ```
///
/// Any previous ordering is discarded.
public func orderByPrimaryKey() -> Self {
let tableName = self.databaseTableName
return orderWhenConnected { db in
try db.primaryKey(tableName).columns.map(SQLExpression.column)
}
}
}
extension TableRequest where Self: AggregatingRequest {
/// Returns an aggregate request grouped on the primary key.
///
/// Any previous grouping is discarded.
public func groupByPrimaryKey() -> Self {
let tableName = self.databaseTableName
return groupWhenConnected { db in
let primaryKey = try db.primaryKey(tableName)
if let rowIDColumn = primaryKey.rowIDColumn {
// Prefer the user-provided name of the rowid:
//
// // CREATE TABLE player (id INTEGER PRIMARY KEY, ...)
// // SELECT * FROM player GROUP BY id
// Player.all().groupByPrimaryKey()
return [Column(rowIDColumn)]
} else if primaryKey.tableHasRowID {
// Prefer the rowid
//
// // CREATE TABLE player (uuid TEXT NOT NULL PRIMARY KEY, ...)
// // SELECT * FROM player GROUP BY rowid
// Player.all().groupByPrimaryKey()
return [.rowID]
} else {
// WITHOUT ROWID table: group by primary key columns
//
// // CREATE TABLE player (uuid TEXT NOT NULL PRIMARY KEY, ...) WITHOUT ROWID
// // SELECT * FROM player GROUP BY uuid
// Player.all().groupByPrimaryKey()
return primaryKey.columns.map { Column($0) }
}
}
}
}
// MARK: - AggregatingRequest
/// A request that can aggregate database rows.
///
/// ## Topics
///
/// ### The GROUP BY Clause
///
/// - ``group(_:)-edak``
/// - ``group(_:)-4216o``
/// - ``group(literal:)``
/// - ``group(sql:arguments:)``
/// - ``groupWhenConnected(_:)``
///
/// ### The HAVING Clause
///
/// - ``having(_:)``
/// - ``having(literal:)``
/// - ``having(sql:arguments:)``
/// - ``havingWhenConnected(_:)``
public protocol AggregatingRequest {
/// Returns an aggregate request grouped on the given SQL expressions.
///
/// The `expressions` parameter is a closure that accepts a database
/// connection and returns an array of grouping SQL expressions. It is
/// evaluated when the request has an access to the database, and can
/// perform database requests in order to build its result.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .groupWhenConnected { db in [Column("teamId")] }
/// ```
///
/// Any previous grouping is discarded.
///
/// - parameter expressions: A closure that accepts a database connection
/// and returns an array of SQL expressions.
func groupWhenConnected(_ expressions: @escaping (Database) throws -> [any SQLExpressible]) -> Self
/// Filters the aggregated groups with a boolean SQL expression.
///
/// The `predicate` parameter is a closure that accepts a database
/// connection and returns a boolean SQL expression. It is evaluated when
/// the request has an access to the database, and can perform database
/// requests in order to build its result.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// // HAVING MAX(score) > 1000
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .group(Column("teamId"))
/// .havingWhenConnected { db in max(Column("score")) > 1000 }
/// ```
///
/// - parameter predicate: A closure that accepts a database connection and
/// returns a boolean SQL expression.
func havingWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self
}
extension AggregatingRequest {
/// Returns an aggregate request grouped on the given SQL expressions.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .group([Column("teamId")])
/// ```
///
/// Any previous grouping is discarded.
///
/// - parameter expressions: An array of SQL expressions.
public func group(_ expressions: [any SQLExpressible]) -> Self {
groupWhenConnected { _ in expressions }
}
/// Returns an aggregate request grouped on the given SQL expressions.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .group(Column("teamId"))
/// ```
///
/// Any previous grouping is discarded.
///
/// - parameter expressions: An array of SQL expressions.
public func group(_ expressions: any SQLExpressible...) -> Self {
group(expressions)
}
/// Returns an aggregate request grouped on an SQL string.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .group(sql: "teamId")
/// ```
///
/// Any previous grouping is discarded.
public func group(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
group(SQL(sql: sql, arguments: arguments))
}
/// Returns an aggregate request grouped on an ``SQL`` literal.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .group(literal: "teamId")
/// ```
///
/// Any previous grouping is discarded.
public func group(literal sqlLiteral: SQL) -> Self {
// NOT TESTED
group(sqlLiteral)
}
/// Filters the aggregated groups with a boolean SQL expression.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// // HAVING MAX(score) > 1000
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .group(Column("teamId"))
/// .having(max(Column("score")) > 1000)
/// ```
public func having(_ predicate: some SQLExpressible) -> Self {
havingWhenConnected { _ in predicate }
}
/// Filters the aggregated groups with an SQL string.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// // HAVING MAX(score) > 1000
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .group(Column("teamId"))
/// .having(sql: "MAX(score) > 1000")
/// ```
public func having(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
having(SQL(sql: sql, arguments: arguments))
}
/// Filters the aggregated groups with an ``SQL`` literal.
///
/// For example:
///
/// ```swift
/// // SELECT teamId, MAX(score)
/// // FROM player
/// // GROUP BY teamId
/// // HAVING MAX(score) > 1000
/// let request = Player
/// .select(Column("teamId"), max(Column("score")))
/// .group(Column("teamId"))
/// .having(literal: "MAX(score) > 1000")
/// ```
public func having(literal sqlLiteral: SQL) -> Self {
// NOT TESTED
having(sqlLiteral)
}
}
// MARK: - OrderedRequest
/// A request that can sort database rows.
///
/// ## Topics
///
/// ### The ORDER BY Clause
///
/// - ``order(_:)-63rzl``
/// - ``order(_:)-6co0m``
/// - ``order(literal:)``
/// - ``order(sql:arguments:)``
/// - ``orderWhenConnected(_:)``
/// - ``reversed()``
/// - ``unordered()``
/// - ``withStableOrder()``
public protocol OrderedRequest {
/// Sorts the fetched rows according to the given SQL ordering terms.
///
/// The `orderings` parameter is a closure that accepts a database
/// connection and returns an array of SQL ordering terms. It is evaluated
/// when the request has an access to the database, and can perform database
/// requests in order to build its result.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.all().orderWhenConnected { db in
/// [Column("score").desc, Column("name")]
/// }
/// ```
///
/// Any previous ordering is discarded:
///
/// ```swift
/// // SELECT * FROM player ORDER BY name
/// let request = Player.all()
/// .orderWhenConnected { db in [Column("score").desc] }
/// .orderWhenConnected { db in [Column("name")] }
/// ```
///
/// - parameter orderings: A closure that accepts a database connection and
/// returns an array of SQL ordering terms.
func orderWhenConnected(_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) -> Self
/// Returns a request with reversed ordering.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player ORDER BY name DESC
/// let request = Player.all()
/// .order(Column("name"))
/// .reversed()
/// ```
///
/// If no ordering was already specified, this method has no effect:
///
/// ```swift
/// // SELECT * FROM player
/// let request = Player.all().reversed()
/// ```
func reversed() -> Self
/// Returns a request without any ordering.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player
/// let request = Player.all()
/// .order(Column("name"))
/// .unordered()
/// ```
func unordered() -> Self
/// Returns a request with a stable order.
///
/// The returned request lifts ordering ambiguities and always return
/// its results in the same order.
///
/// The purpose of this method is to make requests testable.
func withStableOrder() -> Self
}
extension OrderedRequest {
/// Sorts the fetched rows according to the given SQL ordering terms.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.all()
/// .order(Column("score").desc, Column("name"))
/// ```
///
/// Any previous ordering is discarded:
///
/// ```swift
/// // SELECT * FROM player ORDER BY name
/// let request = Player.all()
/// .order(Column("score").desc)
/// .order(Column("name"))
/// ```
public func order(_ orderings: any SQLOrderingTerm...) -> Self {
orderWhenConnected { _ in orderings }
}
/// Sorts the fetched rows according to the given SQL ordering terms.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.all()
/// .order([Column("score").desc, Column("name")])
/// ```
///
/// Any previous ordering is discarded:
///
/// ```swift
/// // SELECT * FROM player ORDER BY name
/// let request = Player.all()
/// .order([Column("score").desc])
/// .order([Column("name")])
/// ```
public func order(_ orderings: [any SQLOrderingTerm]) -> Self {
orderWhenConnected { _ in orderings }
}
/// Sorts the fetched rows according to the given SQL string.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.all()
/// .order(sql: "score DESC, name")
/// ```
///
/// Any previous ordering is discarded.
public func order(sql: String, arguments: StatementArguments = StatementArguments()) -> Self {
order(SQL(sql: sql, arguments: arguments))
}
/// Sorts the fetched rows according to the given ``SQL`` literal.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.all()
/// .order(literal: "score DESC, name")
/// ```
///
/// Any previous ordering is discarded.
public func order(literal sqlLiteral: SQL) -> Self {
// NOT TESTED
order(sqlLiteral)
}
}
// MARK: - JoinableRequest
/// A request that can join and prefetch associations.
///
/// `JoinableRequest` is adopted by ``QueryInterfaceRequest`` and all
/// types conforming to ``Association``.
///
/// It provides the methods that build requests involving several tables linked
/// through associations.
///
/// ## Topics
///
/// ### Extending the Selection with Columns of Associated Records
///
/// - ``annotated(withOptional:)``
/// - ``annotated(withRequired:)``
///
/// ### Prefetching Associated Records
///
/// - ``including(all:)``
/// - ``including(optional:)``
/// - ``including(required:)``
///
/// ### Joining Associated Records
///
/// - ``joining(optional:)``
/// - ``joining(required:)``
public protocol JoinableRequest<RowDecoder>: TypedRequest {
/// Creates a request that prefetches an association.
func _including(all association: _SQLAssociation) -> Self
/// Creates a request that includes an association. The columns of the
/// associated record are selected. The returned request does not
/// require that the associated database table contains a matching row.
func _including(optional association: _SQLAssociation) -> Self
/// Creates a request that includes an association. The columns of the
/// associated record are selected. The returned request requires
/// that the associated database table contains a matching row.
func _including(required association: _SQLAssociation) -> Self
/// Creates a request that joins an association. The columns of the
/// associated record are not selected. The returned request does not
/// require that the associated database table contains a matching row.
func _joining(optional association: _SQLAssociation) -> Self
/// Creates a request that joins an association. The columns of the
/// associated record are not selected. The returned request requires
/// that the associated database table contains a matching row.
func _joining(required association: _SQLAssociation) -> Self
}
extension JoinableRequest {
/// Returns a request that fetches all records associated with each record
/// in this request.
///
/// For example, we can fetch authors along with their books:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord, Decodable {
/// static let books = hasMany(Book.self)
/// }
/// struct Book: TableRecord, FetchableRecord, Decodable { }
///
/// struct AuthorInfo: FetchableRecord, Decodable {
/// var author: Author
/// var books: [Book]
/// }
///
/// let authorInfos = try Author.all()
/// .including(all: Author.books)
/// .asRequest(of: AuthorInfo.self)
/// .fetchAll(db)
/// ```
public func including<A: AssociationToMany>(all association: A) -> Self where A.OriginRowDecoder == RowDecoder {
_including(all: association._sqlAssociation)
}
/// Returns a request that fetches the eventual record associated with each
/// record of this request.
///
/// For example, we can fetch books along with their eventual author:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord, Decodable { }
/// struct Book: TableRecord, FetchableRecord, Decodable {
/// static let author = belongsTo(Author.self)
/// }
///
/// struct BookInfo: FetchableRecord, Decodable {
/// var book: Book
/// var author: Author?
/// }
///
/// let bookInfos = try Book.all()
/// .including(optional: Book.author)
/// .asRequest(of: BookInfo.self)
/// .fetchAll(db)
/// ```
public func including<A: Association>(optional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
_including(optional: association._sqlAssociation)
}
/// Returns a request that fetches the record associated with each record in
/// this request. Records that do not have an associated record
/// are discarded.
///
/// For example, we can fetch books along with their eventual author:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord, Decodable { }
/// struct Book: TableRecord, FetchableRecord, Decodable {
/// static let author = belongsTo(Author.self)
/// }
///
/// struct BookInfo: FetchableRecord, Decodable {
/// var book: Book
/// var author: Author
/// }
///
/// let bookInfos = try Book.all()
/// .including(required: Book.author)
/// .asRequest(of: BookInfo.self)
/// .fetchAll(db)
/// ```
public func including<A: Association>(required association: A) -> Self where A.OriginRowDecoder == RowDecoder {
_including(required: association._sqlAssociation)
}
/// Returns a request that joins each record of this request to its
/// eventual associated record.
public func joining<A: Association>(optional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
_joining(optional: association._sqlAssociation)
}
/// Returns a request that joins each record of this request to its
/// associated record. Records that do not have an associated record
/// are discarded.
///
/// For example, we can fetch only books whose author is French:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord, Decodable { }
/// struct Book: TableRecord, FetchableRecord, Decodable {
/// static let author = belongsTo(Author.self)
/// }
///
/// let frenchAuthors = Book.author.filter(Column("countryCode") == "FR")
/// let bookInfos = try Book.all()
/// .joining(required: frenchAuthors)
/// .fetchAll(db)
/// ```
public func joining<A: Association>(required association: A) -> Self where A.OriginRowDecoder == RowDecoder {
_joining(required: association._sqlAssociation)
}
}
extension JoinableRequest where Self: SelectionRequest {
/// Appends the columns of the eventual associated record to the
/// selected columns.
///
/// For example:
///
/// ```swift
/// // SELECT player.*, team.color
/// // FROM player LEFT JOIN team ...
/// let teamColor = Player.team.select(Column("color"))
/// let request = Player.all().annotated(withOptional: teamColor)
/// ```
///
/// This method performs the exact same SQL request as
/// ``including(optional:)``. The difference is in the record type that can
/// decode such a request: the columns of the associated record must be
/// decoded at the same level as the main record. For example:
///
/// ```swift
/// struct PlayerWithTeamColor: FetchableRecord, Decodable {
/// var player: Player
/// var color: String?
/// }
/// try dbQueue.read { db in
/// let players = try request
/// .asRequest(of: PlayerWithTeamColor.self)
/// .fetchAll(db)
/// }
/// ```
///
/// This method is a convenience. You can build the same request with
/// ``TableAlias``, ``SelectionRequest/annotated(with:)-6ehs4``, and
/// ``JoinableRequest/joining(optional:)``:
///
/// ```swift
/// let teamAlias = TableAlias()
/// let request = Player.all()
/// .annotated(with: teamAlias[Column("color")])
/// .joining(optional: Player.team.aliased(teamAlias))
/// ```
public func annotated<A: Association>(withOptional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
// TODO: find a way to prefix the selection with the association key
let alias = TableAlias()
let selection = association._sqlAssociation.destination.relation.selectionPromise
return self
.joining(optional: association.aliased(alias))
.annotatedWhenConnected(with: { db in
try selection.resolve(db).map { selection in
selection.qualified(with: alias)
}
})
}
/// Appends the columns of the associated record to the selected columns.
/// Records that do not have an associated record are discarded.
///
/// For example:
///
/// ```swift
/// // SELECT player.*, team.color
/// // FROM player JOIN team ...
/// let teamColor = Player.team.select(Column("color"))
/// let request = Player.all().annotated(withRequired: teamColor)
/// ```
///
/// This method performs the exact same SQL request as
/// ``including(required:)``. The difference is in the record type that can
/// decode such a request: the columns of the associated record must be
/// decoded at the same level as the main record. For example:
///
/// ```swift
/// struct PlayerWithTeamColor: FetchableRecord, Decodable {
/// var player: Player
/// var color: String
/// }
/// try dbQueue.read { db in
/// let players = try request
/// .asRequest(of: PlayerWithTeamColor.self)
/// .fetchAll(db)
/// }
/// ```
///
/// This method is a convenience. You can build the same request with
/// ``TableAlias``, ``SelectionRequest/annotated(with:)-6ehs4``, and
/// ``JoinableRequest/joining(required:)``:
///
/// ```swift
/// let teamAlias = TableAlias()
/// let request = Player.all()
/// .annotated(with: teamAlias[Column("color")])
/// .joining(required: Player.team.aliased(teamAlias))
/// ```
public func annotated<A: Association>(withRequired association: A) -> Self where A.OriginRowDecoder == RowDecoder {
// TODO: find a way to prefix the selection with the association key
let selection = association._sqlAssociation.destination.relation.selectionPromise
let alias = TableAlias()
return self
.joining(required: association.aliased(alias))
.annotatedWhenConnected(with: { db in
try selection.resolve(db).map { selection in
selection.qualified(with: alias)
}
})
}
}
// MARK: - DerivableRequest
/// `DerivableRequest` is the base protocol for ``QueryInterfaceRequest``
/// and ``Association``.
///
/// Most features of `DerivableRequest` come from the protocols it
/// inherits from.
///
/// ## Topics
///
/// ### Instance Methods
///
/// - ``TableRequest/aliased(_:)``
/// - ``TableAlias``
///
/// ### The WITH Clause
///
/// - ``with(_:)``
///
/// ### The SELECT Clause
///
/// - ``SelectionRequest/annotated(with:)-4qcem``
/// - ``SelectionRequest/annotated(with:)-6ehs4``
/// - ``SelectionRequest/annotatedWhenConnected(with:)``
/// - ``distinct()``
/// - ``SelectionRequest/select(_:)-30yzl``
/// - ``SelectionRequest/select(_:)-7e2y5``
/// - ``SelectionRequest/select(literal:)``
/// - ``SelectionRequest/select(sql:arguments:)``
/// - ``SelectionRequest/selectWhenConnected(_:)``
///
/// ### The WHERE Clause
///
/// - ``FilteredRequest/all()``
/// - ``FilteredRequest/filter(_:)``
/// - ``TableRequest/filter(id:)``
/// - ``TableRequest/filter(ids:)``
/// - ``TableRequest/filter(key:)-1p9sq``
/// - ``TableRequest/filter(key:)-2te6v``
/// - ``TableRequest/filter(keys:)-6ggt1``
/// - ``TableRequest/filter(keys:)-8fbn9``
/// - ``FilteredRequest/filter(literal:)``
/// - ``FilteredRequest/filter(sql:arguments:)``
/// - ``FilteredRequest/filterWhenConnected(_:)``
/// - ``TableRequest/matching(_:)-3s3zr``
/// - ``TableRequest/matching(_:)-7c1e8``
/// - ``FilteredRequest/none()``
///
/// ### The GROUP BY and HAVING Clauses
///
/// - ``AggregatingRequest/group(_:)-edak``
/// - ``AggregatingRequest/group(_:)-4216o``
/// - ``AggregatingRequest/group(literal:)``
/// - ``AggregatingRequest/group(sql:arguments:)``
/// - ``TableRequest/groupByPrimaryKey()``
/// - ``AggregatingRequest/groupWhenConnected(_:)``
/// - ``AggregatingRequest/having(_:)``
/// - ``AggregatingRequest/having(literal:)``
/// - ``AggregatingRequest/having(sql:arguments:)``
/// - ``AggregatingRequest/havingWhenConnected(_:)``
///
/// ### The ORDER BY Clause
///
/// - ``OrderedRequest/order(_:)-63rzl``
/// - ``OrderedRequest/order(_:)-6co0m``
/// - ``OrderedRequest/order(literal:)``
/// - ``OrderedRequest/order(sql:arguments:)``
/// - ``OrderedRequest/orderWhenConnected(_:)``
/// - ``TableRequest/orderByPrimaryKey()``
/// - ``OrderedRequest/reversed()``
/// - ``OrderedRequest/unordered()``
/// - ``OrderedRequest/withStableOrder()``
///
/// ### Associations
///
/// - ``JoinableRequest/annotated(withOptional:)``
/// - ``JoinableRequest/annotated(withRequired:)``
/// - ``annotated(with:)-74xfs``
/// - ``annotated(with:)-8snn4``
/// - ``having(_:)``
/// - ``JoinableRequest/including(all:)``
/// - ``JoinableRequest/including(optional:)``
/// - ``JoinableRequest/including(required:)``
/// - ``JoinableRequest/joining(optional:)``
/// - ``JoinableRequest/joining(required:)``
///
/// ### Supporting Types
///
/// - ``AggregatingRequest``
/// - ``FilteredRequest``
/// - ``JoinableRequest``
/// - ``OrderedRequest``
/// - ``SelectionRequest``
/// - ``TableRequest``
/// - ``TypedRequest``
public protocol DerivableRequest<RowDecoder>: AggregatingRequest, FilteredRequest,
JoinableRequest, OrderedRequest,
SelectionRequest, TableRequest
{
/// Returns a request which returns distinct rows.
///
/// For example:
///
/// ```swift
/// // SELECT DISTINCT * FROM player
/// let request = Player.all().distinct()
///
/// // SELECT DISTINCT name FROM player
/// let request = Player.select(Column("name")).distinct()
/// ```
func distinct() -> Self
/// Embeds a common table expression.
///
/// If a common table expression with the same table name had already been
/// embedded, it is replaced by the new one.
///
/// For example, you can build a request that fetches all chats with their
/// latest message:
///
/// ```swift
/// let latestMessageRequest = Message
/// .annotated(with: max(Column("date")))
/// .group(Column("chatID"))
///
/// let latestMessageCTE = CommonTableExpression(
/// named: "latestMessage",
/// request: latestMessageRequest)
///
/// let latestMessageAssociation = Chat.association(
/// to: latestMessageCTE,
/// on: { chat, latestMessage in
/// chat[Column("id")] == latestMessage[Column("chatID")]
/// })
///
/// // WITH latestMessage AS
/// // (SELECT *, MAX(date) FROM message GROUP BY chatID)
/// // SELECT chat.*, latestMessage.*
/// // FROM chat
/// // LEFT JOIN latestMessage ON chat.id = latestMessage.chatID
/// let request = Chat.all()
/// .with(latestMessageCTE)
/// .including(optional: latestMessageAssociation)
/// ```
func with<RowDecoder>(_ cte: CommonTableExpression<RowDecoder>) -> Self
}
// Association aggregates don't require all DerivableRequest abilities. The
// minimum set of requirements is:
//
// - AggregatingRequest, for the GROUP BY and HAVING clauses
// - TableRequest, for grouping by primary key
// - JoinableRequest, for joining associations
// - SelectionRequest, for annotating the selection
//
// It is just that extending DerivableRequest is simpler. We want the user to
// use aggregates on QueryInterfaceRequest and associations: both conform to
// DerivableRequest already.
extension DerivableRequest {
private func annotated(with aggregate: AssociationAggregate<RowDecoder>) -> Self {
var request = self
let expression = aggregate.prepare(&request)
if let key = aggregate.key {
return request.annotated(with: expression.forKey(key))
} else {
return request.annotated(with: expression)
}
}
/// Appends association aggregates to the selected columns.
///
/// For example:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord, Decodable {
/// static let books = hasMany(Book.self)
/// }
/// struct Book: TableRecord, FetchableRecord, Decodable { }
///
/// struct AuthorInfo: FetchableRecord, Decodable {
/// var author: Author
/// var bookCount: Int
/// }
///
/// // SELECT author.*, COUNT(DISTINCT book.id) AS bookCount
/// // FROM author
/// // LEFT JOIN book ON book.authorId = author.id
/// // GROUP BY author.id
/// let authorInfos = try Author.all()
/// .annotated(with: Author.books.count)
/// .asRequest(of: AuthorInfo.self)
/// .fetchAll(db)
/// ```
public func annotated(with aggregates: AssociationAggregate<RowDecoder>...) -> Self {
annotated(with: aggregates)
}
/// Appends association aggregates to the selected columns.
///
/// For example:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord, Decodable {
/// static let books = hasMany(Book.self)
/// }
/// struct Book: TableRecord, FetchableRecord, Decodable { }
///
/// struct AuthorInfo: FetchableRecord, Decodable {
/// var author: Author
/// var bookCount: Int
/// }
///
/// // SELECT author.*, COUNT(DISTINCT book.id) AS bookCount
/// // FROM author
/// // LEFT JOIN book ON book.authorId = author.id
/// // GROUP BY author.id
/// let authorInfos = try Author.all()
/// .annotated(with: [Author.books.count])
/// .asRequest(of: AuthorInfo.self)
/// .fetchAll(db)
/// ```
public func annotated(with aggregates: [AssociationAggregate<RowDecoder>]) -> Self {
aggregates.reduce(self) { request, aggregate in
request.annotated(with: aggregate)
}
}
/// Filters the fetched records with an association aggregate.
///
/// For example:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord {
/// static let books = hasMany(Book.self)
/// }
/// struct Book: TableRecord, FetchableRecord { }
///
/// // SELECT author.*
/// // FROM author
/// // LEFT JOIN book ON book.authorId = author.id
/// // GROUP BY author.id
/// // HAVING COUNT(DISTINCT book.id) > 5
/// let authors = try Author.all()
/// .having(Author.books.count > 5)
/// .fetchAll(db)
/// ```
public func having(_ predicate: AssociationAggregate<RowDecoder>) -> Self {
var request = self
let expression = predicate.prepare(&request)
return request.having(expression)
}
}