add swiftUI code

This commit is contained in:
zeus
2025-01-22 14:09:10 +08:00
parent 68e7b7347c
commit 8a99853829
2531 changed files with 486215 additions and 0 deletions
@@ -0,0 +1,62 @@
extension TableRequest where Self: FilteredRequest {
// MARK: Full Text Search
/// Filters rows that match an ``FTS3`` full-text pattern.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM book WHERE book MATCH 'sqlite OR database'
/// let pattern = FTS3Pattern(matchingAnyTokenIn: "SQLite Database")
/// let request = Book.all().matching(pattern)
/// ```
///
/// If `pattern` is nil, the returned request fetches no row.
///
/// - parameter pattern: An ``FTS3Pattern``.
public func matching(_ pattern: FTS3Pattern?) -> Self {
guard let pattern else {
return none()
}
let alias = TableAlias()
let matchExpression = SQLExpression.tableMatch(alias, pattern.sqlExpression)
return self.aliased(alias).filter(matchExpression)
}
}
extension TableRecord {
// MARK: Full Text Search
/// Returns a request filtered on records that match an ``FTS3``
/// full-text pattern.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM book WHERE book MATCH 'sqlite OR database'
/// let pattern = FTS3Pattern(matchingAnyTokenIn: "SQLite Database")
/// let request = Book.matching(pattern)
/// ```
///
/// If `pattern` is nil, the returned request fetches no row.
///
/// - parameter pattern: An ``FTS3Pattern``.
public static func matching(_ pattern: FTS3Pattern?) -> QueryInterfaceRequest<Self> {
all().matching(pattern)
}
}
extension ColumnExpression {
/// A matching SQL expression with the `MATCH` SQL operator.
///
/// // content MATCH '...'
/// Column("content").match(pattern)
///
/// If the search pattern is nil, SQLite will evaluate the expression
/// to false.
public func match(_ pattern: FTS3Pattern?) -> SQLExpression {
.binary(.match, sqlExpression, pattern?.sqlExpression ?? .null)
}
}
@@ -0,0 +1,62 @@
#if SQLITE_ENABLE_FTS5
extension TableRequest where Self: FilteredRequest {
// MARK: Full Text Search
/// Filters rows that match an ``FTS5`` full-text pattern.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM book WHERE book MATCH 'sqlite OR database'
/// let pattern = FTS5Pattern(matchingAnyTokenIn: "SQLite Database")
/// let request = Book.all().matching(pattern)
/// ```
///
/// If `pattern` is nil, the returned request fetches no row.
///
/// - parameter pattern: An ``FTS5Pattern``.
public func matching(_ pattern: FTS5Pattern?) -> Self {
guard let pattern else {
return none()
}
let alias = TableAlias()
let matchExpression = SQLExpression.tableMatch(alias, pattern.sqlExpression)
return self.aliased(alias).filter(matchExpression)
}
}
extension TableRecord {
// MARK: Full Text Search
/// Returns a request filtered on records that match an ``FTS5``
/// full-text pattern.
///
/// For example:
///
/// ```swift
/// // SELECT * FROM book WHERE book MATCH 'sqlite OR database'
/// let pattern = FTS5Pattern(matchingAnyTokenIn: "SQLite Database")
/// let request = Book.matching(pattern)
/// ```
///
/// If `pattern` is nil, the returned request fetches no row.
///
/// - parameter pattern: An ``FTS5Pattern``.
public static func matching(_ pattern: FTS5Pattern?) -> QueryInterfaceRequest<Self> {
all().matching(pattern)
}
}
extension ColumnExpression {
/// A matching SQL expression with the `MATCH` SQL operator.
///
/// // content MATCH '...'
/// Column("content").match(pattern)
public func match(_ pattern: FTS5Pattern) -> SQLExpression {
.binary(.match, sqlExpression, pattern.sqlExpression)
}
}
#endif
@@ -0,0 +1,101 @@
/// A `ForeignKey` defines on which columns an association between two tables
/// is established.
///
/// You will need a `ForeignKey` when you define an ``Association`` between two
/// tables that are not unambiguously related with a single SQLite foreign key.
///
/// Sometimes the database schema does not define any foreign key between two
/// tables. And sometimes, there are several foreign keys from a table
/// to another:
///
/// | Table book | | Table person |
/// | ------------ | | ------------ |
/// | id | +--> id |
/// | authorId ---+ | name |
/// | translatorId ---+
/// | title |
///
/// When this happens, associations can't be automatically inferred from the
/// database schema. GRDB will complain with a fatal error such as "Ambiguous
/// foreign key from book to person", or "Could not infer foreign key from book
/// to person".
///
/// Your help is needed. You have to instruct which foreign key to use.
/// For example:
///
/// ```swift
/// struct Book: TableRecord {
/// // Define foreign keys
/// static let authorForeignKey = ForeignKey(["authorId"]))
/// static let translatorForeignKey = ForeignKey(["translatorId"]))
///
/// // Use foreign keys to define associations:
/// static let author = belongsTo(
/// Person.self,
/// key: "author",
/// using: authorForeignKey)
/// static let translator = belongsTo(
/// Person.self,
/// key: "translator",
/// using: translatorForeignKey)
/// }
/// ```
///
/// Foreign keys can also be defined from query interface columns:
///
/// ```swift
/// struct Book: TableRecord {
/// enum Columns: String, ColumnExpression {
/// case id, title, authorId, translatorId
/// }
///
/// static let authorForeignKey = ForeignKey([Columns.authorId]))
/// static let translatorForeignKey = ForeignKey([Columns.translatorId]))
/// }
/// ```
///
/// When the destination table does not define any primary key, you need to
/// provide the destination columns:
///
/// ```swift
/// struct Book: TableRecord {
/// static let authorForeignKey = ForeignKey(["authorId"], to: ["id"]))
/// static let translatorForeignKey = ForeignKey(["translatorId"], to: ["id"]))
/// }
/// ```
///
/// Foreign keys are always defined from the table that contains the columns at
/// the origin of the foreign key. `Person`'s symmetric associations reuse
/// foreign keys of `Book`:
///
/// ```swift
/// struct Person: TableRecord {
/// static let writtenBooks = hasMany(
/// Book.self,
/// key: "writtenBooks",
/// using: Book.authorForeignKey)
/// static let translatedBooks = hasMany(
/// Book.self,
/// key: "translatedBooks",
/// using: Book.translatorForeignKey)
/// }
/// ```
public struct ForeignKey: Equatable, Sendable {
var originColumns: [String]
var destinationColumns: [String]?
/// - parameter originColumns: The columns at the origin of the foreign key.
/// - parameter destinationColumns: The columns at the destination of the
/// foreign key. Use nil for the columns of the primary key.
public init(_ originColumns: [String], to destinationColumns: [String]? = nil) {
self.originColumns = originColumns
self.destinationColumns = destinationColumns
}
/// - parameter originColumns: The columns at the origin of the foreign key.
/// - parameter destinationColumns: The columns at the destination of the
/// foreign key. Use nil for the columns of the primary key.
public init(_ originColumns: [any ColumnExpression], to destinationColumns: [any ColumnExpression]? = nil) {
self.init(originColumns.map(\.name), to: destinationColumns?.map(\.name))
}
}
@@ -0,0 +1,314 @@
import Foundation
/// A type that defines a connection between two tables.
///
/// ``Association`` feeds methods of the ``JoinableRequest`` protocol. They are
/// built from a ``TableRecord`` type, or a ``Table`` instance.
///
/// ## Topics
///
/// ### Instance Methods
///
/// - ``forKey(_:)-247af``
/// - ``forKey(_:)-54yh6``
///
/// ### Associations To One
///
/// - ``BelongsToAssociation``
/// - ``HasOneAssociation``
/// - ``HasOneThroughAssociation``
/// - ``AssociationToOne``
///
/// ### Associations To Many
///
/// - ``HasManyAssociation``
/// - ``HasManyThroughAssociation``
/// - ``AssociationToMany``
///
/// ### Associations to Common Table Expressions
///
/// - ``JoinAssociation``
///
/// ### Supporting Types
///
/// - ``ForeignKey``
/// - ``Inflections``
public protocol Association: DerivableRequest {
// OriginRowDecoder and RowDecoder inherited from DerivableRequest provide
// type safety:
//
// Book.including(required: Book.author) // compiles
// Fruit.including(required: Book.author) // does not compile
/// The record type at the origin of the association.
///
/// In the ``BelongsToAssociation`` association below, it is `Book`:
///
/// ```swift
/// struct Book: TableRecord {
/// // BelongsToAssociation<Book, Author>
/// static let author = belongsTo(Author.self)
/// }
/// ```
associatedtype OriginRowDecoder
var _sqlAssociation: _SQLAssociation { get set }
/// Returns an association with the given key.
///
/// For example:
///
/// ```swift
/// struct Employee: FetchableRecord, TableRecord {
/// static let manager = belongsTo(Employee.self).forKey("manager")
/// static let subordinates = hasMany(Employee.self).forKey("subordinates")
/// }
///
/// struct EmployeeInfo: FetchableRecord, Decodable {
/// var employee: Employee
/// var manager: Employee? // property name matches the association key
/// var subordinates: [Employee] // property name matches the association key
/// }
///
/// try dbQueue.read { db in
/// let employeeInfos: [EmployeeInfo] = try Employee
/// .including(optional: Employee.manager)
/// .including(all: Employee.subordinates)
/// .asRequest(of: EmployeeInfo.self)
/// .fetchAll(db)
/// }
/// ```
func forKey(_ key: String) -> Self
}
extension Association {
/// Returns self modified with the *update* function.
func with(_ update: (inout Self) throws -> Void) rethrows -> Self {
var result = self
try update(&result)
return result
}
/// Returns self with destination relation modified with the *update* function.
fileprivate func withDestinationRelation(_ update: (inout SQLRelation) throws -> Void) rethrows -> Self {
var result = self
try update(&result._sqlAssociation.destination.relation)
return result
}
}
extension Association {
public func _including(all association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._including(all: association)
}
}
public func _including(optional association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._including(optional: association)
}
}
public func _including(required association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._including(required: association)
}
}
public func _joining(optional association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._joining(optional: association)
}
}
public func _joining(required association: _SQLAssociation) -> Self {
withDestinationRelation { relation in
relation = relation._joining(required: association)
}
}
}
extension Association {
/// The association key defines how rows fetched from this association
/// should be consumed.
///
/// For example:
///
/// struct Player: TableRecord {
/// // The default key of this association is the name of the
/// // database table for teams, let's say "team":
/// static let team = belongsTo(Team.self)
/// }
/// print(Player.team.key) // Prints "team"
///
/// // Consume rows:
/// let request = Player.including(required: Player.team)
/// for row in Row.fetchAll(db, request) {
/// let team: Team = row["team"] // the association key
/// }
///
/// The key can be redefined with the `forKey` method:
///
/// let request = Player.including(required: Player.team.forKey("custom"))
/// for row in Row.fetchAll(db, request) {
/// let team: Team = row["custom"]
/// }
var key: SQLAssociationKey { _sqlAssociation.destination.key }
/// Returns an association with the given key.
public func forKey(_ codingKey: some CodingKey) -> Self {
forKey(codingKey.stringValue)
}
}
// TableRequest conformance
extension Association {
public func aliased(_ alias: TableAlias) -> Self {
withDestinationRelation { relation in
relation = relation.aliased(alias)
}
}
}
// SelectionRequest conformance
extension Association {
public func selectWhenConnected(_ selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self {
withDestinationRelation { relation in
relation = relation.selectWhenConnected { db in
try selection(db).map(\.sqlSelection)
}
}
}
public func annotatedWhenConnected(with selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self {
withDestinationRelation { relation in
relation = relation.annotatedWhenConnected { db in
try selection(db).map(\.sqlSelection)
}
}
}
}
// FilteredRequest conformance
extension Association {
public func filterWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self {
withDestinationRelation { relation in
relation = relation.filterWhenConnected { db in
try predicate(db).sqlExpression
}
}
}
}
// OrderedRequest conformance
extension Association {
public func orderWhenConnected(_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) -> Self {
withDestinationRelation { relation in
relation = relation.orderWhenConnected { db in
try orderings(db).map(\.sqlOrdering)
}
}
}
public func reversed() -> Self {
withDestinationRelation { relation in
relation = relation.reversed()
}
}
public func unordered() -> Self {
withDestinationRelation { relation in
relation = relation.unordered()
}
}
public func withStableOrder() -> Self {
withDestinationRelation { relation in
relation = relation.withStableOrder()
}
}
}
// TableRequest conformance
extension Association {
public var databaseTableName: String {
_sqlAssociation.destination.relation.source.tableName
}
}
// AggregatingRequest conformance
extension Association {
public func groupWhenConnected(_ expressions: @escaping (Database) throws -> [any SQLExpressible]) -> Self {
withDestinationRelation { relation in
relation = relation.groupWhenConnected { db in
try expressions(db).map(\.sqlExpression)
}
}
}
public func havingWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self {
withDestinationRelation { relation in
relation = relation.havingWhenConnected { db in
try predicate(db).sqlExpression
}
}
}
}
// DerivableRequest conformance
extension Association {
public func distinct() -> Self {
withDestinationRelation { relation in
relation.isDistinct = true
}
}
public func with<RowDecoder>(_ cte: CommonTableExpression<RowDecoder>) -> Self {
withDestinationRelation { relation in
relation.ctes[cte.tableName] = cte.cte
}
}
}
// MARK: - AssociationToOne
/// An association that defines a to-one connection.
public protocol AssociationToOne: Association { }
extension AssociationToOne {
public func forKey(_ key: String) -> Self {
let associationKey = SQLAssociationKey.fixedSingular(key)
return with {
$0._sqlAssociation = $0._sqlAssociation.forDestinationKey(associationKey)
}
}
}
// MARK: - AssociationToMany
/// An association that defines a to-many connection.
///
/// ## Topics
///
/// ### Building Association Aggregates
///
/// - ``average(_:)``
/// - ``count``
/// - ``isEmpty``
/// - ``max(_:)``
/// - ``min(_:)``
/// - ``sum(_:)``
/// - ``total(_:)``
///
/// - ``AssociationAggregate``
public protocol AssociationToMany: Association { }
extension AssociationToMany {
public func forKey(_ key: String) -> Self {
let associationKey = SQLAssociationKey.fixedPlural(key)
return with {
$0._sqlAssociation = $0._sqlAssociation.forDestinationKey(associationKey)
}
}
}
@@ -0,0 +1,883 @@
import Foundation
extension AssociationToMany {
private func makeAggregate(_ expression: SQLExpression) -> AssociationAggregate<OriginRowDecoder> {
AssociationAggregate(preparation: BasePreparation(association: self, expression: expression))
}
/// The number of associated records.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// // Fetch all teams with at least ten players:
/// let teams: [Team] = try Team
/// .having(Team.players.count >= 10)
/// .fetchAll(db)
/// }
/// ```
///
/// The returned association aggregate is named `"[key]Count"`, where `key`
/// is the association key. For example:
///
/// ```swift
/// struct TeamInfo: FetchableRecord, Decodable {
/// var team: Team
/// var playerCount: Int
/// }
///
/// try dbQueue.read { db in
/// let infos: [TeamInfo] = try Team
/// .annotated(with: Team.players.count)
/// .asRequest(of: TeamInfo.self)
/// .fetchAll(db)
/// }
/// ```
public var count: AssociationAggregate<OriginRowDecoder> {
makeAggregate(.countDistinct(.fastPrimaryKey))
.forKey("\(key.singularizedName)Count")
}
/// Returns a boolean aggregate that is true if no associated
/// records exist.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// // Fetch all teams without any player
/// let teams: [Team] = try Team
/// .having(Team.players.isEmpty)
/// .fetchAll(db)
///
/// // Fetch all teams without some player
/// let teams: [Team] = try Team
/// .having(Team.players.isEmpty == false)
/// .fetchAll(db)
/// }
/// ```
///
/// The returned association aggregate is named `"hasNo[key]"`, where `key`
/// is the association key. For example:
///
/// ```swift
/// struct TeamInfo: FetchableRecord, Decodable {
/// var team: Team
/// var hasNoPlayer: Int
/// }
///
/// try dbQueue.read { db in
/// let infos: [TeamInfo] = try Team
/// .annotated(with: Team.players.isEmpty)
/// .asRequest(of: TeamInfo.self)
/// .fetchAll(db)
/// }
/// ```
public var isEmpty: AssociationAggregate<OriginRowDecoder> {
makeAggregate(.isEmpty(.countDistinct(.fastPrimaryKey)))
.forKey("hasNo\(key.singularizedName.uppercasingFirstCharacter)")
}
/// Returns the average of the given expression in associated records.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// // Fetch all teams whose average player score is greater than 1000
/// let averageScore = Team.players.average(Column("score"))
/// let teams: [Team] = try Team
/// .having(averageScore >= 1000)
/// .fetchAll(db)
/// }
/// ```
///
/// When the input expression is a ``ColumnExpression``, the returned
/// association aggregate is named `"average[Key][Column]"`, where `key` is
/// the association key. For example:
///
/// ```swift
/// struct TeamInfo: FetchableRecord, Decodable {
/// var team: Team
/// var averagePlayerScore: Double
/// }
///
/// try dbQueue.read { db in
/// let averageScore = Team.players.average(Column("score"))
/// let infos: [TeamInfo] = try Team
/// .annotated(with: averageScore)
/// .asRequest(of: TeamInfo.self)
/// .fetchAll(db)
/// }
/// ```
public func average(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(.function("AVG", [expression.sqlExpression]))
if let column = expression as? any ColumnExpression {
let name = key.singularizedName
return aggregate.forKey("average\(name.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// Returns the maximum value of the given expression in associated records.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// // Fetch all teams whose maximum player score is greater than 1000
/// let maxScore = Team.players.max(Column("score"))
/// let teams: [Team] = try Team
/// .having(maxScore >= 1000)
/// .fetchAll(db)
/// }
/// ```
///
/// When the input expression is a ``ColumnExpression``, the returned
/// association aggregate is named `"maximum[Key][Column]"`, where `key` is
/// the association key. For example:
///
/// ```swift
/// struct TeamInfo: FetchableRecord, Decodable {
/// var team: Team
/// var maximumPlayerScore: Double
/// }
///
/// try dbQueue.read { db in
/// let maxScore = Team.players.max(Column("score"))
/// let infos: [TeamInfo] = try Team
/// .annotated(with: maxScore)
/// .asRequest(of: TeamInfo.self)
/// .fetchAll(db)
/// }
/// ```
public func max(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(.function("MAX", [expression.sqlExpression]))
if let column = expression as? any ColumnExpression {
let name = key.singularizedName
return aggregate.forKey("max\(name.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// Returns the minimum value of the given expression in associated records.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// // Fetch all teams whose minimum player score is less than 1000
/// let minScore = Team.players.min(Column("score"))
/// let teams: [Team] = try Team
/// .having(minScore < 1000)
/// .fetchAll(db)
/// }
/// ```
///
/// When the input expression is a ``ColumnExpression``, the returned
/// association aggregate is named `"minimum[Key][Column]"`, where `key` is
/// the association key. For example:
///
/// ```swift
/// struct TeamInfo: FetchableRecord, Decodable {
/// var team: Team
/// var minimumPlayerScore: Double
/// }
///
/// try dbQueue.read { db in
/// let minScore = Team.players.min(Column("score"))
/// let infos: [TeamInfo] = try Team
/// .annotated(with: minScore)
/// .asRequest(of: TeamInfo.self)
/// .fetchAll(db)
/// }
/// ```
public func min(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(.function("MIN", [expression.sqlExpression]))
if let column = expression as? any ColumnExpression {
let name = key.singularizedName
return aggregate.forKey("min\(name.uppercasingFirstCharacter)\(column.name.uppercasingFirstCharacter)")
} else {
return aggregate
}
}
/// Returns the sum of the given expression in associated records.
///
/// This aggregate invokes the `SUM` SQL function. See also ``total(_:)``
/// and <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// // Fetch all teams whose sum of player scores is greater than 1000
/// let scoreSum = Team.players.sum(Column("score"))
/// let teams: [Team] = try Team
/// .having(scoreSum >= 1000)
/// .fetchAll(db)
/// }
/// ```
///
/// When the input expression is a ``ColumnExpression``, the returned
/// association aggregate is named `"[key][Column]Sum"`, where `key` is the
/// association key. For example:
///
/// ```swift
/// struct TeamInfo: FetchableRecord, Decodable {
/// var team: Team
/// var playerScoreSum: Double
/// }
///
/// try dbQueue.read { db in
/// let scoreSum = Team.players.sum(Column("score"))
/// let infos: [TeamInfo] = try Team
/// .annotated(with: scoreSum)
/// .asRequest(of: TeamInfo.self)
/// .fetchAll(db)
/// }
/// ```
public func sum(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(.function("SUM", [expression.sqlExpression]))
if let column = expression as? any ColumnExpression {
let name = key.singularizedName
return aggregate.forKey("\(name)\(column.name.uppercasingFirstCharacter)Sum")
} else {
return aggregate
}
}
/// Returns the sum of the given expression in associated records.
///
/// This aggregate invokes the `TOTAL` SQL function. See also ``sum(_:)``
/// and <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// // Fetch all teams whose sum of player scores is greater than 1000
/// let totalScore = Team.players.total(Column("score"))
/// let teams: [Team] = try Team
/// .having(totalScore >= 1000)
/// .fetchAll(db)
/// }
/// ```
///
/// When the input expression is a ``ColumnExpression``, the returned
/// association aggregate is named `"[key][Column]Sum"`, where `key` is the
/// association key. For example:
///
/// ```swift
/// struct TeamInfo: FetchableRecord, Decodable {
/// var team: Team
/// var playerScoreSum: Double
/// }
///
/// try dbQueue.read { db in
/// let totalScore = Team.players.total(Column("score"))
/// let infos: [TeamInfo] = try Team
/// .annotated(with: totalScore)
/// .asRequest(of: TeamInfo.self)
/// .fetchAll(db)
/// }
/// ```
public func total(_ expression: some SQLSpecificExpressible) -> AssociationAggregate<OriginRowDecoder> {
let aggregate = makeAggregate(.function("TOTAL", [expression.sqlExpression]))
if let column = expression as? any ColumnExpression {
let name = key.singularizedName
// Yes we use the `Sum` suffix instead of `Total`. Both `total(_:)`
// and `sum(_:)` compute sums.
return aggregate.forKey("\(name)\(column.name.uppercasingFirstCharacter)Sum")
} else {
return aggregate
}
}
}
/// A value aggregated from a population of associated records.
///
/// You build an `AssociationAggregate` from an ``AssociationToMany``.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// // An association aggregate
/// let playerCount = Team.players.count
///
/// // Fetch all teams with at least ten players:
/// let teams: [Team] = try Team
/// .having(playerCount >= 10)
/// .fetchAll(db)
/// }
/// ```
///
/// ## Topics
///
/// ### Instance Methods
///
/// - ``forKey(_:)-1rvux``
/// - ``forKey(_:)-1ua4j``
///
/// ### Top-Level Functions
///
/// - ``abs(_:)-43n8v``
/// - ``cast(_:as:)-63ttx``
/// - ``length(_:)-9dr2v``
public struct AssociationAggregate<RowDecoder> {
fileprivate let preparation: AssociationAggregatePreparation<RowDecoder>
/// The SQL name for the value of this aggregate. See forKey(_:).
var key: String? = nil
/// Extends the request with the associated records used to compute the
/// aggregate, and returns the aggregated expression.
///
/// For example:
///
/// struct Author: TableRecord {
/// static let books = hasMany(Book.self)
/// }
///
/// // SELECT * FROM author
/// var request = Author.all()
///
/// let aggregate = Author.books.count
/// let expression = aggregate.prepare(&request)
///
/// // The request has been extended with associated records:
/// //
/// // SELECT author.* FROM author
/// // LEFT JOIN book ON book.authorId = author.id
/// // GROUP BY author.id
/// request
///
/// // The aggregated value:
/// //
/// // COUNT(DISTINCT book.id)
/// expression
///
/// The aggregated expression is not embedded in the extended request:
///
/// - We don't know yet if the aggregated expression will be used in the
/// SQL selection, or in the HAVING clause.
/// - It helps implementing aggregate operators such as `&&`, `+`, etc.
func prepare(_ request: inout some DerivableRequest<RowDecoder>) -> SQLExpression {
preparation.prepare(&request)
}
}
extension AssociationAggregate: Refinable {
/// Returns an aggregate that is selected in a column with the given name.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Team: FetchableRecord, TableRecord {
/// static let players = Team.hasMany(Player.self)
/// }
///
/// struct TeamInfo: FetchableRecord, Decodable {
/// var team: Team
/// var numberOfBooks: Int
/// }
///
/// try dbQueue.read { db in
/// let playerCount = Team.players.count.forKey("numberOfBooks")
///
/// let infos: [TeamInfo] = try Team
/// .annotated(with: playerCount)
/// .asRequest(of: TeamInfo.self)
/// .fetchAll(db)
/// }
/// ```
public func forKey(_ key: String) -> Self {
with {
$0.key = key
}
}
/// Returns an aggregate that is selected in a column named like the given
/// coding key.
///
/// See ``forKey(_:)-1rvux``.
public func forKey(_ key: some CodingKey) -> Self {
forKey(key.stringValue)
}
}
// MARK: - AssociationAggregatePreparation
/// An abstract class that only exists as support for
/// `AssociationAggregate.prepare(_:)`, which needs to prepare both query
/// interface requests and associations through their conformance
/// to `DerivableRequest`:
///
/// aggregate.prepare(&request)
/// aggregate.prepare(&association)
///
/// We could have used a generic closure instead of this class... if only Swift
/// would support generic closures.
private class AssociationAggregatePreparation<RowDecoder> {
func prepare(_ request: inout some DerivableRequest<RowDecoder>) -> SQLExpression {
fatalError("subclass must override")
}
}
/// Prepares a request so that it can use association aggregates.
private class BasePreparation<Association: AssociationToMany>:
AssociationAggregatePreparation<Association.OriginRowDecoder>
{
private let association: Association
private let expression: SQLExpression
init(association: Association, expression: SQLExpression) {
self.association = association
self.expression = expression
}
override func prepare(_ request: inout some DerivableRequest<Association.OriginRowDecoder>) -> SQLExpression {
// The fundamental request that supports association aggregate:
//
// SELECT parent.*
// LEFT JOIN child ON child.parentID = parent.id
// GROUP BY parent.id
let tableAlias = TableAlias()
request = request
.joining(optional: association.aliased(tableAlias))
.groupByPrimaryKey()
// The fundamental request can now be annotated, or filtered in the
// having clause, with the association aggregate expression:
// MIN(child.score), COUNT(DISTINCT child.id), etc.
return expression.qualified(with: tableAlias)
}
}
/// Transforms the expression of an aggregate.
private class MapPreparation<RowDecoder>: AssociationAggregatePreparation<RowDecoder> {
private let base: AssociationAggregatePreparation<RowDecoder>
private let transform: (SQLExpression) -> SQLExpression
init(
base: AssociationAggregatePreparation<RowDecoder>,
transform: @escaping (SQLExpression) -> SQLExpression)
{
self.base = base
self.transform = transform
}
override func prepare(_ request: inout some DerivableRequest<RowDecoder>) -> SQLExpression {
transform(base.prepare(&request))
}
}
extension AssociationAggregate {
/// Transforms the expression, and does not preserve key.
fileprivate func map(_ transform: @escaping (SQLExpression) -> SQLExpression) -> Self {
AssociationAggregate(preparation: MapPreparation(base: preparation, transform: transform))
}
}
/// Combines the expressions of two aggregates.
private class CombinePreparation<RowDecoder>: AssociationAggregatePreparation<RowDecoder> {
private let lhs: AssociationAggregatePreparation<RowDecoder>
private let rhs: AssociationAggregatePreparation<RowDecoder>
private let combine: (_ lhs: SQLExpression, _ rhs: SQLExpression) -> SQLExpression
init(
_ lhs: AssociationAggregatePreparation<RowDecoder>,
_ rhs: AssociationAggregatePreparation<RowDecoder>,
combine: @escaping (_ lhs: SQLExpression, _ rhs: SQLExpression) -> SQLExpression)
{
self.lhs = lhs
self.rhs = rhs
self.combine = combine
}
override func prepare(_ request: inout some DerivableRequest<RowDecoder>) -> SQLExpression {
let lhsExpression = lhs.prepare(&request)
let rhsExpression = rhs.prepare(&request)
return combine(lhsExpression, rhsExpression)
}
}
/// Combines the expression of two aggregates.
private func combine<RowDecoder>(
_ lhs: AssociationAggregate<RowDecoder>,
_ rhs: AssociationAggregate<RowDecoder>,
with combine: @escaping (_ lhs: SQLExpression, _ rhs: SQLExpression) -> SQLExpression)
-> AssociationAggregate<RowDecoder>
{
AssociationAggregate(preparation: CombinePreparation(lhs.preparation, rhs.preparation, combine: combine))
}
// MARK: - Logical Operators (AND, OR, NOT)
extension AssociationAggregate {
/// A negated logical aggregate.
///
/// For example:
///
/// ```swift
/// Author.having(!Author.books.isEmpty)
/// ```
public static prefix func ! (aggregate: Self) -> Self {
aggregate.map { !$0 }
}
/// The `AND` SQL operator.
public static func && (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: &&)
}
// TODO: test
/// The `AND` SQL operator.
public static func && (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 && rhs }
}
// TODO: test
/// The `AND` SQL operator.
public static func && (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs && $0 }
}
/// The `OR` SQL operator.
public static func || (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: ||)
}
// TODO: test
/// The `OR` SQL operator.
public static func || (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 || rhs }
}
// TODO: test
/// The `OR` SQL operator.
public static func || (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs || $0 }
}
}
// MARK: - Egality and Identity Operators (=, <>, IS, IS NOT)
extension AssociationAggregate {
/// The `=` SQL operator.
public static func == (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: ==)
}
/// The `=` SQL operator.
///
/// When the right operand is nil, `IS NULL` is used instead of the
/// `=` operator.
public static func == (lhs: Self, rhs: (any SQLExpressible)?) -> Self {
lhs.map { $0 == rhs }
}
/// The `=` SQL operator.
///
/// When the left operand is nil, `IS NULL` is used instead of the
/// `=` operator.
public static func == (lhs: (any SQLExpressible)?, rhs: Self) -> Self {
rhs.map { lhs == $0 }
}
/// The `=` SQL operator.
public static func == (lhs: Self, rhs: Bool) -> Self {
lhs.map { $0 == rhs }
}
/// The `=` SQL operator.
public static func == (lhs: Bool, rhs: Self) -> Self {
rhs.map { lhs == $0 }
}
/// The `<>` SQL operator.
public static func != (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: !=)
}
/// The `<>` SQL operator.
///
/// When the right operand is nil, `IS NOT NULL` is used instead of the
/// `<>` operator.
public static func != (lhs: Self, rhs: (any SQLExpressible)?) -> Self {
lhs.map { $0 != rhs }
}
/// The `<>` SQL operator.
///
/// When the left operand is nil, `IS NOT NULL` is used instead of the
/// `<>` operator.
public static func != (lhs: (any SQLExpressible)?, rhs: Self) -> Self {
rhs.map { lhs != $0 }
}
/// The `<>` SQL operator.
public static func != (lhs: Self, rhs: Bool) -> Self {
lhs.map { $0 != rhs }
}
/// The `<>` SQL operator.
public static func != (lhs: Bool, rhs: Self) -> Self {
rhs.map { lhs != $0 }
}
/// The `IS` SQL operator.
public static func === (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: ===)
}
/// The `IS` SQL operator.
public static func === (lhs: Self, rhs: (any SQLExpressible)?) -> Self {
lhs.map { $0 === rhs }
}
/// The `IS` SQL operator.
public static func === (lhs: (any SQLExpressible)?, rhs: Self) -> Self {
rhs.map { lhs === $0 }
}
/// The `IS NOT` SQL operator.
public static func !== (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: !==)
}
/// The `IS NOT` SQL operator.
public static func !== (lhs: Self, rhs: (any SQLExpressible)?) -> Self {
lhs.map { $0 !== rhs }
}
/// The `IS NOT` SQL operator.
public static func !== (lhs: (any SQLExpressible)?, rhs: Self) -> Self {
rhs.map { lhs !== $0 }
}
}
// MARK: - Comparison Operators (<, >, <=, >=)
extension AssociationAggregate {
/// The `<=` SQL operator.
public static func <= (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: <=)
}
/// The `<=` SQL operator.
public static func <= (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 <= rhs }
}
/// The `<=` SQL operator.
public static func <= (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs <= $0 }
}
/// The `<` SQL operator.
public static func < (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: <)
}
/// The `<` SQL operator.
public static func < (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 < rhs }
}
/// The `<` SQL operator.
public static func < (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs < $0 }
}
/// The `>` SQL operator.
public static func > (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: >)
}
/// The `>` SQL operator.
public static func > (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 > rhs }
}
/// The `>` SQL operator.
public static func > (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs > $0 }
}
/// The `>=` SQL operator.
public static func >= (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: >=)
}
/// The `>=` SQL operator.
public static func >= (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 >= rhs }
}
/// The `>=` SQL operator.
public static func >= (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs >= $0 }
}
}
// MARK: - Arithmetic Operators (+, -, *, /)
extension AssociationAggregate {
/// The `-` SQL operator.
public static prefix func - (aggregate: Self) -> Self {
aggregate.map { -$0 }
}
/// The `+` SQL operator.
public static func + (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: +)
}
/// The `+` SQL operator.
public static func + (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 + rhs }
}
/// The `+` SQL operator.
public static func + (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs + $0 }
}
/// The `-` SQL operator.
public static func - (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: -)
}
/// The `-` SQL operator.
public static func - (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 - rhs }
}
/// The `-` SQL operator.
public static func - (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs - $0 }
}
/// The `*` SQL operator.
public static func * (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: *)
}
/// The `*` SQL operator.
public static func * (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 * rhs }
}
/// The `*` SQL operator.
public static func * (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs * $0 }
}
/// The `/` SQL operator.
public static func / (lhs: Self, rhs: Self) -> Self {
combine(lhs, rhs, with: /)
}
/// The `/` SQL operator.
public static func / (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs.map { $0 / rhs }
}
/// The `/` SQL operator.
public static func / (lhs: some SQLExpressible, rhs: Self) -> Self {
rhs.map { lhs / $0 }
}
}
// MARK: - Functions
extension AssociationAggregate {
/// The `IFNULL` SQL function.
///
/// For example:
///
/// ```swift
/// Team.annotated(with: Team.players.min(Column("score")) ?? 0)
/// ```
///
/// The returned aggregate has the same key as the input.
public static func ?? (lhs: Self, rhs: some SQLExpressible) -> Self {
lhs
.map { $0 ?? rhs }
.with { $0.key = lhs.key } // Preserve key
}
}
/// The `ABS` SQL function.
public func abs<RowDecoder>(_ aggregate: AssociationAggregate<RowDecoder>)
-> AssociationAggregate<RowDecoder>
{
aggregate.map(abs)
}
/// The `CAST` SQL function.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_expr.html#castexpr>
public func cast<RowDecoder>(
_ aggregate: AssociationAggregate<RowDecoder>,
as storageClass: Database.StorageClass)
-> AssociationAggregate<RowDecoder>
{
aggregate
.map { cast($0, as: storageClass) }
.with { $0.key = aggregate.key } // Preserve key
}
/// The `LENGTH` SQL function.
public func length<RowDecoder>(_ aggregate: AssociationAggregate<RowDecoder>)
-> AssociationAggregate<RowDecoder>
{
aggregate.map(length)
}
@@ -0,0 +1,94 @@
/// Thes `BelongsToAssociation` sets up a one-to-one connection from a record
/// type to another record type, such as each instance of the declaring record
/// "belongs to" an instance of the other record.
///
/// For example, if your application includes authors and books, and each book
/// is assigned its author, you'd declare the association this way:
///
/// ```swift
/// struct Author: TableRecord { }
/// struct Book: TableRecord {
/// static let author = belongsTo(Author.self)
/// }
/// ```
///
/// A `BelongsToAssociation` should be supported by an SQLite foreign key.
///
/// Foreign keys are the recommended way to declare relationships between
/// database tables because not only will SQLite guarantee the integrity of your
/// data, but GRDB will be able to use those foreign keys to automatically
/// configure your associations.
///
/// You define the foreign key when you create database tables. For example:
///
/// ```swift
/// try db.create(table: "author") { t in
/// t.autoIncrementedPrimaryKey("id") // (1)
/// t.column("name", .text)
/// }
/// try db.create(table: "book") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("author", onDelete: .cascade) // (2)
/// .notNull() // (3)
/// t.column("title", .text)
/// }
/// ```
///
/// 1. The author table has a primary key.
/// 2. The `book.authorId` column is used to link a book to the author it
/// belongs to. This column is indexed in order to ease the selection of
/// an author's books. A foreign key is defined from `book.authorId`
/// column to `authors.id`, so that SQLite guarantees that no book refers
/// to a missing author. The `onDelete: .cascade` option has SQLite
/// automatically delete all of an author's books when that author is
/// deleted. See <https://sqlite.org/foreignkeys.html#fk_actions> for
/// more information.
/// 3. Make the `book.authorId` column not null if you want SQLite to guarantee
/// that all books have an author.
///
/// The example above uses auto-incremented primary keys. But generally
/// speaking, all primary keys are supported.
///
/// If the database schema does not define foreign keys between tables, you can
/// still use `BelongsToAssociation`. But your help is needed to define the
/// missing foreign key:
///
/// ```swift
/// struct Book: TableRecord {
/// static let author = belongsTo(Author.self, using: ForeignKey(...))
/// }
/// ```
public struct BelongsToAssociation<Origin, Destination> {
public var _sqlAssociation: _SQLAssociation
init(
to destinationRelation: SQLRelation,
key: String?,
using foreignKey: ForeignKey?)
{
let destinationTable = destinationRelation.source.tableName
let foreignKeyCondition = SQLForeignKeyCondition(
destinationTable: destinationTable,
foreignKey: foreignKey,
originIsLeft: true)
let associationKey: SQLAssociationKey
if let key {
associationKey = .fixedSingular(key)
} else {
associationKey = .inflected(destinationTable)
}
_sqlAssociation = _SQLAssociation(
key: associationKey,
condition: .foreignKey(foreignKeyCondition),
relation: destinationRelation,
cardinality: .toOne)
}
}
extension BelongsToAssociation: AssociationToOne {
public typealias OriginRowDecoder = Origin
public typealias RowDecoder = Destination
}
@@ -0,0 +1,93 @@
/// The `HasManyAssociation` indicates a one-to-many connection between two
/// record types, such as each instance of the declaring record "has many"
/// instances of the other record.
///
/// For example, if your application includes authors and books, and each author
/// is assigned zero or more books, you'd declare the association this way:
///
/// ```swift
/// struct Book: TableRecord { }
/// struct Author: TableRecord {
/// static let books = hasMany(Book.self)
/// }
/// ```
///
/// A `HasManyAssociation` should be supported by an SQLite foreign key.
///
/// Foreign keys are the recommended way to declare relationships between
/// database tables because not only will SQLite guarantee the integrity of your
/// data, but GRDB will be able to use those foreign keys to automatically
/// configure your associations.
///
/// You define the foreign key when you create database tables. For example:
///
/// ```swift
/// try db.create(table: "author") { t in
/// t.autoIncrementedPrimaryKey("id") // (1)
/// t.column("name", .text)
/// }
/// try db.create(table: "book") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("author", onDelete: .cascade) // (2)
/// .notNull() // (3)
/// t.column("title", .text)
/// }
/// ```
///
/// 1. The author table has a primary key.
/// belongs to. This column is indexed in order to ease the selection of
/// an author's books. A foreign key is defined from `book.authorId`
/// column to `authors.id`, so that SQLite guarantees that no book refers
/// to a missing author. The `onDelete: .cascade` option has SQLite
/// automatically delete all of an author's books when that author is
/// deleted. See <https://sqlite.org/foreignkeys.html#fk_actions> for
/// more information.
/// 3. Make the `book.authorId` column not null if you want SQLite to guarantee
/// that all books have an author.
///
/// The example above uses auto-incremented primary keys. But generally
/// speaking, all primary keys are supported.
///
/// If the database schema does not define foreign keys between tables, you can
/// still use `HasManyAssociation`. But your help is needed to define the
/// missing foreign key:
///
/// ```swift
/// struct Author: TableRecord {
/// static let books = hasMany(Book.self, using: ForeignKey(...))
/// }
/// ```
public struct HasManyAssociation<Origin, Destination> {
public var _sqlAssociation: _SQLAssociation
init(
to destinationRelation: SQLRelation,
key: String?,
using foreignKey: ForeignKey?)
{
let destinationTable = destinationRelation.source.tableName
let foreignKeyCondition = SQLForeignKeyCondition(
destinationTable: destinationTable,
foreignKey: foreignKey,
originIsLeft: false)
let associationKey: SQLAssociationKey
if let key {
associationKey = .fixedPlural(key)
} else {
associationKey = .inflected(destinationTable)
}
_sqlAssociation = _SQLAssociation(
key: associationKey,
condition: .foreignKey(foreignKeyCondition),
relation: destinationRelation,
cardinality: .toMany)
}
}
extension HasManyAssociation: AssociationToMany {
public typealias OriginRowDecoder = Origin
public typealias RowDecoder = Destination
}
@@ -0,0 +1,66 @@
/// The `HasManyThroughAssociation` is often used to set up a many-to-many
/// connection with another record. This association indicates that the
/// declaring record can be matched with zero or more instances of another
/// record by proceeding through a third record.
///
/// For example, consider the practice of passport delivery. One country
/// "has many" citizens "through" its passports:
///
/// ```swift
/// struct Citizen: TableRecord { }
///
/// struct Passport: TableRecord {
/// static let citizen = belongsTo(Citizen.self)
/// }
///
/// struct Country: TableRecord {
/// static let passports = hasMany(Passport.self)
/// static let citizens = hasMany(Citizen.self,
/// through: passports,
/// using: Passport.citizen)
/// }
/// ```
///
/// The `HasManyThroughAssociation` is also useful for setting up "shortcuts"
/// through nested associations. For example, if a document has many sections,
/// and a section has many paragraphs, you may sometimes want to get a simple
/// collection of all paragraphs in the document. You could set
/// that up this way:
///
/// ```swift
/// struct Paragraph: TableRecord { }
///
/// struct Section: TableRecord {
/// static let paragraphs = hasMany(Paragraph.self)
/// }
///
/// struct Document: TableRecord {
/// static let sections = hasMany(Section.self)
/// static let paragraphs = hasMany(Paragraph.self,
/// through: sections,
/// using: Section.paragraphs)
/// }
/// ```
///
/// As in the examples above, `HasManyThroughAssociation` is always built from
/// two other associations. Those associations can be any ``Association``.
public struct HasManyThroughAssociation<Origin, Destination> {
public var _sqlAssociation: _SQLAssociation
init<Pivot, Target>(
through pivot: Pivot,
using target: Target)
where Pivot: Association,
Target: Association,
Pivot.OriginRowDecoder == Origin,
Pivot.RowDecoder == Target.OriginRowDecoder,
Target.RowDecoder == Destination
{
_sqlAssociation = target._sqlAssociation.through(pivot._sqlAssociation)
}
}
extension HasManyThroughAssociation: AssociationToMany {
public typealias OriginRowDecoder = Origin
public typealias RowDecoder = Destination
}
@@ -0,0 +1,100 @@
/// The `HasOneAssociation` indicates a one-to-one connection between two
/// record types, such as each instance of the declaring record "has one"
/// instances of the other record.
///
/// For example, if your application has one database table for countries, and
/// another for their demographic profiles, you'd declare the association
/// this way:
///
/// ```swift
/// struct Demographics: TableRecord { }
/// struct Country: TableRecord {
/// static let demographics = hasOne(Demographics.self)
/// }
/// ```
///
/// A `HasOneAssociation` should be supported by an SQLite foreign key.
///
/// Foreign keys are the recommended way to declare relationships between
/// database tables because not only will SQLite guarantee the integrity of your
/// data, but GRDB will be able to use those foreign keys to automatically
/// configure your associations.
///
/// You define the foreign key when you create database tables. For example:
///
/// ```swift
/// try db.create(table: "country") { t in
/// t.primaryKey("code", .text) // (1)
/// t.column("name", .text)
/// }
/// try db.create(table: "demographics") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("country", onDelete: .cascade) // (2)
/// .notNull() // (3)
/// .unique() // (4)
/// t.column("population", .integer)
/// t.column("density", .double)
/// }
/// ```
///
/// 1. The country table has a primary key.
/// 2. The `demographics.countryCode` column is used to link a demographic
/// profile to the country it belongs to. This column is indexed in order
/// to ease the selection of the demographics of a country. A foreign key
/// is defined from `demographics.countryCode` column to `country.code`,
/// so that SQLite guarantees that no profile refers to a missing
/// country. The `onDelete: .cascade` option has SQLite automatically
/// delete a profile when its country is deleted. See
/// <https://sqlite.org/foreignkeys.html#fk_actions> for more information.
/// 3. Make the `demographics.countryCode` column not null if you want SQLite to
/// guarantee that all profiles are linked to a country.
/// 4. Create a unique index on the `demographics.countryCode` column in order
/// to guarantee the unicity of any country's demographics.
///
/// The example above uses a string primary for the country table. But generally
/// speaking, all primary keys are supported.
///
/// If the database schema does not define foreign keys between tables, you can
/// still use `HasOneAssociation`. But your help is needed to define the
/// missing foreign key:
///
/// ```swift
/// struct Demographics: TableRecord { }
/// struct Country: TableRecord {
/// static let demographics = hasOne(Demographics.self, using: ForeignKey(...)
/// }
/// ```
public struct HasOneAssociation<Origin, Destination> {
public var _sqlAssociation: _SQLAssociation
init(
to destinationRelation: SQLRelation,
key: String?,
using foreignKey: ForeignKey?)
{
let destinationTable = destinationRelation.source.tableName
let foreignKeyCondition = SQLForeignKeyCondition(
destinationTable: destinationTable,
foreignKey: foreignKey,
originIsLeft: false)
let associationKey: SQLAssociationKey
if let key {
associationKey = .fixedSingular(key)
} else {
associationKey = .inflected(destinationTable)
}
_sqlAssociation = _SQLAssociation(
key: associationKey,
condition: .foreignKey(foreignKeyCondition),
relation: destinationRelation,
cardinality: .toOne)
}
}
extension HasOneAssociation: AssociationToOne {
public typealias OriginRowDecoder = Origin
public typealias RowDecoder = Destination
}
@@ -0,0 +1,47 @@
/// The `HasOneThroughAssociation` sets up a one-to-one connection with
/// another record. This association indicates that the declaring record can be
/// matched with one instance of another record by proceeding through a third
/// record.
///
/// For example, if each book belongs to a library, and each library has
/// one address, then one knows where the book should be returned to:
///
/// ```swift
/// struct Address: TableRecord { }
///
/// struct Library: TableRecord {
/// static let address = hasOne(Address.self)
/// }
///
/// struct Book: TableRecord {
/// static let library = belongsTo(Library.self)
/// static let returnAddress = hasOne(Address.self,
/// through: library,
/// using: Library.address,
/// key: "returnAddress")
/// }
/// ```
///
/// As in the example above, `HasOneThroughAssociation` is always built from
/// two other associations. Those associations can be any association that
/// declares a to-one connection (``AssociationToOne``).
public struct HasOneThroughAssociation<Origin, Destination> {
public var _sqlAssociation: _SQLAssociation
init<Pivot, Target>(
through pivot: Pivot,
using target: Target)
where Pivot: AssociationToOne,
Target: AssociationToOne,
Pivot.OriginRowDecoder == Origin,
Pivot.RowDecoder == Target.OriginRowDecoder,
Target.RowDecoder == Destination
{
_sqlAssociation = target._sqlAssociation.through(pivot._sqlAssociation)
}
}
extension HasOneThroughAssociation: AssociationToOne {
public typealias OriginRowDecoder = Origin
public typealias RowDecoder = Destination
}
@@ -0,0 +1,22 @@
/// The `JoinAssociation` joins common table expression to regular
/// tables or other common table expressions.
public struct JoinAssociation<Origin, Destination> {
public var _sqlAssociation: _SQLAssociation
/// Creates a `JoinAssociation` whose key is the table name of the relation.
init(
to relation: SQLRelation,
condition: SQLAssociationCondition)
{
_sqlAssociation = _SQLAssociation(
key: .inflected(relation.source.tableName),
condition: condition,
relation: relation,
cardinality: .toOne)
}
}
extension JoinAssociation: AssociationToOne {
public typealias OriginRowDecoder = Origin
public typealias RowDecoder = Destination
}
@@ -0,0 +1,482 @@
/// A [common table expression](https://sqlite.org/lang_with.html) that can be
/// used with the GRDB query interface.
public struct CommonTableExpression<RowDecoder> {
/// The table name of the common table expression.
///
/// For example:
///
/// // WITH answer AS (SELECT 42) ...
/// let answer = CommonTableExpression(
/// named: "answer",
/// sql: "SELECT 42")
/// answer.tableName // "answer"
public var tableName: String {
cte.tableName
}
var cte: SQLCTE
/// Creates a common table expression from a request.
///
/// For example:
///
/// // WITH p AS (SELECT * FROM player) ...
/// let p = CommonTableExpression(
/// named: "p",
/// request: Player.all(),
/// type: Void.self)
///
/// // WITH p AS (SELECT * FROM player) ...
/// let p = CommonTableExpression(
/// named: "p",
/// request: SQLRequest<Player>(sql: "SELECT * FROM player"),
/// type: Void.self)
///
/// - parameter recursive: Whether this common table expression needs a
/// `WITH RECURSIVE` sql clause.
/// - parameter tableName: The table name of the common table expression.
/// - parameter columns: The columns of the common table expression. If nil,
/// the columns are the columns of the request.
/// - parameter request: A request.
private init(
recursive: Bool = false,
named tableName: String,
columns: [String]? = nil,
request: some SQLSubqueryable,
type: RowDecoder.Type)
{
self.cte = SQLCTE(
tableName: tableName,
columns: columns,
sqlSubquery: request.sqlSubquery,
isRecursive: recursive)
}
}
extension CommonTableExpression {
/// Creates a common table expression from a request.
///
/// For example:
///
/// // WITH p AS (SELECT * FROM player) ...
/// let p = CommonTableExpression<Void>(
/// named: "p",
/// request: Player.all())
///
/// // WITH p AS (SELECT * FROM player) ...
/// let p = CommonTableExpression<Void>(
/// named: "p",
/// request: SQLRequest<Player>(sql: "SELECT * FROM player"))
///
/// - parameter recursive: Whether this common table expression needs a
/// `WITH RECURSIVE` sql clause.
/// - parameter tableName: The table name of the common table expression.
/// - parameter columns: The columns of the common table expression. If nil,
/// the columns are the columns of the request.
/// - parameter request: A request.
public init(
recursive: Bool = false,
named tableName: String,
columns: [String]? = nil,
request: some SQLSubqueryable)
{
self.init(
recursive: recursive,
named: tableName,
columns: columns,
request: request,
type: RowDecoder.self)
}
/// Creates a common table expression from an SQL string and
/// optional arguments.
///
/// For example:
///
/// // WITH p AS (SELECT * FROM player WHERE name = 'O''Brien') ...
/// let p = CommonTableExpression<Void>(
/// named: "p",
/// sql: "SELECT * FROM player WHERE name = ?",
/// arguments: ["O'Brien"])
///
/// - parameter recursive: Whether this common table expression needs a
/// `WITH RECURSIVE` sql clause.
/// - parameter tableName: The table name of the common table expression.
/// - parameter columns: The columns of the common table expression. If nil,
/// the columns are the columns of the request.
/// - parameter sql: An SQL string.
/// - parameter arguments: Statement arguments.
public init(
recursive: Bool = false,
named tableName: String,
columns: [String]? = nil,
sql: String,
arguments: StatementArguments = StatementArguments())
{
self.init(
recursive: recursive,
named: tableName,
columns: columns,
request: SQLRequest(sql: sql, arguments: arguments),
type: RowDecoder.self)
}
/// Creates a common table expression from an SQL *literal*.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// // WITH p AS (SELECT * FROM player WHERE name = 'O''Brien') ...
/// let name = "O'Brien"
/// let p = CommonTableExpression<Void>(
/// named: "p",
/// literal: "SELECT * FROM player WHERE name = \(name)")
///
/// - parameter recursive: Whether this common table expression needs a
/// `WITH RECURSIVE` sql clause.
/// - parameter tableName: The table name of the common table expression.
/// - parameter columns: The columns of the common table expression. If nil,
/// the columns are the columns of the request.
/// - parameter sqlLiteral: An ``SQL`` literal.
public init(
recursive: Bool = false,
named tableName: String,
columns: [String]? = nil,
literal sqlLiteral: SQL)
{
self.init(
recursive: recursive,
named: tableName,
columns: columns,
request: SQLRequest(literal: sqlLiteral),
type: RowDecoder.self)
}
}
extension CommonTableExpression<Row> {
/// Creates a common table expression from a request.
///
/// For example:
///
/// // WITH p AS (SELECT * FROM player) ...
/// let p = CommonTableExpression(
/// named: "p",
/// request: Player.all())
///
/// // WITH p AS (SELECT * FROM player) ...
/// let p = CommonTableExpression(
/// named: "p",
/// request: SQLRequest<Player>(sql: "SELECT * FROM player"))
///
/// - parameter recursive: Whether this common table expression needs a
/// `WITH RECURSIVE` sql clause.
/// - parameter tableName: The table name of the common table expression.
/// - parameter columns: The columns of the common table expression. If nil,
/// the columns are the columns of the request.
/// - parameter request: A request.
public init(
recursive: Bool = false,
named tableName: String,
columns: [String]? = nil,
request: some SQLSubqueryable)
{
self.init(
recursive: recursive,
named: tableName,
columns: columns,
request: request,
type: Row.self)
}
/// Creates a common table expression from an SQL string and
/// optional arguments.
///
/// For example:
///
/// // WITH p AS (SELECT * FROM player WHERE name = 'O''Brien') ...
/// let p = CommonTableExpression(
/// named: "p",
/// sql: "SELECT * FROM player WHERE name = ?",
/// arguments: ["O'Brien"])
///
/// - parameter recursive: Whether this common table expression needs a
/// `WITH RECURSIVE` sql clause.
/// - parameter tableName: The table name of the common table expression.
/// - parameter columns: The columns of the common table expression. If nil,
/// the columns are the columns of the request.
/// - parameter sql: An SQL string.
/// - parameter arguments: Statement arguments.
public init(
recursive: Bool = false,
named tableName: String,
columns: [String]? = nil,
sql: String,
arguments: StatementArguments = StatementArguments())
{
self.init(
recursive: recursive,
named: tableName,
columns: columns,
request: SQLRequest(sql: sql, arguments: arguments),
type: Row.self)
}
/// Creates a common table expression from an SQL *literal*.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// // WITH p AS (SELECT * FROM player WHERE name = 'O''Brien') ...
/// let name = "O'Brien"
/// let p = CommonTableExpression(
/// named: "p",
/// literal: "SELECT * FROM player WHERE name = \(name)")
///
/// - parameter recursive: Whether this common table expression needs a
/// `WITH RECURSIVE` sql clause.
/// - parameter tableName: The table name of the common table expression.
/// - parameter columns: The columns of the common table expression. If nil,
/// the columns are the columns of the request.
/// - parameter sqlLiteral: An ``SQL`` literal.
public init(
recursive: Bool = false,
named tableName: String,
columns: [String]? = nil,
literal sqlLiteral: SQL)
{
self.init(
recursive: recursive,
named: tableName,
columns: columns,
request: SQLRequest(literal: sqlLiteral),
type: Row.self)
}
}
extension CommonTableExpression {
var relationForAll: SQLRelation {
.all(fromTable: tableName)
}
/// Creates a request for all rows of the common table expression.
///
/// You can fetch from this request:
///
/// // WITH answer AS (SELECT 42 AS value)
/// // SELECT * FROM answer
/// struct Answer: Decodable, FetchableRecord {
/// var value: Int
/// }
/// let cte = CommonTableExpression<Answer>(
/// named: "answer",
/// sql: "SELECT 42 AS value")
/// let answer = try cte.all().with(cte).fetchOne(db)!
/// print(answer.value) // prints 42
///
/// You can embed this request as a subquery:
///
/// // WITH answer AS (SELECT 42 AS value)
/// // SELECT * FROM player
/// // WHERE score = (SELECT * FROM answer)
/// let answer = CommonTableExpression(
/// named: "answer",
/// sql: "SELECT 42 AS value")
/// let players = try Player
/// .filter(Column("score") == answer.all())
/// .with(answer)
/// .fetchAll(db)
public func all() -> QueryInterfaceRequest<RowDecoder> {
QueryInterfaceRequest(relation: relationForAll)
}
/// An SQL expression that checks the inclusion of an expression in a
/// common table expression.
///
/// let playerNameCTE = CommonTableExpression(
/// named: "playerName",
/// request: Player.select(Column("name"))
///
/// // name IN playerName
/// playerNameCTE.contains(Column("name"))
public func contains(_ element: some SQLExpressible) -> SQLExpression {
SQLCollection.table(tableName).contains(element.sqlExpression)
}
}
/// A low-level common table expression
struct SQLCTE {
/// The table name of the common table expression.
var tableName: String
/// The columns of the common table expression.
///
/// When nil, the CTE selects the columns of the request:
///
/// -- Columns a, b
/// WITH t AS (SELECT 1 AS a, 2 AS b) ...
///
/// When not nil, `columns` provides the columns of the CTE:
///
/// -- Column id
/// WITH t(id) AS (SELECT 1) ...
/// ~~
var columns: [String]?
/// The common table expression subquery.
///
/// WITH t AS (SELECT ...)
/// ~~~~~~~~~~
var sqlSubquery: SQLSubquery
/// Whether this common table expression needs a `WITH RECURSIVE`
/// sql clause.
var isRecursive: Bool
/// The number of columns in the common table expression.
func columnCount(_ db: Database) throws -> Int {
if let columns {
// No need to hit the database
return columns.count
}
do {
return try sqlSubquery.columnCount(db)
} catch let error as DatabaseError where error.resultCode == .SQLITE_ERROR {
// Maybe the CTE refers to other CTEs: https://github.com/groue/GRDB.swift/issues/1275
// We can't modify the CTE request by creating or extending the
// WITH clause with other CTEs, because we'd need to parse SQL.
// So let's rewrite the error message, and guide the user towards
// a more precise CTE definition:
let message = [
[
"""
Can't compute the number of columns in the \
\(String(reflecting: tableName)) common table expression
""",
error.message,
].compactMap { $0 }.joined(separator: ": "),
"""
Check the syntax of the SQL definition, or provide the \
explicit list of selected columns with the `columns` parameter \
in the CommonTableExpression initializer.
""",
].joined(separator: ". ")
throw DatabaseError(
resultCode: error.extendedResultCode,
message: message,
sql: error.sql,
arguments: error.arguments,
publicStatementArguments: error.publicStatementArguments)
}
}
}
extension CommonTableExpression {
/// Creates an association to a common table expression that you can join
/// or include in another request.
///
/// The key of the returned association is the table name of the common
/// table expression.
///
/// - parameter cte: A common table expression.
/// - parameter condition: A function that returns the joining clause.
/// - parameter left: A `TableAlias` for the left table.
/// - parameter right: A `TableAlias` for the right table.
/// - returns: An association to the common table expression.
public func association<Destination>(
to cte: CommonTableExpression<Destination>,
on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible)
-> JoinAssociation<RowDecoder, Destination>
{
JoinAssociation(
to: cte.relationForAll,
condition: .expression { condition($0, $1).sqlExpression })
}
/// Creates an association to a common table expression that you can join
/// or include in another request.
///
/// The key of the returned association is the table name of the common
/// table expression.
///
/// - parameter cte: A common table expression.
/// - returns: An association to the common table expression.
public func association<Destination>(
to cte: CommonTableExpression<Destination>)
-> JoinAssociation<RowDecoder, Destination>
{
JoinAssociation(to: cte.relationForAll, condition: .none)
}
/// Creates an association to a table record that you can join
/// or include in another request.
///
/// The key of the returned association is the table name of `Destination`.
///
/// - parameter destination: The record type at the other side of
/// the association.
/// - parameter condition: A function that returns the joining clause.
/// - parameter left: A `TableAlias` for the left table.
/// - parameter right: A `TableAlias` for the right table.
/// - returns: An association to the common table expression.
public func association<Destination>(
to destination: Destination.Type,
on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible)
-> JoinAssociation<RowDecoder, Destination>
where Destination: TableRecord
{
JoinAssociation(
to: Destination.relationForAll,
condition: .expression { condition($0, $1).sqlExpression })
}
/// Creates an association to a table record that you can join
/// or include in another request.
///
/// The key of the returned association is the table name of `Destination`.
///
/// - parameter destination: The record type at the other side of
/// the association.
/// - returns: An association to the common table expression.
public func association<Destination>(
to destination: Destination.Type)
-> JoinAssociation<RowDecoder, Destination>
where Destination: TableRecord
{
JoinAssociation(to: Destination.relationForAll, condition: .none)
}
/// Creates an association to a table that you can join
/// or include in another request.
///
/// The key of the returned association is the table name of `Destination`.
///
/// - parameter destination: The table at the other side of the association.
/// - parameter condition: A function that returns the joining clause.
/// - parameter left: A `TableAlias` for the left table.
/// - parameter right: A `TableAlias` for the right table.
/// - returns: An association to the common table expression.
public func association<Destination>(
to destination: Table<Destination>,
on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible)
-> JoinAssociation<RowDecoder, Destination>
{
JoinAssociation(
to: destination.relationForAll,
condition: .expression { condition($0, $1).sqlExpression })
}
/// Creates an association to a table that you can join
/// or include in another request.
///
/// The key of the returned association is the table name of `Destination`.
///
/// - parameter destination: The table at the other side of the association.
/// - returns: An association to the common table expression.
public func association<Destination>(
to destination: Table<Destination>)
-> JoinAssociation<RowDecoder, Destination>
{
JoinAssociation(to: destination.relationForAll, condition: .none)
}
}
@@ -0,0 +1,126 @@
/// A type that represents a column in a database table.
///
/// ## Topics
///
/// ### Standard Columns
///
/// - ``rowID``
///
/// ### Deriving SQL Expressions
///
/// - ``detached``
/// - ``match(_:)-727nk``
/// - ``match(_:)-1vvo8``
///
/// ### Creating Column Assignments
///
/// - ``noOverwrite``
/// - ``set(to:)``
public protocol ColumnExpression: SQLSpecificExpressible {
/// The column name.
///
/// The column name is never qualified with a table name.
///
/// For example, the name of a column can be `"score"`, but
/// not `"player.score"`.
var name: String { get }
}
extension ColumnExpression {
/// Returns an SQL column.
public var sqlExpression: SQLExpression {
.column(name)
}
/// An SQL expression that refers to an aliased column
/// (`expression AS alias`).
///
/// Once detached, a column is never qualified with any table name in the
/// SQL generated by the query interface.
///
/// For example, see how `Column("total").detached` makes it possible to
/// sort this query, when a raw `Column("total")` could not:
///
/// ```swift
/// // SELECT player.*,
/// // (player.score + player.bonus) AS total,
/// // team.*
/// // FROM player
/// // JOIN team ON team.id = player.teamID
/// // ORDER BY total, player.name
/// // ~~~~~
/// let request = Player
/// .annotated(with: (Column("score") + Column("bonus")).forKey("total"))
/// .including(required: Player.team)
/// .order(Column("total").detached, Column("name"))
/// ```
public var detached: SQLExpression {
SQL(sql: name.quotedDatabaseIdentifier).sqlExpression
}
}
extension ColumnExpression where Self == Column {
/// The hidden rowID column.
public static var rowID: Self { Column.rowID }
}
/// A column in a database table.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord {
/// var score: Int
/// }
///
/// let maximumScore = try dbQueue.read { db in
/// // SELECT MAX(score) FROM player
/// try Player
/// .select(max(Column("score")), as: Int.self)
/// .fetchOne(db)
/// }
/// ```
///
/// ## Topics
///
/// ### Standard Columns
///
/// - ``rowID-3bn70``
/// - ``rank``
///
/// ### Creating A Column
///
/// - ``init(_:)-5grmu``
/// - ``init(_:)-7xc4z``
public struct Column: Sendable {
/// The hidden rowID column.
public static let rowID = Column("rowid")
public var name: String
/// Creates a `Column` given its name.
///
/// The name should be unqualified, such as `"score"`. Qualified name such
/// as `"player.score"` are unsupported.
public init(_ name: String) {
self.name = name
}
/// Creates a `Column` given a `CodingKey`.
public init(_ codingKey: some CodingKey) {
self.name = codingKey.stringValue
}
}
extension Column: ColumnExpression { }
/// Support for column enums:
///
/// struct Player {
/// enum Columns: String, ColumnExpression {
/// case id, name, score
/// }
/// }
extension ColumnExpression where Self: RawRepresentable, Self.RawValue == String {
public var name: String { rawValue }
}
@@ -0,0 +1,50 @@
/// DatabasePromise represents a value that can only be resolved when a
/// database connection is available.
///
/// This type is important for the query interface, which lets the user define
/// requests without any database context.
///
/// For example, consider those two requests:
///
/// let playerRequest = Player.filter(key: 1)
/// let countryRequest = Country.filter(key: "FR")
///
/// Both need a database connection in order to introspect the database schema,
/// find the primary key of both table, and generate the correct SQL:
///
/// try dbQueue.read { db in
/// // SELECT * FROM player WHERE id = 1
/// let player = try playerRequest.fetchOne(db)
/// // SELECT * FROM country WHERE code = 'FR'
/// let country = try countryRequest.fetchOne(db)
/// }
///
/// Such late computations are backed by DatabasePromise. In our example,
/// see SQLRelation.filterPromise.
struct DatabasePromise<T> {
/// Returns the resolved value.
let resolve: (Database) throws -> T
/// Creates a promise that resolves to a value.
init(value: T) {
self.resolve = { _ in value }
}
/// Creates a promise from a closure.
init(_ resolve: @escaping (Database) throws -> T) {
self.resolve = resolve
}
/// Returns a promise whose value is transformed by the given closure.
func map<U>(_ transform: @escaping (T) throws -> U) -> DatabasePromise<U> {
DatabasePromise<U> { db in
try transform(resolve(db))
}
}
}
extension DatabasePromise: CustomStringConvertible {
var description: String {
"DatabasePromise<\(T.self)>"
}
}
@@ -0,0 +1,311 @@
// MARK: - _SQLAssociation
/// An SQL association is a non-empty chain of steps which starts at the
/// "pivot" and ends on the "destination":
///
/// // SELECT origin.*, destination.*
/// // FROM origin
/// // JOIN pivot ON ...
/// // JOIN ...
/// // JOIN ...
/// // JOIN destination ON ...
/// Origin.including(required: association)
///
/// For direct associations such as BelongTo or HasMany, the chain contains a
/// single element, the "destination", without intermediate step:
///
/// // "Origin" belongsTo "destination":
/// // SELECT origin.*, destination.*
/// // FROM origin
/// // JOIN destination ON destination.originId = origin.id
/// let association = Origin.belongsTo(Destination.self)
/// Origin.including(required: association)
///
/// Indirect associations such as HasManyThrough have one or several
/// intermediate steps:
///
/// // "Origin" has many "destination" through "pivot":
/// // SELECT origin.*, destination.*
/// // FROM origin
/// // JOIN pivot ON pivot.originId = origin.id
/// // JOIN destination ON destination.id = pivot.destinationId
/// let association = Origin.hasMany(
/// Destination.self,
/// through: Origin.hasMany(Pivot.self),
/// via: Pivot.belongsTo(Destination.self))
/// Origin.including(required: association)
///
/// // "Origin" has many "destination" through "pivot1" and "pivot2":
/// // SELECT origin.*, destination.*
/// // FROM origin
/// // JOIN pivot1 ON pivot1.originId = origin.id
/// // JOIN pivot2 ON pivot2.pivot1Id = pivot1.id
/// // JOIN destination ON destination.id = pivot.destinationId
/// let association = Origin.hasMany(
/// Destination.self,
/// through: Origin.hasMany(Pivot1.self),
/// via: Pivot1.hasMany(
/// Destination.self,
/// through: Pivot1.hasMany(Pivot2.self),
/// via: Pivot2.belongsTo(Destination.self)))
/// Origin.including(required: association)
public struct _SQLAssociation {
// All steps, from pivot to destination. Never empty.
private(set) var steps: [SQLAssociationStep]
var keyPath: [String] { steps.map(\.keyName) }
var destination: SQLAssociationStep {
get { steps[steps.count - 1] }
set { steps[steps.count - 1] = newValue }
}
var pivot: SQLAssociationStep {
get { steps[0] }
set { steps[0] = newValue }
}
init(steps: [SQLAssociationStep]) {
assert(!steps.isEmpty)
self.steps = steps
}
init(
key: SQLAssociationKey,
condition: SQLAssociationCondition,
relation: SQLRelation,
cardinality: SQLAssociationCardinality)
{
let step = SQLAssociationStep(
key: key,
condition: condition,
relation: relation,
cardinality: cardinality)
self.init(steps: [step])
}
/// Changes the destination key
func forDestinationKey(_ key: SQLAssociationKey) -> Self {
with {
$0.destination.key = key
}
}
/// Returns a new association
func through(_ other: _SQLAssociation) -> Self {
_SQLAssociation(steps: other.steps + steps)
}
/// Returns the destination of the association, reversing the association
/// up to the pivot.
///
/// This method feeds `TableRecord.request(for:)`, and allows
/// `including(all:)` to prefetch associated records.
func destinationRelation() -> SQLRelation {
if steps.count == 1 {
return destination.relation
}
// This is an indirect join from origin to destination, through
// some intermediate steps:
//
// SELECT destination.*
// FROM destination
// JOIN pivot ON (pivot.destinationId = destination.id) AND (pivot.originId = 1)
//
// let association = Origin.hasMany(
// Destination.self,
// through: Origin.hasMany(Pivot.self),
// via: Pivot.belongsTo(Destination.self))
// Origin(id: 1).request(for: association)
let reversedSteps = zip(steps, steps.dropFirst())
.map { (step, nextStep) in
// Intermediate steps are not selected, and including(all:)
// children can't impact the destination relation:
let relation = step.relation
.selectOnly([])
.removingPrefetchedAssociations()
// Don't interfere with user-defined keys that could be added later
let key = step.key.with {
$0.baseName = "grdb_\($0.baseName)"
}
return SQLAssociationStep(
key: key,
condition: nextStep.condition.reversed(to: step.relation.source.tableName),
relation: relation,
cardinality: .toOne)
}
.reversed()
let reversedAssociation = _SQLAssociation(steps: Array(reversedSteps))
return destination.relation.appendingChild(for: reversedAssociation, kind: .oneRequired)
}
}
extension _SQLAssociation: Refinable { }
struct SQLAssociationStep: Refinable {
var key: SQLAssociationKey
var condition: SQLAssociationCondition
var relation: SQLRelation
var cardinality: SQLAssociationCardinality
var keyName: String { key.name(singular: cardinality.isSingular) }
}
enum SQLAssociationCardinality {
case toOne
case toMany
var isSingular: Bool {
switch self {
case .toOne:
return true
case .toMany:
return false
}
}
}
// MARK: - SQLAssociationKey
/// Associations are meant to be consumed, most often into Decodable records.
///
/// Those records have singular or plural property names, and we want
/// associations to be able to fill those singular or plural names
/// automatically, so that the user does not have to perform explicit
/// decoding configuration.
///
/// Those plural or singular names are not decided when the association is
/// defined. For example, the Author.books association, which looks plural, may
/// actually generate "book" or "books" depending on the context:
///
/// struct Author: TableRecord {
/// static let books = hasMany(Book.self)
/// }
/// struct Book: TableRecord {
/// }
///
/// // "books"
/// struct AuthorInfo: FetchableRecord, Decodable {
/// var author: Author
/// var books: [Book]
/// }
/// let request = Author.including(all: Author.books)
/// let authorInfos = try AuthorInfo.fetchAll(db, request)
///
/// "book"
/// struct AuthorInfo: FetchableRecord, Decodable {
/// var author: Author
/// var book: Book
/// }
/// let request = Author.including(required: Author.books)
/// let authorInfos = try AuthorInfo.fetchAll(db, request)
///
/// "bookCount"
/// struct AuthorInfo: FetchableRecord, Decodable {
/// var author: Author
/// var bookCount: Int
/// }
/// let request = Author.annotated(with: Author.books.count)
/// let authorInfos = try AuthorInfo.fetchAll(db, request)
///
/// The SQLAssociationKey type aims at providing the necessary support for
/// those various inflections.
enum SQLAssociationKey: Refinable {
/// A key that is inflected in singular and plural contexts.
///
/// For example:
///
/// struct Author: TableRecord {
/// static let databaseTableName = "authors"
/// }
/// struct Book: TableRecord {
/// let author = belongsTo(Author.self)
/// }
///
/// let request = Book.including(required: Book.author)
/// let row = try Row.fetchOne(db, request)!
/// row.scopes["author"] // singularized "authors" table name
case inflected(String)
/// A key that is inflected in plural contexts, but stricly honors
/// user-provided name in singular contexts.
///
/// For example:
///
/// struct Country: TableRecord {
/// let demographics = hasOne(Demographics.self, key: "demographics")
/// }
///
/// let request = Country.including(required: Country.demographics)
/// let row = try Row.fetchOne(db, request)!
/// row.scopes["demographics"] // not singularized
case fixedSingular(String)
/// A key that is inflected in singular contexts, but stricly honors
/// user-provided name in plural contexts.
/// See .inflected and .fixedSingular for some context.
case fixedPlural(String)
/// A key that is never inflected.
case fixed(String)
var baseName: String {
get {
switch self {
case let .inflected(name),
let .fixedSingular(name),
let .fixedPlural(name),
let .fixed(name):
return name
}
}
set {
switch self {
case .inflected:
self = .inflected(newValue)
case .fixedSingular:
self = .fixedSingular(newValue)
case .fixedPlural:
self = .fixedPlural(newValue)
case .fixed:
self = .fixed(newValue)
}
}
}
func name(singular: Bool) -> String {
if singular {
return singularizedName
} else {
return pluralizedName
}
}
var pluralizedName: String {
switch self {
case .inflected(let name):
return name.pluralized
case .fixedSingular(let name):
return name.pluralized
case .fixedPlural(let name):
return name
case .fixed(let name):
return name
}
}
var singularizedName: String {
switch self {
case .inflected(let name):
return name.singularized
case .fixedSingular(let name):
return name
case .fixedPlural(let name):
return name.singularized
case .fixed(let name):
return name
}
}
}
@@ -0,0 +1,119 @@
/// 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)
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,177 @@
/// SQLForeignKeyRequest looks for the foreign keys associations need to
/// join tables.
///
/// Columns mapping come from foreign keys, when they exist in the
/// database schema.
///
/// When the schema does not define any foreign key, we can still infer complete
/// mapping from partial information and primary keys.
struct SQLForeignKeyRequest {
let originTable: String
let destinationTable: String
let originColumns: [String]?
let destinationColumns: [String]?
init(originTable: String, destinationTable: String, foreignKey: ForeignKey?) {
self.originTable = originTable
self.destinationTable = destinationTable
self.originColumns = foreignKey?.originColumns
self.destinationColumns = foreignKey?.destinationColumns
}
/// The (origin, destination) column pairs that join a left table to a right table.
func fetchForeignKeyMapping(_ db: Database) throws -> ForeignKeyMapping {
if let originColumns, let destinationColumns {
// Total information: no need to query the database schema.
GRDBPrecondition(originColumns.count == destinationColumns.count, "Number of columns don't match")
let mapping = zip(originColumns, destinationColumns).map {
(origin: $0, destination: $1)
}
return mapping
}
// Incomplete information: let's look for schema foreign keys
//
// But maybe the tables are views. In this case, don't throw
// "no such table" error, because this is confusing for the user,
// as discovered in <https://github.com/groue/GRDB.swift/discussions/1481>.
// Instead, we'll crash with a clear message.
guard let originType = try tableType(db, for: originTable) else {
throw DatabaseError.noSuchTable(originTable)
}
if originType.isView {
if originColumns == nil {
fatalError("""
Could not infer foreign key from '\(originTable)' \
to '\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}
} else {
let foreignKeys = try db.foreignKeys(on: originTable).filter { foreignKey in
if destinationTable.lowercased() != foreignKey.destinationTable.lowercased() {
return false
}
if let originColumns {
let originColumns = Set(originColumns.lazy.map { $0.lowercased() })
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.origin.lowercased() })
if originColumns != foreignKeyColumns {
return false
}
}
if let destinationColumns {
// TODO: test
let destinationColumns = Set(destinationColumns.lazy.map { $0.lowercased() })
let foreignKeyColumns = Set(foreignKey.mapping.lazy.map { $0.destination.lowercased() })
if destinationColumns != foreignKeyColumns {
return false
}
}
return true
}
// Matching foreign key(s) found
if let foreignKey = foreignKeys.first {
if foreignKeys.count == 1 {
// Non-ambiguous
return foreignKey.mapping
} else {
// Ambiguous: can't choose
fatalError("""
Ambiguous foreign key from '\(originTable)' to \
'\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}
}
}
// No matching foreign key found: use the destination primary key
if let originColumns {
guard let destinationType = try tableType(db, for: destinationTable) else {
throw DatabaseError.noSuchTable(destinationTable)
}
if destinationType.isView {
fatalError("""
Could not infer foreign key from '\(originTable)' \
to '\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition, \
with both origin and destination columns.
""")
}
let destinationColumns = try db.primaryKey(destinationTable).columns
if originColumns.count == destinationColumns.count {
let mapping = zip(originColumns, destinationColumns).map {
(origin: $0, destination: $1)
}
return mapping
}
}
fatalError("""
Could not infer foreign key from '\(originTable)' to \
'\(destinationTable)'. To fix this error, provide an \
explicit `ForeignKey` in the association definition.
""")
}
private struct TableType {
var isView: Bool
}
private func tableType(_ db: Database, for name: String) throws -> TableType? {
for schemaID in try db.schemaIdentifiers() {
if try db.schema(schemaID).containsObjectNamed(name, ofType: .table) {
return TableType(isView: false)
}
if try db.schema(schemaID).containsObjectNamed(name, ofType: .view) {
return TableType(isView: true)
}
}
return nil
}
}
// Foreign key columns mapping
typealias ForeignKeyMapping = [(origin: String, destination: String)]
// Join columns mapping
typealias JoinMapping = [(left: String, right: String)]
extension ForeignKeyMapping {
/// Orient the foreign key mapping for a SQL join.
///
/// - parameter originIsLeft: Whether the table at the origin of a
/// foreign key is on the left of a JOIN clause.
///
/// For example, the two requests below use the same
/// `ForeignKeyMapping` from `book.authorID` (origin of the foreign key)
/// to `author.id` (destination).
///
/// In the first request, the book origin is on the left of the
/// join clause:
///
/// // SELECT book.*, author.*
/// // FROM book
/// // JOIN author ON author.id = book.authorID
/// Book.including(required: Book.author)
///
/// In the second request, the book origin is on the right of the
/// join clause:
///
/// // SELECT author.*, book.*
/// // FROM author
/// // JOIN book ON book.authorID = author.id
/// Author.including(required: Author.books)
func joinMapping(originIsLeft: Bool) -> JoinMapping {
if originIsLeft {
return map { (left: $0.origin, right: $0.destination) }
} else {
return map { (left: $0.destination, right: $0.origin) }
}
}
}
@@ -0,0 +1,535 @@
/// The `ABS` SQL function.
///
/// For example:
///
/// ```swift
/// // ABS(amount)
/// abs(Column("amount"))
/// ```
public func abs(_ value: some SQLSpecificExpressible) -> SQLExpression {
.function("ABS", [value.sqlExpression])
}
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// The `AVG` SQL function.
///
/// For example:
///
/// ```swift
/// // AVG(length)
/// average(Column("length"))
/// ```
public func average(
_ value: some SQLSpecificExpressible,
filter: (any SQLSpecificExpressible)? = nil)
-> SQLExpression {
.aggregateFunction("AVG", [value.sqlExpression], filter: filter?.sqlExpression)
}
#else
/// The `AVG` SQL function.
///
/// For example:
///
/// ```swift
/// // AVG(length) FILTER (WHERE length > 0)
/// average(Column("length"), filter: Column("length") > 0)
/// ```
@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+
public func average(
_ value: some SQLSpecificExpressible,
filter: some SQLSpecificExpressible)
-> SQLExpression {
.aggregateFunction(
"AVG", [value.sqlExpression],
filter: filter.sqlExpression)
}
/// The `AVG` SQL function.
///
/// For example:
///
/// ```swift
/// // AVG(length)
/// average(Column("length"))
/// ```
public func average(_ value: some SQLSpecificExpressible) -> SQLExpression {
.aggregateFunction("AVG", [value.sqlExpression])
}
#endif
/// The `CAST` SQL function.
///
/// For example:
///
/// ```swift
/// // CAST(value AS REAL)
/// cast(Column("value"), as: .real)
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_expr.html#castexpr>
public func cast(_ expression: some SQLSpecificExpressible, as storageClass: Database.StorageClass) -> SQLExpression {
.cast(expression.sqlExpression, as: storageClass)
}
/// The `COUNT` SQL function.
///
/// For example:
///
/// ```swift
/// // COUNT(email)
/// count(Column("email"))
/// ```
public func count(_ counted: some SQLSpecificExpressible) -> SQLExpression {
.count(counted.sqlExpression)
}
/// The `COUNT(DISTINCT)` SQL function.
///
/// For example:
///
/// ```swift
/// // COUNT(DISTINCT email)
/// count(distinct: Column("email"))
/// ```
public func count(distinct value: some SQLSpecificExpressible) -> SQLExpression {
.countDistinct(value.sqlExpression)
}
extension SQLSpecificExpressible {
/// The `IFNULL` SQL function.
///
/// For example:
///
/// ```swift
/// // IFNULL(name, 'Anonymous')
/// Column("name") ?? "Anonymous"
/// ```
public static func ?? (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.function("IFNULL", [lhs.sqlExpression, rhs.sqlExpression])
}
}
/// The `LENGTH` SQL function.
///
/// For example:
///
/// ```swift
/// // LENGTH(name)
/// length(Column("name"))
/// ```
public func length(_ value: some SQLSpecificExpressible) -> SQLExpression {
.function("LENGTH", [value.sqlExpression])
}
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// The `MAX` SQL function.
///
/// For example:
///
/// ```swift
/// // MAX(score)
/// max(Column("score"))
/// ```
public func max(
_ value: some SQLSpecificExpressible,
filter: (any SQLSpecificExpressible)? = nil)
-> SQLExpression {
.aggregateFunction("MAX", [value.sqlExpression], filter: filter?.sqlExpression)
}
#else
/// The `MAX` SQL function.
///
/// For example:
///
/// ```swift
/// // MAX(score) FILTER (WHERE score < 0)
/// max(Column("score"), filter: Column("score") < 0)
/// ```
@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+
public func max(
_ value: some SQLSpecificExpressible,
filter: some SQLSpecificExpressible)
-> SQLExpression {
.aggregateFunction("MAX", [value.sqlExpression], filter: filter.sqlExpression)
}
/// The `MAX` SQL function.
///
/// For example:
///
/// ```swift
/// // MAX(score)
/// max(Column("score"))
/// ```
public func max(_ value: some SQLSpecificExpressible) -> SQLExpression {
.aggregateFunction("MAX", [value.sqlExpression])
}
#endif
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// The `MIN` SQL function.
///
/// For example:
///
/// ```swift
/// // MIN(score)
/// min(Column("score"))
/// ```
public func min(
_ value: some SQLSpecificExpressible,
filter: (any SQLSpecificExpressible)? = nil)
-> SQLExpression {
.aggregateFunction("MIN", [value.sqlExpression], filter: filter?.sqlExpression)
}
#else
/// The `MIN` SQL function.
///
/// For example:
///
/// ```swift
/// // MIN(score) FILTER (WHERE score > 0)
/// min(Column("score"), filter: Column("score") > 0)
/// ```
@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+
public func min(
_ value: some SQLSpecificExpressible,
filter: some SQLSpecificExpressible)
-> SQLExpression {
.aggregateFunction("MIN", [value.sqlExpression], filter: filter.sqlExpression)
}
/// The `MIN` SQL function.
///
/// For example:
///
/// ```swift
/// // MIN(score)
/// min(Column("score"))
/// ```
public func min(_ value: some SQLSpecificExpressible) -> SQLExpression {
.aggregateFunction("MIN", [value.sqlExpression])
}
#endif
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// The `SUM` SQL function.
///
/// For example:
///
/// ```swift
/// // SUM(amount)
/// sum(Column("amount"))
/// ```
///
/// See also ``total(_:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
public func sum(
_ value: some SQLSpecificExpressible,
orderBy ordering: (any SQLOrderingTerm)? = nil,
filter: (any SQLSpecificExpressible)? = nil)
-> SQLExpression
{
.aggregateFunction(
"SUM", [value.sqlExpression],
ordering: ordering?.sqlOrdering,
filter: filter?.sqlExpression)
}
#else
/// The `SUM` SQL function.
///
/// For example:
///
/// ```swift
/// // SUM(amount) FILTER (WHERE amount > 0)
/// sum(Column("amount"), filter: Column("amount") > 0)
/// ```
///
/// See also ``total(_:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+
public func sum(
_ value: some SQLSpecificExpressible,
filter: some SQLSpecificExpressible)
-> SQLExpression {
.aggregateFunction(
"SUM", [value.sqlExpression],
filter: filter.sqlExpression)
}
/// The `SUM` SQL function.
///
/// For example:
///
/// ```swift
/// // SUM(amount)
/// sum(Column("amount"))
/// ```
///
/// See also ``total(_:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
public func sum(_ value: some SQLSpecificExpressible) -> SQLExpression {
.aggregateFunction("SUM", [value.sqlExpression])
}
#endif
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// The `TOTAL` SQL function.
///
/// For example:
///
/// ```swift
/// // TOTAL(amount)
/// total(Column("amount"))
/// ```
///
/// See also ``sum(_:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
public func total(
_ value: some SQLSpecificExpressible,
orderBy ordering: (any SQLOrderingTerm)? = nil,
filter: (any SQLSpecificExpressible)? = nil)
-> SQLExpression
{
.aggregateFunction(
"TOTAL", [value.sqlExpression],
ordering: ordering?.sqlOrdering,
filter: filter?.sqlExpression)
}
#else
/// The `TOTAL` SQL function.
///
/// For example:
///
/// ```swift
/// // TOTAL(amount) FILTER (WHERE amount > 0)
/// total(Column("amount"), filter: Column("amount") > 0)
/// ```
///
/// See also ``total(_:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+
public func total(
_ value: some SQLSpecificExpressible,
filter: some SQLSpecificExpressible)
-> SQLExpression {
.aggregateFunction(
"TOTAL", [value.sqlExpression],
filter: filter.sqlExpression)
}
/// The `TOTAL` SQL function.
///
/// For example:
///
/// ```swift
/// // TOTAL(amount)
/// total(Column("amount"))
/// ```
///
/// See also ``sum(_:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_aggfunc.html#sumunc>.
public func total(_ value: some SQLSpecificExpressible) -> SQLExpression {
.aggregateFunction("TOTAL", [value.sqlExpression])
}
#endif
// MARK: - String functions
extension SQLSpecificExpressible {
/// An SQL expression that calls the Foundation
/// `String.capitalized` property.
///
/// For example:
///
/// ```swift
/// Column("name").capitalized
/// ```
public var capitalized: SQLExpression {
DatabaseFunction.capitalize(sqlExpression)
}
/// An SQL expression that calls the Swift
/// `String.lowercased()` method.
///
/// For example:
///
/// ```swift
/// Column("name").lowercased
/// ```
public var lowercased: SQLExpression {
DatabaseFunction.lowercase(sqlExpression)
}
/// An SQL expression that calls the Swift
/// `String.uppercased()` method.
///
/// For example:
///
/// ```swift
/// Column("name").uppercased
/// ```
public var uppercased: SQLExpression {
DatabaseFunction.uppercase(sqlExpression)
}
}
extension SQLSpecificExpressible {
/// An SQL expression that calls the Foundation
/// `String.localizedCapitalized` property.
///
/// For example:
///
/// ```swift
/// Column("name").localizedCapitalized
/// ```
public var localizedCapitalized: SQLExpression {
DatabaseFunction.localizedCapitalize(sqlExpression)
}
/// An SQL expression that calls the Foundation
/// `String.localizedLowercase` property.
///
/// For example:
///
/// ```swift
/// Column("name").localizedLowercased
/// ```
public var localizedLowercased: SQLExpression {
DatabaseFunction.localizedLowercase(sqlExpression)
}
/// An SQL expression that calls the Foundation
/// `String.localizedUppercase` property.
///
/// For example:
///
/// ```swift
/// Column("name").localizedUppercased
/// ```
public var localizedUppercased: SQLExpression {
DatabaseFunction.localizedUppercase(sqlExpression)
}
}
// MARK: - Date functions
/// A date modifier for SQLite date functions.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_datefunc.html>
public enum SQLDateModifier: SQLSpecificExpressible, Sendable {
/// Adds the specified amount of seconds
case second(Double)
/// Adds the specified amount of minutes
case minute(Int)
/// Adds the specified amount of hours
case hour(Int)
/// Adds the specified amount of days
case day(Int)
/// Adds the specified amount of months
case month(Int)
/// Adds the specified amount of years
case year(Int)
/// Shifts the date backwards to the beginning of the current day
case startOfDay
/// Shifts the date backwards to the beginning of the current month
case startOfMonth
/// Shifts the date backwards to the beginning of the current year
case startOfYear
/// See <https://www.sqlite.org/lang_datefunc.html>
case weekday(Int)
/// See <https://www.sqlite.org/lang_datefunc.html>
case unixEpoch
/// See <https://www.sqlite.org/lang_datefunc.html>
case localTime
/// See <https://www.sqlite.org/lang_datefunc.html>
case utc
public var sqlExpression: SQLExpression {
rawValue.sqlExpression
}
var rawValue: String {
switch self {
case let .day(value):
return "\(value) days"
case let .hour(value):
return "\(value) hours"
case let .minute(value):
return "\(value) minutes"
case let .second(value):
return "\(value) seconds"
case let .month(value):
return "\(value) months"
case let .year(value):
return "\(value) years"
case .startOfMonth:
return "start of month"
case .startOfYear:
return "start of year"
case .startOfDay:
return "start of day"
case let .weekday(value):
return "weekday \(value)"
case .unixEpoch:
return "unixepoch"
case .localTime:
return "localtime"
case .utc:
return "utc"
}
}
}
/// The `JULIANDAY` SQL function.
///
/// For example:
///
/// ```swift
/// // JULIANDAY(date)
/// julianDay(Column("date"))
///
/// // JULIANDAY(date, '1 days')
/// julianDay(Column("date"), .day(1))
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_datefunc.html>
public func julianDay(_ value: some SQLSpecificExpressible, _ modifiers: SQLDateModifier...) -> SQLExpression {
.function("JULIANDAY", [value.sqlExpression] + modifiers.map(\.sqlExpression))
}
// MARK: DATETIME(...)
/// The `DATETIME` SQL function.
///
/// For example:
///
/// ```swift
/// // DATETIME(date)
/// dateTime(Column("date"))
///
/// // DATETIME(date, '1 days')
/// dateTime(Column("date"), .day(1))
/// ```
///
/// Related SQLite documentation:<https://www.sqlite.org/lang_datefunc.html>
public func dateTime(_ value: some SQLSpecificExpressible, _ modifiers: SQLDateModifier...) -> SQLExpression {
.function("DATETIME", [value.sqlExpression] + modifiers.map(\.sqlExpression))
}
@@ -0,0 +1,525 @@
extension SQLSpecificExpressible {
// MARK: - Egality and Identity Operators (=, <>, IS, IS NOT)
/// Compares two SQL expressions.
///
/// For example:
///
/// ```swift
/// // name = 'Arthur'
/// Column("name") == "Arthur"
/// ```
///
/// When the right operand is nil, `IS NULL` is used instead of the
/// `=` operator:
///
/// ```swift
/// // name IS NULL
/// Column("name") == nil
/// ```
public static func == (lhs: Self, rhs: (any SQLExpressible)?) -> SQLExpression {
.equal(lhs.sqlExpression, rhs?.sqlExpression ?? .null)
}
/// The `=` SQL operator.
public static func == (lhs: Self, rhs: Bool) -> SQLExpression {
if rhs {
return lhs.sqlExpression.is(.true)
} else {
return lhs.sqlExpression.is(.false)
}
}
/// Compares two SQL expressions.
///
/// For example:
///
/// ```swift
/// // 'Arthur' = name
/// "Arthur" == Column("name")
/// ```
///
/// When the left operand is nil, `IS NULL` is used instead of the
/// `=` operator:
///
/// ```swift
/// // name IS NULL
/// nil == Column("name")
/// ```
public static func == (lhs: (any SQLExpressible)?, rhs: Self) -> SQLExpression {
.equal(lhs?.sqlExpression ?? .null, rhs.sqlExpression)
}
/// The `=` SQL operator.
public static func == (lhs: Bool, rhs: Self) -> SQLExpression {
if lhs {
return rhs.sqlExpression.is(.true)
} else {
return rhs.sqlExpression.is(.false)
}
}
/// The `=` SQL operator.
public static func == (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.equal(lhs.sqlExpression, rhs.sqlExpression)
}
/// Compares two SQL expressions.
///
/// For example:
///
/// ```swift
/// // name <> 'Arthur'
/// Column("name") != "Arthur"
/// ```
///
/// When the right operand is nil, `IS NOT NULL` is used instead of the
/// `<>` operator:
///
/// ```swift
/// // name IS NOT NULL
/// Column("name") != nil
/// ```
public static func != (lhs: Self, rhs: (any SQLExpressible)?) -> SQLExpression {
!(lhs == rhs)
}
/// The `<>` SQL operator.
public static func != (lhs: Self, rhs: Bool) -> SQLExpression {
!(lhs == rhs)
}
/// Compares two SQL expressions.
///
/// For example:
///
/// ```swift
/// // 'Arthur' <> name
/// "Arthur" != Column("name")
/// ```
///
/// When the left operand is nil, `IS NOT NULL` is used instead of the
/// `<>` operator:
///
/// ```swift
/// // name IS NOT NULL
/// nil != Column("name")
/// ```
public static func != (lhs: (any SQLExpressible)?, rhs: Self) -> SQLExpression {
!(lhs == rhs)
}
/// The `<>` SQL operator.
public static func != (lhs: Bool, rhs: Self) -> SQLExpression {
!(lhs == rhs)
}
/// The `<>` SQL operator.
public static func != (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
!(lhs == rhs)
}
/// The `IS` SQL operator.
public static func === (lhs: Self, rhs: (any SQLExpressible)?) -> SQLExpression {
.compare(.is, lhs.sqlExpression, rhs?.sqlExpression ?? .null)
}
/// The `IS` SQL operator.
public static func === (lhs: (any SQLExpressible)?, rhs: Self) -> SQLExpression {
if let lhs {
return .compare(.is, lhs.sqlExpression, rhs.sqlExpression)
} else {
return .compare(.is, rhs.sqlExpression, .null)
}
}
/// The `IS` SQL operator.
public static func === (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.compare(.is, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `IS NOT` SQL operator.
public static func !== (lhs: Self, rhs: (any SQLExpressible)?) -> SQLExpression {
!(lhs === rhs)
}
/// The `IS NOT` SQL operator.
public static func !== (lhs: (any SQLExpressible)?, rhs: Self) -> SQLExpression {
!(lhs === rhs)
}
/// The `IS NOT` SQL operator.
public static func !== (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
!(lhs === rhs)
}
// MARK: - Comparison Operators (<, >, <=, >=)
/// The `<` SQL operator.
public static func < (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.binary(.lessThan, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `<` SQL operator.
public static func < (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.binary(.lessThan, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `<` SQL operator.
public static func < (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.binary(.lessThan, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `<=` SQL operator.
public static func <= (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.binary(.lessThanOrEqual, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `<=` SQL operator.
public static func <= (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.binary(.lessThanOrEqual, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `<=` SQL operator.
public static func <= (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.binary(.lessThanOrEqual, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>` SQL operator.
public static func > (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.binary(.greaterThan, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>` SQL operator.
public static func > (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.binary(.greaterThan, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>` SQL operator.
public static func > (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.binary(.greaterThan, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>=` SQL operator.
public static func >= (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.binary(.greaterThanOrEqual, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>=` SQL operator.
public static func >= (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.binary(.greaterThanOrEqual, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>=` SQL operator.
public static func >= (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.binary(.greaterThanOrEqual, lhs.sqlExpression, rhs.sqlExpression)
}
}
// MARK: - Inclusion Operators (BETWEEN, IN)
extension Range where Bound: SQLExpressible {
/// Returns an SQL expression that checks the inclusion of an expression in
/// a range.
///
/// For example:
///
/// ```swift
/// // email >= 'A' AND email < 'B'
/// ("A"..<"B").contains(Column("email"))
/// ```
public func contains(_ element: some SQLSpecificExpressible) -> SQLExpression {
(element >= lowerBound) && (element < upperBound)
}
}
extension ClosedRange where Bound: SQLExpressible {
/// Returns an SQL expression that checks the inclusion of an expression in
/// a range.
///
/// For example:
///
/// ```swift
/// // initial BETWEEN 'A' AND 'B'
/// ("A"..."B").contains(Column("initial"))
/// ```
public func contains(_ element: some SQLSpecificExpressible) -> SQLExpression {
.between(
expression: element.sqlExpression,
lowerBound: lowerBound.sqlExpression,
upperBound: upperBound.sqlExpression)
}
}
extension CountableRange where Bound: SQLExpressible {
/// Returns an SQL expression that checks the inclusion of an expression in
/// a range.
///
/// For example:
///
/// ```swift
/// // id >= 1 AND id < 10
/// (1..<10).contains(Column("id"))
/// ```
public func contains(_ element: some SQLSpecificExpressible) -> SQLExpression {
(element >= lowerBound) && (element < upperBound)
}
}
extension CountableClosedRange where Bound: SQLExpressible {
/// Returns an SQL expression that checks the inclusion of an expression in
/// a range.
///
/// For example:
///
/// ```swift
/// // id BETWEEN 1 AND 10
/// (1...10).contains(Column("id"))
/// ```
public func contains(_ element: some SQLSpecificExpressible) -> SQLExpression {
.between(
expression: element.sqlExpression,
lowerBound: lowerBound.sqlExpression,
upperBound: upperBound.sqlExpression)
}
}
extension Sequence where Element: SQLExpressible {
/// Returns an SQL expression that checks the inclusion of an expression in
/// a sequence.
///
/// For example:
///
/// ```swift
/// // id IN (1,2,3)
/// [1, 2, 3].contains(Column("id"))
/// ```
public func contains(_ element: some SQLSpecificExpressible) -> SQLExpression {
SQLCollection.array(map(\.sqlExpression)).contains(element.sqlExpression)
}
}
extension Sequence where Element == any SQLExpressible {
/// Returns an SQL expression that checks the inclusion of an expression in
/// a sequence.
///
/// For example:
///
/// ```swift
/// // id IN (1,2,3)
/// [1, 2, 3].contains(Column("id"))
/// ```
public func contains(_ element: some SQLSpecificExpressible) -> SQLExpression {
SQLCollection.array(map(\.sqlExpression)).contains(element.sqlExpression)
}
}
// MARK: - Arithmetic Operators (+, -, *, /)
extension SQLSpecificExpressible {
/// The `*` SQL operator.
public static func * (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.associativeBinary(.multiply, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `*` SQL operator.
public static func * (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.associativeBinary(.multiply, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `*` SQL operator.
public static func * (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.associativeBinary(.multiply, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `/` SQL operator.
public static func / (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.binary(.divide, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `/` SQL operator.
public static func / (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.binary(.divide, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `/` SQL operator.
public static func / (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.binary(.divide, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `+` SQL operator.
public static func + (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.associativeBinary(.add, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `+` SQL operator.
public static func + (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.associativeBinary(.add, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `+` SQL operator.
public static func + (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.associativeBinary(.add, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `-` SQL operator.
public static prefix func - (value: Self) -> SQLExpression {
.unary(.minus, value.sqlExpression)
}
/// The `-` SQL operator.
public static func - (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.binary(.subtract, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `-` SQL operator.
public static func - (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.binary(.subtract, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `-` SQL operator.
public static func - (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.binary(.subtract, lhs.sqlExpression, rhs.sqlExpression)
}
// MARK: - Logical Operators (AND, OR, NOT)
/// The `AND` SQL operator.
public static func && (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.associativeBinary(.and, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `AND` SQL operator.
public static func && (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.associativeBinary(.and, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `AND` SQL operator.
public static func && (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.associativeBinary(.and, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `OR` SQL operator.
public static func || (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.associativeBinary(.or, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `OR` SQL operator.
public static func || (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.associativeBinary(.or, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `OR` SQL operator.
public static func || (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.associativeBinary(.or, [lhs.sqlExpression, rhs.sqlExpression])
}
/// A negated logical SQL expression.
///
/// For example:
///
/// ```swift
/// // NOT isBlue
/// !Column("isBlue")
/// ```
///
/// Some expressions are negated with specific SQL operators:
///
/// ```swift
/// // id NOT BETWEEN 1 AND 10
/// !((1...10).contains(Column("id")))
/// ```
public static prefix func ! (value: Self) -> SQLExpression {
value.sqlExpression.is(.falsey)
}
}
// MARK: - Bitwise Operators (&, |, ~, <<, >>)
extension SQLSpecificExpressible {
/// The `&` SQL operator.
public static func & (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.associativeBinary(.bitwiseAnd, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `&` SQL operator.
public static func & (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.associativeBinary(.bitwiseAnd, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `&` SQL operator.
public static func & (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.associativeBinary(.bitwiseAnd, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `|` SQL operator.
public static func | (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.associativeBinary(.bitwiseOr, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `|` SQL operator.
public static func | (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.associativeBinary(.bitwiseOr, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `|` SQL operator.
public static func | (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.associativeBinary(.bitwiseOr, [lhs.sqlExpression, rhs.sqlExpression])
}
/// The `<<` SQL operator.
public static func << (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.binary(.leftShift, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `<<` SQL operator.
public static func << (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.binary(.leftShift, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `<<` SQL operator.
public static func << (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.binary(.leftShift, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>>` SQL operator.
public static func >> (lhs: Self, rhs: some SQLExpressible) -> SQLExpression {
.binary(.rightShift, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>>` SQL operator.
public static func >> (lhs: some SQLExpressible, rhs: Self) -> SQLExpression {
.binary(.rightShift, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `>>` SQL operator.
public static func >> (lhs: Self, rhs: some SQLSpecificExpressible) -> SQLExpression {
.binary(.rightShift, lhs.sqlExpression, rhs.sqlExpression)
}
/// The `~` SQL operator.
public static prefix func ~ (value: Self) -> SQLExpression {
.unary(.bitwiseNot, value.sqlExpression)
}
}
// MARK: - Like Operator
extension SQLSpecificExpressible {
/// The `LIKE` SQL operator.
///
/// For example:
///
/// ```swift
/// // email LIKE '%@example.com"
/// Column("email").like("%@example.com")
///
/// // title LIKE '%10\%%' ESCAPE '\'
/// Column("title").like("%10\\%%", escape: "\\")
/// ```
public func like(_ pattern: some SQLExpressible, escape: (any SQLExpressible)? = nil) -> SQLExpression {
.escapableBinary(.like, sqlExpression, pattern.sqlExpression, escape: escape?.sqlExpression)
}
}
@@ -0,0 +1,150 @@
/// An SQL ordering term.
///
/// `SQLOrdering` is an opaque representation of an SQL ordering term.
/// You generally build `SQLOrdering` from other expressions. For example:
///
/// ```swift
/// Column("score").desc
/// SQL("score DESC").sqlOrdering
/// ```
///
/// `SQLOrdering` is better used as the return type of a function. For
/// function arguments, prefer the ``SQLOrderingTerm`` protocol.
///
/// Related SQLite documentation: <https://www.sqlite.org/syntax/ordering-term.html>
public struct SQLOrdering {
private var impl: Impl
private enum Impl {
/// An expression
///
/// ORDER BY score
case expression(SQLExpression)
/// An ascending expression
///
/// ORDER BY score ASC
case asc(SQLExpression)
/// An descending expression
///
/// ORDER BY score DESC
case desc(SQLExpression)
/// Only available from SQLite 3.30.0
case ascNullsLast(SQLExpression)
/// Only available from SQLite 3.30.0
case descNullsFirst(SQLExpression)
/// A literal SQL ordering
case literal(SQL)
}
static func expression(_ expression: SQLExpression) -> SQLOrdering {
self.init(impl: .expression(expression))
}
static func asc(_ expression: SQLExpression) -> SQLOrdering {
self.init(impl: .asc(expression))
}
static func desc(_ expression: SQLExpression) -> SQLOrdering {
self.init(impl: .desc(expression))
}
static func ascNullsLast(_ expression: SQLExpression) -> SQLOrdering {
self.init(impl: .ascNullsLast(expression))
}
static func descNullsFirst(_ expression: SQLExpression) -> SQLOrdering {
self.init(impl: .descNullsFirst(expression))
}
static func literal(_ sqlLiteral: SQL) -> SQLOrdering {
self.init(impl: .literal(sqlLiteral))
}
}
extension SQLOrdering {
func sql(_ context: SQLGenerationContext) throws -> String {
switch impl {
case .expression(let expression):
return try expression.sql(context)
case .asc(let expression):
return try expression.sql(context) + " ASC"
case .desc(let expression):
return try expression.sql(context) + " DESC"
case .ascNullsLast(let expression):
return try expression.sql(context) + " ASC NULLS LAST"
case .descNullsFirst(let expression):
return try expression.sql(context) + " DESC NULLS FIRST"
case .literal(let literal):
return try literal.sql(context)
}
}
}
extension SQLOrdering {
func qualified(with alias: TableAlias) -> SQLOrdering {
switch impl {
case .expression(let expression):
return .expression(expression.qualified(with: alias))
case .asc(let expression):
return .asc(expression.qualified(with: alias))
case .desc(let expression):
return .desc(expression.qualified(with: alias))
case .ascNullsLast(let expression):
return .ascNullsLast(expression.qualified(with: alias))
case .descNullsFirst(let expression):
return .descNullsFirst(expression.qualified(with: alias))
case .literal(let literal):
return .literal(literal.qualified(with: alias))
}
}
}
extension SQLOrdering {
var reversed: SQLOrdering {
switch impl {
case .expression(let expression):
return .desc(expression)
case .asc(let expression):
return .desc(expression)
case .desc(let expression):
return .asc(expression)
case .ascNullsLast(let expression):
return .descNullsFirst(expression)
case .descNullsFirst(let expression):
return .ascNullsLast(expression)
case .literal:
fatalError("""
Ordering literals can't be reversed. \
To resolve this error, order by expression literals instead. \
For example: order(SQL("(score + bonus)").sqlExpression)
""")
}
}
}
// MARK: - SQLOrderingTerm
/// A type that can be used as an SQL ordering term.
///
/// Related SQLite documentation <https://www.sqlite.org/syntax/ordering-term.html>
///
/// ## Topics
///
/// ### Supporting Type
///
/// - ``SQLOrdering``
public protocol SQLOrderingTerm {
/// Returns an SQL ordering.
var sqlOrdering: SQLOrdering { get }
}
extension SQLOrdering: SQLOrderingTerm {
// Not a real deprecation, just a usage warning
@available(*, deprecated, message: "Already QLOrdering:")
public var sqlOrdering: SQLOrdering { self }
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,336 @@
/// An SQL result column.
///
/// `SQLSelection` is an opaque representation of an SQL result column.
/// You generally build `SQLSelection` from other expressions. For example:
///
/// ```swift
/// // Aliased expressions
/// (Column("score") + Column("bonus")).forKey("total")
///
/// // Literal selection
/// SQL("IFNULL(name, \(defaultName)) AS name").sqlSelection
/// ```
///
/// `SQLSelection` is better used as the return type of a function. For
/// function arguments, prefer the ``SQLSelectable`` protocol.
///
/// Related SQLite documentation: <https://www.sqlite.org/syntax/result-column.html>
public struct SQLSelection {
private var impl: Impl
/// The private implementation of the public `SQLSelection`.
private enum Impl {
/// All columns: `*`
case allColumns
/// All columns, qualified: `player.*`
case qualifiedAllColumns(TableAlias)
/// An expression
case expression(SQLExpression)
/// An aliased expression
///
/// <expression> AS name
case aliasedExpression(SQLExpression, String)
/// A literal SQL selection
case literal(SQL)
}
/// All columns: `*`
static let allColumns = SQLSelection(impl: .allColumns)
/// All columns, qualified: `player.*`
static func qualifiedAllColumns(_ alias: TableAlias) -> Self {
self.init(impl: .qualifiedAllColumns(alias))
}
/// An expression
static func expression(_ expression: SQLExpression) -> Self {
self.init(impl: .expression(expression))
}
/// An aliased expression
///
/// <expression> AS name
static func aliasedExpression(_ expression: SQLExpression, _ name: String) -> Self {
self.init(impl: .aliasedExpression(expression, name))
}
/// A literal SQL selection
static func literal(_ sqlLiteral: SQL) -> Self {
self.init(impl: .literal(sqlLiteral))
}
}
extension SQLSelection {
/// Returns the number of columns in the selection.
///
/// Returns nil when the number of columns is unknown.
func columnCount(_ context: SQLGenerationContext) throws -> Int? {
switch impl {
case .allColumns:
// Likely a GRDB bug: we can't count the number of columns in an
// unqualified table.
return nil
case let .qualifiedAllColumns(alias):
return try context.columnCount(in: alias.tableName)
case .expression,
.aliasedExpression:
return 1
case .literal:
// We do not embed any SQL parser: we can't count the number of
// columns in a literal selection.
return nil
}
}
/// If the selection can be counted, return how to count it.
func count(distinct: Bool) -> SQLCount? {
switch impl {
case .allColumns:
// SELECT DISTINCT * FROM tableName ...
if distinct {
// Can't count
return nil
}
// SELECT * FROM tableName ...
// ->
// SELECT COUNT(*) FROM tableName ...
return .all
case .qualifiedAllColumns:
return nil
case let .expression(expression),
let .aliasedExpression(expression, _):
if distinct {
// SELECT DISTINCT expr FROM tableName ...
// ->
// SELECT COUNT(DISTINCT expr) FROM tableName ...
return .distinct(expression)
} else {
// SELECT expr FROM tableName ...
// ->
// SELECT COUNT(*) FROM tableName ...
return .all
}
case .literal:
return nil
}
}
/// Returns the SQL that feeds the argument of the `COUNT` function.
///
/// For example:
///
/// COUNT(*)
/// COUNT(id)
/// ^---- countedSQL
///
/// - parameter context: An SQL generation context which accepts
/// statement arguments.
func countedSQL(_ context: SQLGenerationContext) throws -> String {
switch impl {
case .allColumns:
return "*"
case let .qualifiedAllColumns(alias):
if context.qualifier(for: alias) != nil {
// SELECT COUNT(t.*) is invalid SQL
fatalError("Not implemented, or invalid query")
}
return "*"
case let .expression(expression),
let .aliasedExpression(expression, _):
return try expression.sql(context)
case .literal:
fatalError("""
Selection literals can't be counted. \
To resolve this error, select one or several literal expressions instead. \
See SQL.sqlExpression.
""")
}
}
/// Returns the SQL that feeds the selection of a `SELECT` statement.
///
/// For example:
///
/// 1
/// name
/// COUNT(*)
/// (score + bonus) AS total
///
/// See <https://sqlite.org/syntax/result-column.html>
///
/// - parameter context: An SQL generation context which accepts
/// statement arguments.
func sql(_ context: SQLGenerationContext) throws -> String {
switch impl {
case .allColumns:
return "*"
case let .qualifiedAllColumns(alias):
if let qualifier = context.qualifier(for: alias) {
return qualifier.quotedDatabaseIdentifier + ".*"
}
return "*"
case let .expression(expression):
return try expression.sql(context)
case let .aliasedExpression(expression, name):
return try expression.sql(context) + " AS " + name.quotedDatabaseIdentifier
case let .literal(sqlLiteral):
return try sqlLiteral.sql(context)
}
}
/// Returns true if the selection is an aggregate.
///
/// When in doubt, returns false.
///
/// SELECT * -- 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 .expression(expression),
let .aliasedExpression(expression, _):
return expression.isAggregate
default:
return false
}
}
/// Returns a qualified selection
func qualified(with alias: TableAlias) -> SQLSelection {
switch impl {
case .qualifiedAllColumns:
return self
case .allColumns:
return .qualifiedAllColumns(alias)
case let .expression(expression):
return .expression(expression.qualified(with: alias))
case let .aliasedExpression(expression, name):
return .aliasedExpression(expression.qualified(with: alias), name)
case let .literal(sqlLiteral):
return .literal(sqlLiteral.qualified(with: alias))
}
}
/// Supports SQLRelation.fetchCount.
///
/// See <https://github.com/groue/GRDB.swift/issues/1357>
var isTriviallyCountable: Bool {
switch impl {
case .aliasedExpression, .literal:
return false
case .allColumns, .qualifiedAllColumns, .expression:
return true
}
}
}
extension [SQLSelection] {
/// Returns the number of columns in the selection.
///
/// This method raises a fatal error if the selection contains a literal,
///
/// See `SQLSelection.columnCount(_:)` for testability.
func columnCount(_ context: SQLGenerationContext) throws -> Int {
try reduce(0) { acc, selection in
guard let count = try selection.columnCount(context) else {
// Found an SQL literal:
// - Player.select(sql: "id, name, score")
// - Player.select(literal: "id, name, score")
fatalError("""
Selection literals don't known how many columns they contain. \
To resolve this error, select one or several expressions instead.
""")
}
return acc + count
}
}
}
enum SQLCount {
/// Represents `COUNT(*)`
case all
/// Represents `COUNT(DISTINCT expression)`
case distinct(SQLExpression)
}
// MARK: - SQLSelectable
/// A type that can be used as SQL result columns.
///
/// Related SQLite documentation <https://www.sqlite.org/syntax/result-column.html>
///
/// ## Topics
///
/// ### Supporting Types
///
/// - ``AllColumns``
/// - ``SQLSelection``
public protocol SQLSelectable {
/// Returns an SQL selection.
var sqlSelection: SQLSelection { get }
}
extension SQLSelection: SQLSelectable {
// Not a real deprecation, just a usage warning
@available(*, deprecated, message: "Already SQLSelection")
public var sqlSelection: SQLSelection { self }
}
// MARK: - AllColumns
/// `AllColumns` is the `*` in `SELECT *`.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// // SELECT * FROM player
/// let players = try Player.select(AllColumns()).fetchAll(db)
/// }
/// ```
public struct AllColumns: Sendable {
/// The `*` selection.
public init() { }
}
extension AllColumns: SQLSelectable {
public var sqlSelection: SQLSelection {
.allColumns
}
}
@@ -0,0 +1,136 @@
/// 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)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
enum SQLColumnGenerator {
case columnDefinition(ColumnDefinition)
case columnLiteral(SQL)
/// - parameter tableName: The name of the table that contains
/// the column.
/// - parameter primaryKeyColumns: A closure that returns the
/// primary key columns in the table that contains the column. If
/// the result is nil, the primary key is the hidden rowID.
func sql(
_ db: Database,
tableName: String,
primaryKeyColumns: () throws -> [SQLColumnDescriptor]?)
throws -> String
{
switch self {
case let .columnDefinition(column):
return try columnSQL(
db, column: column,
tableName: tableName,
primaryKeyColumns: primaryKeyColumns)
case let .columnLiteral(sqlLiteral):
let context = SQLGenerationContext(db, argumentsSink: .literalValues)
return try sqlLiteral.sql(context)
}
}
private func columnSQL(
_ db: Database,
column: ColumnDefinition,
tableName: String,
primaryKeyColumns: () throws -> [SQLColumnDescriptor]?)
throws -> String
{
var chunks: [String] = []
chunks.append(column.name.quotedDatabaseIdentifier)
if let type = column.type {
chunks.append(type.rawValue)
}
if let (conflictResolution, autoincrement) = column.primaryKey {
chunks.append("PRIMARY KEY")
if let conflictResolution {
chunks.append("ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
if autoincrement {
chunks.append("AUTOINCREMENT")
}
}
switch column.notNullConflictResolution {
case .none:
break
case .abort:
chunks.append("NOT NULL")
case let conflictResolution?:
chunks.append("NOT NULL ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
switch column.indexing {
case .none:
break
case .unique(let conflictResolution):
switch conflictResolution {
case .abort:
chunks.append("UNIQUE")
default:
chunks.append("UNIQUE ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
case .index:
break
}
for checkConstraint in column.checkConstraints {
try chunks.append("CHECK (\(checkConstraint.quotedSQL(db)))")
}
if let defaultExpression = column.defaultExpression {
try chunks.append("DEFAULT \(defaultExpression.quotedSQL(db))")
}
if let collationName = column.collationName {
chunks.append("COLLATE")
chunks.append(collationName)
}
for constraint in column.foreignKeyConstraints {
chunks.append("REFERENCES")
if let column = constraint.destinationColumn {
// explicit referenced column names
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)\
(\(column.quotedDatabaseIdentifier))
""")
} else {
// implicit reference to primary key
let pkColumns: [String]
if constraint.destinationTable.lowercased() == tableName.lowercased() {
// autoreference
let primaryKeyColumns = try primaryKeyColumns() ?? [.rowID]
pkColumns = primaryKeyColumns.map(\.name)
} else {
pkColumns = try db.primaryKey(constraint.destinationTable).columns
}
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)\
(\(pkColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")))
""")
}
if let deleteAction = constraint.deleteAction {
chunks.append("ON DELETE")
chunks.append(deleteAction.rawValue)
}
if let updateAction = constraint.updateAction {
chunks.append("ON UPDATE")
chunks.append(updateAction.rawValue)
}
if constraint.isDeferred {
chunks.append("DEFERRABLE INITIALLY DEFERRED")
}
}
if let constraint = column.generatedColumnConstraint {
try chunks.append("GENERATED ALWAYS AS (\(constraint.expression.quotedSQL(db)))")
let qualificationLiteral: String
switch constraint.qualification {
case .stored:
qualificationLiteral = "STORED"
case .virtual:
qualificationLiteral = "VIRTUAL"
}
chunks.append(qualificationLiteral)
}
return chunks.joined(separator: " ")
}
}
@@ -0,0 +1,567 @@
/// SQLGenerationContext supports SQL generation:
///
/// - It provides a database connection during SQL generation, for any purpose
/// such as schema introspection.
///
/// - It provides unique table aliases in order to disambiguates table names
/// and columns.
///
/// - It gathers SQL arguments in order to prevent SQL injection.
final class SQLGenerationContext {
private enum Parent {
case none(db: Database, argumentsSink: StatementArgumentsSink)
case context(SQLGenerationContext)
}
/// A database connection.
var db: Database {
switch parent {
case let .none(db: db, argumentsSink: _): return db
case let .context(context): return context.db
}
}
/// All gathered arguments
var arguments: StatementArguments { argumentsSink.arguments }
/// Access to the database connection, the arguments sink, ctes, and resolved
/// names of table aliases from outer contexts (useful in case of
/// subquery generation).
private let parent: Parent
/// The arguments sink which prevents SQL injection.
private var argumentsSink: StatementArgumentsSink {
switch parent {
case let .none(db: _, argumentsSink: argumentsSink): return argumentsSink
case let .context(context): return context.argumentsSink
}
}
private let resolvedNames: [TableAlias: String]
private let ownAliases: Set<TableAlias>
private let ownCTEs: [String: SQLCTE]
/// Creates a generation context.
///
/// - parameter db: A database connection.
/// - parameter argumentsSink: An arguments sink.
/// - parameter aliases: An array of table aliases to disambiguate.
/// - parameter ctes: An dictionary of available CTEs.
init(
_ db: Database,
argumentsSink: StatementArgumentsSink = StatementArgumentsSink(),
aliases: [TableAlias] = [],
ctes: OrderedDictionary<String, SQLCTE> = [:])
{
self.parent = .none(db: db, argumentsSink: argumentsSink)
self.resolvedNames = aliases.resolvedNames
self.ownAliases = Set(aliases)
self.ownCTEs = Dictionary(uniqueKeysWithValues: ctes.lazy.map { ($0.lowercased(), $1) })
}
/// Creates a generation context.
///
/// - parameter parent: A parent context.
/// - parameter aliases: An array of table aliases to disambiguate.
/// - parameter ctes: An dictionary of available CTEs.
private init(
parent: SQLGenerationContext,
aliases: [TableAlias],
ctes: OrderedDictionary<String, SQLCTE>)
{
self.parent = .context(parent)
self.resolvedNames = aliases.resolvedNames
self.ownAliases = Set(aliases)
self.ownCTEs = Dictionary(uniqueKeysWithValues: ctes.lazy.map { ($0.lowercased(), $1) })
}
/// Returns a generation context suitable for subqueries.
func subqueryContext(
aliases: [TableAlias] = [],
ctes: OrderedDictionary<String, SQLCTE> = [:]) -> SQLGenerationContext
{
SQLGenerationContext(parent: self, aliases: aliases, ctes: ctes)
}
/// Returns whether arguments could be appended.
///
/// A false result means that the generation context does not support
/// SQL arguments, and `?` placeholders are not supported.
/// This happens, for example, when we are creating tables:
///
/// // CREATE TABLE player (
/// // name TEXT DEFAULT 'Anonymous' -- String literal instead of ?
/// // )
/// let defaultName = "Anonymous"
/// try db.create(table: "player") { t in
/// t.column(literal: "name TEXT DEFAULT \(defaultName)")
/// }
///
/// A false result is turned into a fatal error when the user uses
/// SQL arguments at unsupported locations:
///
/// // Fatal error:
/// // Not implemented: turning an SQL parameter into an SQL literal value
/// let defaultName = "Anonymous"
/// let literal = SQL(sql: "name TEXT DEFAULT ?", arguments: [defaultName])
/// try db.create(table: "player") { t in
/// t.column(literal: literal)
/// }
func append(arguments: StatementArguments) -> Bool {
argumentsSink.append(arguments: arguments)
}
/// May be nil, when a qualifier is not needed:
///
/// WHERE <qualifier>.column == 1
/// SELECT <qualifier>.*
///
/// WHERE column == 1
/// SELECT *
func qualifier(for alias: TableAlias) -> String? {
if alias.hasUserName {
return alias.identityName
}
if !ownAliases.contains(alias) {
return resolvedName(for: alias)
}
if ownAliases.count > 1 {
return resolvedName(for: alias)
}
return nil
}
/// WHERE <resolvedName> MATCH pattern
func resolvedName(for alias: TableAlias) -> String {
if let name = resolvedNames[alias] {
return name
}
switch parent {
case .none:
return alias.identityName
case let .context(context):
return context.resolvedName(for: alias)
}
}
/// FROM tableName <alias>
func aliasName(for alias: TableAlias) -> String? {
let resolvedName = self.resolvedName(for: alias)
if resolvedName != alias.tableName {
return resolvedName
}
return nil
}
func columnCount(in tableName: String) throws -> Int {
if let cte = ownCTEs[tableName.lowercased()] {
return try cte.columnCount(db)
}
switch parent {
case let .context(context):
return try context.columnCount(in: tableName)
case let .none(db: db, argumentsSink: _):
return try db.columns(in: tableName).count
}
}
}
/// A class that gathers statement arguments, and can be shared between
/// several SQLGenerationContext.
class StatementArgumentsSink {
private(set) var arguments: StatementArguments
private let rawSQL: Bool
/// A sink which turns all argument values into SQL literals.
///
/// The `"WHERE name = \("O'Brien")"` SQL literal is turned into the
/// `WHERE name = 'O''Brien'` SQL.
static let literalValues = StatementArgumentsSink(rawSQL: true)
private init(rawSQL: Bool) {
self.arguments = []
self.rawSQL = rawSQL
}
/// A sink which turns all argument values into `?` SQL parameters.
///
/// The `"WHERE name = \("O'Brien")"` SQL literal is turned into the
/// `WHERE name = ?` SQL.
convenience init() {
self.init(rawSQL: false)
}
// fileprivate so that SQLGenerationContext.append(arguments:) is the only
// available api.
/// Returns false for SQLGenerationContext.rawSQLContext
fileprivate func append(arguments: StatementArguments) -> Bool {
if arguments.isEmpty {
return true
}
if rawSQL {
return false
}
self.arguments += arguments
return true
}
}
// MARK: - TableAlias
/// A TableAlias identifies a table in a request.
///
/// See ``TableRequest/aliased(_:)`` for more information and examples.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
public class TableAlias {
private enum Impl {
/// A TableAlias is undefined when it is created by the GRDB user:
///
/// let alias = TableAlias()
/// let alias = TableAlias(name: "custom")
case undefined(userName: String?)
/// A TableAlias is a table when explicitly specified:
///
/// let alias = TableAlias(tableName: "player")
///
/// Or when it qualifies a request that wasn't qualified yet (in which
/// case it turns from undefined to a table):
///
/// // SELECT custom.* FROM player custom
/// let alias = TableAlias(name: "custom")
/// let request = Player.all().aliased(alias)
case table(tableName: String, userName: String?)
/// A TableAlias can be a proxy for another table alias. Two different
/// instances for the same table identifier:
///
/// // Pointless example: make alias2 a proxy for alias1
/// let alias1 = TableAlias()
/// let alias2 = TableAlias()
/// Player.all()
/// .aliased(alias1)
/// .aliased(alias2)
///
/// Proxies are useful because queries get implicit aliases as soon
/// as they are joined with associations. In the example below,
/// customAlias becomes a proxy for the request's implicit alias, which
/// gets a custom name. This allows implicit and user aliases to merge
/// into a single "table identifier" that matches the user's expectations:
///
/// // SELECT custom.*, team.*
/// // FROM player custom
/// // JOIN team ON taem.id = custom.teamId
/// // WHERE custom.name = 'Arthur'
/// let customAlias = TableAlias(name: "custom")
/// let request = Player
/// .including(required: Player.team)
/// .filter(sql: "custom.name = 'Arthur'")
/// .aliased(customAlias)
case proxy(TableAlias)
}
private var impl: Impl
/// Resolve all proxies
private var root: TableAlias {
if case .proxy(let base) = impl {
return base.root
} else {
return self
}
}
// exposed to SQLGenerationContext
fileprivate var identityName: String {
userName ?? tableName
}
// exposed to SQLGenerationContext
fileprivate var hasUserName: Bool {
userName != nil
}
var tableName: String {
switch impl {
case .undefined:
// Likely a GRDB bug
fatalError("Undefined alias has no table name")
case .table(tableName: let tableName, userName: _):
return tableName
case .proxy(let base):
return base.tableName
}
}
private var userName: String? {
switch impl {
case .undefined(let userName):
return userName
case .table(tableName: _, userName: let userName):
return userName
case .proxy(let base):
return base.userName
}
}
/// Creates a TableAlias.
///
/// When the alias is given a name, this name is guaranteed to be used as
/// the table alias in the SQL query:
///
/// ```swift
/// // SELECT p.* FROM player p
/// let alias = TableAlias(name: "p")
/// let request = Player.all().aliased(alias)
/// ```
public init(name: String? = nil) {
self.impl = .undefined(userName: name)
}
init(tableName: String, userName: String? = nil) {
self.impl = .table(tableName: tableName, userName: userName)
}
func becomeProxy(of base: TableAlias) {
if self === base {
return
}
switch impl {
case let .undefined(userName):
if let userName {
// rename
assert(base.userName == nil || base.userName == userName)
base.setUserName(userName)
}
self.impl = .proxy(base)
case let .table(tableName: tableName, userName: userName):
assert(tableName == base.tableName)
if let userName {
// rename
assert(base.userName == nil || base.userName == userName)
base.setUserName(userName)
}
self.impl = .proxy(base)
case let .proxy(selfBase):
selfBase.becomeProxy(of: base)
}
}
/// Returns nil if aliases can't be merged (conflict in tables, aliases...)
func merged(with other: TableAlias) -> TableAlias? {
if self === other {
return self
}
let root = self.root
let otherRoot = other.root
switch (root.impl, otherRoot.impl) {
case let (.table(tableName: tableName, userName: userName),
.table(tableName: otherTableName, userName: otherUserName)):
guard tableName == otherTableName else {
// can't merge
return nil
}
if let userName, let otherUserName, userName != otherUserName {
// can't merge
return nil
}
root.becomeProxy(of: otherRoot)
return otherRoot
default:
// can't merge
return nil
}
}
private func setUserName(_ userName: String) {
switch impl {
case .undefined:
self.impl = .undefined(userName: userName)
case .table(tableName: let tableName, userName: _):
self.impl = .table(tableName: tableName, userName: userName)
case .proxy(let base):
base.setUserName(userName)
}
}
func setTableName(_ tableName: String) {
switch impl {
case .undefined(let userName):
self.impl = .table(tableName: tableName, userName: userName)
case .table(tableName: let initialTableName, userName: _):
// It is a programmer error to reuse the same TableAlias for
// multiple tables.
//
// // Don't do that
// let alias = TableAlias()
// let books = Book.aliased(alias)...
// let authors = Author.aliased(alias)...
GRDBPrecondition(
tableName.lowercased() == initialTableName.lowercased(),
"A TableAlias most not be used to refer to multiple tables")
case .proxy(let base):
base.setTableName(tableName)
}
}
/// Returns a result column that refers to the aliased table.
public subscript(_ selectable: some SQLSelectable) -> SQLSelection {
// TODO: test
selectable.sqlSelection.qualified(with: self)
}
/// Returns an SQL expression that refers to the aliased table.
///
/// For example, let's sort 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 request = Book
/// .joining(required: Book.author.aliased(authorAlias))
/// .order(authorAlias[Column("name")], Column("title"))
/// ```
public subscript(_ expression: some SQLSpecificExpressible & SQLSelectable & SQLOrderingTerm) -> SQLExpression {
expression.sqlExpression.qualified(with: self)
}
public subscript(_ expression: some SQLJSONExpressible &
SQLSpecificExpressible &
SQLSelectable &
SQLOrderingTerm)
-> AnySQLJSONExpressible
{
AnySQLJSONExpressible(sqlExpression: expression.sqlExpression.qualified(with: self))
}
/// Returns an SQL ordering term that refers to the aliased table.
///
/// For example, let's sort 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 ASC, book.title ASC
/// let authorAlias = TableAlias()
/// let request = Book
/// .joining(required: Book.author.aliased(authorAlias))
/// .order(authorAlias[Column("name").asc], Column("title").asc)
/// ```
public subscript(_ ordering: some SQLOrderingTerm) -> SQLOrdering {
ordering.sqlOrdering.qualified(with: self)
}
/// Returns an SQL column that refers to the aliased table.
///
/// For example, let's sort 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 request = Book
/// .joining(required: Book.author.aliased(authorAlias))
/// .order(authorAlias["name"], Column("title"))
/// ```
public subscript(_ column: String) -> SQLExpression {
.qualifiedColumn(column, self)
}
/// A boolean SQL expression indicating whether this alias refers to some
/// rows, or not.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// In the example below, we only fetch books that are not associated to
/// any author:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord { }
/// struct Book: TableRecord, FetchableRecord {
/// static let author = belongsTo(Author.self)
/// }
///
/// try dbQueue.read { db in
/// let authorAlias = TableAlias()
/// let request = Book
/// .joining(optional: Book.author.aliased(authorAlias))
/// .filter(!authorAlias.exists)
/// let books = try request.fetchAll(db)
/// }
/// ```
public var exists: SQLExpression {
SQLExpression.qualifiedExists(self)
}
}
extension TableAlias: Equatable {
public static func == (lhs: TableAlias, rhs: TableAlias) -> Bool {
ObjectIdentifier(lhs.root) == ObjectIdentifier(rhs.root)
}
}
extension TableAlias: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(root))
}
}
extension [TableAlias] {
/// Resolve ambiguities in aliases' names.
fileprivate var resolvedNames: [TableAlias: String] {
// It is a programmer error to reuse the same TableAlias for
// multiple tables.
//
// // Don't do that
// let alias = TableAlias()
// let request = Book
// .including(required: Book.author.aliased(alias)...)
// .including(required: Book.author.aliased(alias)...)
GRDBPrecondition(count == Set(self).count, "A TableAlias most not be used to refer to multiple tables")
let groups = Dictionary(grouping: self) {
$0.identityName.lowercased()
}
var uniqueLowercaseNames: Set<String> = []
var ambiguousGroups: [[TableAlias]] = []
for (lowercaseName, group) in groups {
if group.count > 1 {
// It is a programmer error to reuse the same alias for multiple tables
GRDBPrecondition(
group.countElements(where: \.hasUserName) < 2,
"ambiguous alias: \(group[0].identityName)")
ambiguousGroups.append(group)
} else {
uniqueLowercaseNames.insert(lowercaseName)
}
}
var resolvedNames: [TableAlias: String] = [:]
for group in ambiguousGroups {
var index = 1
for alias in group {
if alias.hasUserName { continue }
let radical = alias.identityName.digitlessRadical
var resolvedName: String
repeat {
resolvedName = "\(radical)\(index)"
index += 1
} while uniqueLowercaseNames.contains(resolvedName.lowercased())
uniqueLowercaseNames.insert(resolvedName.lowercased())
resolvedNames[alias] = resolvedName
}
}
return resolvedNames
}
}
@@ -0,0 +1,42 @@
struct SQLIndexGenerator {
let name: String
let table: String
let expressions: [SQLExpression]
let options: IndexOptions
let condition: SQLExpression?
func sql(_ db: Database) throws -> String {
var sql: SQL = "CREATE"
if options.contains(.unique) {
sql += " UNIQUE"
}
sql += " INDEX"
if options.contains(.ifNotExists) {
sql += " IF NOT EXISTS"
}
sql += " \(identifier: name) ON \(identifier: table)("
sql += expressions.map { SQL($0) }.joined(separator: ", ")
sql += ")"
if let condition {
sql += " WHERE \(condition)"
}
let context = SQLGenerationContext(db, argumentsSink: .literalValues)
return try sql.sql(context)
}
}
extension SQLIndexGenerator {
init(index: IndexDefinition) {
name = index.name
table = index.table
expressions = index.expressions
options = index.options
condition = index.condition
}
}
@@ -0,0 +1,825 @@
/// SQLQueryGenerator is able to generate an SQL SELECT query.
struct SQLQueryGenerator: Refinable {
fileprivate private(set) var relation: SQLQualifiedRelation
private let singleResult: Bool
// For database region
private let prefetchedAssociations: [_SQLAssociation]
/// Creates an SQL query generator.
///
/// - parameter singleResult: A hint as to whether the query should be
/// optimized for a single result.
init(
relation: SQLRelation,
forSingleResult singleResult: Bool = false)
{
self.relation = SQLQualifiedRelation(relation)
self.prefetchedAssociations = relation.prefetchedAssociations
self.singleResult = singleResult
}
func requestSQL(_ context: SQLGenerationContext) throws -> String {
let context = context.subqueryContext(aliases: relation.allAliases, ctes: relation.ctes)
var sql = try commonTableExpressionsPrefix(context)
sql += "SELECT"
if relation.isDistinct {
sql += " DISTINCT"
}
let selection = try relation.selectionPromise.resolve(context.db)
GRDBPrecondition(!selection.isEmpty, "Can't generate SQL with an empty selection")
sql += " "
sql += try selection
.map { try $0.sql(context) }
.joined(separator: ", ")
sql += " FROM "
sql += try relation.source.sql(context)
if relation.joins.isEmpty == false {
let sourceAlias = relation.source.alias
for (_, join) in relation.joins {
sql += " "
sql += try join.sql(context, leftAlias: sourceAlias)
}
}
let filter = try relation.filterPromise?.resolve(context.db)
if let filter {
sql += " WHERE "
sql += try filter.sql(context)
}
let groupExpressions = try relation.groupPromise?.resolve(context.db) ?? []
if !groupExpressions.isEmpty {
sql += " GROUP BY "
sql += try groupExpressions
.map { try $0.sql(context) }
.joined(separator: ", ")
}
if let havingExpression = try relation.havingExpressionPromise?.resolve(context.db) {
sql += " HAVING "
sql += try havingExpression.sql(context)
}
let orderings = try relation.ordering.resolve(context.db)
if !orderings.isEmpty {
sql += " ORDER BY "
sql += try orderings
.map { try $0.sql(context) }
.joined(separator: ", ")
}
var limit = relation.limit
if try limit == nil && singleResult && !expectsSingleResult(
context.db,
selection: selection,
filter: filter,
groupExpressions: groupExpressions)
{
limit = SQLLimit(limit: 1, offset: limit?.offset)
}
if let limit {
sql += " LIMIT "
sql += limit.sql
}
return sql
}
func makePreparedRequest(_ db: Database) throws -> PreparedRequest {
try PreparedRequest(
statement: makeStatement(db),
adapter: rowAdapter(SQLGenerationContext(db, ctes: relation.ctes)))
}
/// The number of fetched columns.
func columnCount(_ db: Database) throws -> Int {
try relation
.selectionPromise
.resolve(db)
.columnCount(SQLGenerationContext(db, ctes: relation.ctes))
}
/// Returns a prepared statement
func makeStatement(_ db: Database) throws -> Statement {
// Build
let context = SQLGenerationContext(db)
let sql = try requestSQL(context)
// Compile & set arguments
let statement = try db.makeStatement(sql: sql)
statement.arguments = context.arguments
// Optimize statement region. This allows us to track individual rowids,
// and also find some provably empty requests such as `Player.none()`.
statement.databaseRegion = try optimizedSelectedRegion(db, statement.databaseRegion)
if !statement.databaseRegion.isEmpty {
// Unless the statement region is provably empty, also append the
// region of prefetched associations.
//
// This makes sure we observe the correct database region when the
// statement is executed, even if we don't actually fetch prefetched
// associations, due to lacking database content.
//
// For example, fetching `parent.including(all: children)` will
// select parents, but won't attempt to select any children if there
// is no parent in the database. And yet we need to observe the
// table for children. This is why we include the prefetched region.
//
// Note that the request `parent.none().including(all: children)` is
// different. Since `parent.none()` is provably empty, its region
// is empty, and we thus avoid this code branch.
let region = try prefetchedRegion(
db,
associations: prefetchedAssociations,
from: relation.source.tableName)
statement.databaseRegion.formUnion(region)
}
return statement
}
/// Returns an optimized database region, when possible.
///
/// The optimized region allows us to track individual rowids, and also
/// discard some provably empty requests such as `Player.none()`.
private func optimizedSelectedRegion(_ db: Database, _ selectedRegion: DatabaseRegion) throws -> DatabaseRegion {
var optimizedRegion = selectedRegion
// Give up unless request feeds from a database table
let tableName = relation.source.tableName
guard try db.tableExists(tableName) else { // skip views
return optimizedRegion
}
// If the request is filtered on rowIds, we can optimize the region:
//
// - Player.filter(Column("id") == 1) // region "player(*)[1]"
// - Player.filter(ids: [1, 2, 3]) // region "player(*)[1, 2, 3]"
// - Player.none() // region "empty"
if let filter = try relation.filterPromise?.resolve(db),
let rowIDs = try filter.identifyingRowIDs(db, for: relation.source.alias)
{
optimizedRegion = optimizedRegion.tableIntersection(tableName, rowIds: rowIDs)
}
return optimizedRegion
}
/// If true, executing this query yields at most one row.
/// If false, we don't know how many rows this query returns.
private func expectsSingleResult(
_ db: Database,
selection: [SQLSelection],
filter: SQLExpression?,
groupExpressions: [SQLExpression])
throws -> Bool
{
if relation.joins.isEmpty == false {
// Don't expect single results as soon as there is a join
return false
}
// Do we filter on a unique key?
let tableName = relation.source.tableName
if try db.tableExists(tableName), // skip views
let identifyingColums = try filter?.identifyingColums(db, for: relation.source.alias),
try db.table(tableName, hasUniqueKey: identifyingColums)
{
// Filter by unique key: guaranteed single row!
return true
}
// Do we aggregate without grouping?
if groupExpressions.isEmpty && selection.contains(where: \.isAggregate) {
// Selection contains an aggregate function: guaranteed single row!Ë
return true
}
return false
}
/// Returns a `DELETE` statement, with `RETURNING` clause if `selection`
/// is not empty.
func makeDeleteStatement(_ db: Database, selection: [any SQLSelectable] = []) throws -> Statement {
switch try grouping(db) {
case .none:
guard relation.joins.isEmpty else {
return try makeTrivialDeleteStatement(db, selection: selection)
}
let context = SQLGenerationContext(db, aliases: relation.allAliases, ctes: relation.ctes)
var sql = try commonTableExpressionsPrefix(context)
sql += try "DELETE FROM " + relation.source.sql(context)
if let filter = try relation.filterPromise?.resolve(db) {
sql += " WHERE "
sql += try filter.sql(context)
}
if let limit = relation.limit {
let orderings = try relation.ordering.resolve(db)
if !orderings.isEmpty {
sql += " ORDER BY "
sql += try orderings
.map { try $0.sql(context) }
.joined(separator: ", ")
}
sql += " LIMIT " + limit.sql
}
return try makeStatement(db, sql: sql, arguments: context.arguments, returning: selection)
case .unique:
return try makeTrivialDeleteStatement(db, selection: selection)
case .nonUnique:
// Programmer error
fatalError("Can't delete query with GROUP BY clause")
}
}
/// DELETE FROM table WHERE id IN (SELECT id FROM table ...)
/// DELETE FROM table WHERE id IN (SELECT id FROM table ...) RETURNING ...
private func makeTrivialDeleteStatement(_ db: Database, selection: [any SQLSelectable]) throws -> Statement {
let tableName = relation.source.tableName
let alias = TableAlias(tableName: tableName)
let context = SQLGenerationContext(db, aliases: [alias])
let subqueryContext = context.subqueryContext(aliases: relation.allAliases, ctes: relation.ctes)
let primaryKey = SQLExpression.fastPrimaryKey
let selectPrimaryKey = self.with {
$0.relation = $0.relation.selectOnly([.expression(primaryKey)])
}
var sql = "DELETE FROM \(tableName.quotedDatabaseIdentifier) WHERE "
sql += try alias[primaryKey].sql(context)
sql += " IN ("
sql += try selectPrimaryKey.requestSQL(subqueryContext)
sql += ")"
return try makeStatement(db, sql: sql, arguments: context.arguments, returning: selection)
}
/// Returns an `UPDATE` statement, with `RETURNING` clause if `selection`
/// is not empty.
///
/// Returns nil if assignments is empty.
func makeUpdateStatement(
_ db: Database,
conflictResolution: Database.ConflictResolution,
assignments: [ColumnAssignment],
selection: [any SQLSelectable] = [])
throws -> Statement?
{
switch try grouping(db) {
case .none:
guard relation.joins.isEmpty else {
return try makeTrivialUpdateStatement(
db,
conflictResolution: conflictResolution,
assignments: assignments,
selection: selection)
}
let context = SQLGenerationContext(db, aliases: relation.allAliases, ctes: relation.ctes)
var sql = try commonTableExpressionsPrefix(context)
sql += "UPDATE "
if conflictResolution != .abort {
sql += "OR \(conflictResolution.rawValue) "
}
sql += try relation.source.sql(context)
let updateSQL = try assignments
.compactMap { try $0.sql(context) }
.joined(separator: ", ")
if updateSQL.isEmpty {
return nil
}
sql += " SET \(updateSQL)"
if let filter = try relation.filterPromise?.resolve(db) {
sql += " WHERE "
sql += try filter.sql(context)
}
if let limit = relation.limit {
let orderings = try relation.ordering.resolve(db)
if !orderings.isEmpty {
sql += " ORDER BY "
sql += try orderings
.map { try $0.sql(context) }
.joined(separator: ", ")
}
sql += " LIMIT " + limit.sql
}
return try makeStatement(db, sql: sql, arguments: context.arguments, returning: selection)
case .unique:
return try makeTrivialUpdateStatement(
db,
conflictResolution: conflictResolution,
assignments: assignments,
selection: selection)
case .nonUnique:
// Programmer error
fatalError("Can't update query with GROUP BY clause")
}
}
/// UPDATE table SET ... WHERE id IN (SELECT id FROM table ...)
/// UPDATE table SET ... WHERE id IN (SELECT id FROM table ...) RETURNING ...
/// Returns nil if assignments is empty
private func makeTrivialUpdateStatement(
_ db: Database,
conflictResolution: Database.ConflictResolution,
assignments: [ColumnAssignment],
selection: [any SQLSelectable])
throws -> Statement?
{
let tableName = relation.source.tableName
let alias = TableAlias(tableName: tableName)
let context = SQLGenerationContext(db, aliases: [alias])
let subqueryContext = context.subqueryContext(aliases: relation.allAliases, ctes: relation.ctes)
let primaryKey = SQLExpression.fastPrimaryKey
let selectPrimaryKey = self.with {
$0.relation = $0.relation.selectOnly([.expression(primaryKey)])
}
// UPDATE table...
var sql = "UPDATE "
if conflictResolution != .abort {
sql += "OR \(conflictResolution.rawValue) "
}
sql += tableName.quotedDatabaseIdentifier
// SET column = value...
let updateSQL = try assignments
.compactMap { try $0.sql(context) }
.joined(separator: ", ")
if updateSQL.isEmpty {
return nil
}
sql += " SET \(updateSQL)"
// WHERE id IN (SELECT id FROM ...)
sql += " WHERE "
sql += try alias[primaryKey].sql(context)
sql += " IN ("
sql += try selectPrimaryKey.requestSQL(subqueryContext)
sql += ")"
return try makeStatement(db, sql: sql, arguments: context.arguments, returning: selection)
}
// Support for the RETURNING clause
private func makeStatement(
_ db: Database,
sql: String,
arguments: StatementArguments,
returning selection: [any SQLSelectable])
throws -> Statement
{
if selection.isEmpty {
let statement = try db.makeStatement(sql: sql)
statement.arguments = arguments
return statement
} else {
let context = SQLGenerationContext(db)
var sql = sql
var arguments = arguments
sql += " RETURNING "
sql += try selection
.map { try $0.sqlSelection.sql(context) }
.joined(separator: ", ")
arguments += context.arguments
let statement = try db.makeStatement(sql: sql)
statement.arguments = arguments
return statement
}
}
private func commonTableExpressionsPrefix(_ context: SQLGenerationContext) throws -> String {
if relation.ctes.isEmpty {
return ""
}
var sql = "WITH "
if relation.ctes.values.contains(where: \.isRecursive) {
sql += "RECURSIVE "
}
sql += try relation.ctes
.map { tableName, cte in
var columnsSQL = ""
if let columns = cte.columns, !columns.isEmpty {
columnsSQL = "("
columnsSQL += columns
.map(\.quotedDatabaseIdentifier)
.joined(separator: ", ")
columnsSQL += ")"
}
let cteContext = context.subqueryContext()
let subquerySQL = try cte.sqlSubquery.sql(cteContext)
return "\(tableName.quotedDatabaseIdentifier)\(columnsSQL) AS (\(subquerySQL))"
}
.joined(separator: ", ")
sql += " "
return sql
}
/// Informs about the query grouping
private enum GroupingInfo {
/// No grouping at all: SELECT ... FROM player
case none
/// Grouped by unique key: SELECT ... FROM player GROUP BY id
case unique
/// Grouped by some non-unique columnns: SELECT ... FROM player GROUP BY teamId
case nonUnique
}
/// Informs about the query grouping
private func grouping(_ db: Database) throws -> GroupingInfo {
// Empty group clause: no grouping
// SELECT * FROM player
guard let groupExpressions = try relation.groupPromise?.resolve(db), groupExpressions.isEmpty == false else {
return .none
}
// Grouping something which is not a table: assume non unique grouping.
let tableName = relation.source.tableName
guard try db.tableExists(tableName) else { // skip views
return .nonUnique
}
var groupingColumns: Set<String> = []
for expression in groupExpressions {
guard let column = try expression.column(db, for: relation.source.alias, acceptsBijection: true) else {
// Grouping by something which is not a column: assume non
// unique grouping.
return .nonUnique
}
groupingColumns.insert(column)
}
// Grouping by some column(s) which are unique
// SELECT * FROM player GROUP BY id
if try db.table(tableName, hasUniqueKey: groupingColumns) {
return .unique
}
// Grouping by some column(s) which are not unique
// SELECT * FROM player GROUP BY score
return .nonUnique
}
/// Returns the row adapter which presents the fetched rows according to the
/// tree of joined relations.
///
/// The adapter is nil for queries without any included relation,
/// because the fetched rows don't need any processing:
///
/// // SELECT * FROM book
/// let request = Book.all()
/// for row in try Row.fetchAll(db, request) {
/// row // [id:1, title:"Moby-Dick"]
/// let book = try Book(row: row)
/// }
///
/// But as soon as the selection includes columns of a included relation,
/// we need an adapter:
///
/// // SELECT book.*, author.* FROM book JOIN author ON author.id = book.authorId
/// let request = Book.including(required: Book.author)
/// for row in try Row.fetchAll(db, request) {
/// row // [id:1, title:"Moby-Dick"]
/// let book = try Book(row: row)
///
/// row.scopes["author"] // [id:12, name:"Herman Melville"]
/// let author: Author = row["author"]
/// }
private func rowAdapter(_ context: SQLGenerationContext) throws -> (any RowAdapter)? {
try relation.rowAdapter(context, fromIndex: 0, rootRelation: true)?.adapter
}
}
/// To generate SQL, we need a "qualified" relation, where all tables,
/// expressions, etc, are qualified with table aliases.
///
/// All those aliases let us disambiguate tables at the SQL level, and
/// prefix columns names. For example, the following request...
///
/// Book.filter(Column("kind") == Book.Kind.novel)
/// .including(optional: Book.author)
/// .including(optional: Book.translator)
/// .annotated(with: Book.awards.count)
///
/// ... generates the following SQL, where all identifiers are correctly
/// disambiguated and qualified:
///
/// SELECT book.*, person1.*, person2.*, COUNT(DISTINCT award.id)
/// FROM book
/// LEFT JOIN person person1 ON person1.id = book.authorId
/// LEFT JOIN person person2 ON person2.id = book.translatorId
/// LEFT JOIN award ON award.bookId = book.id
/// GROUP BY book.id
/// WHERE book.kind = 'novel'
///
/// `SQLQualifiedRelation` contains the following information:
///
/// WITH ... -- ctes
/// SELECT ... -- selectionPromise
/// FROM ... -- source
/// JOIN ... -- joins
/// WHERE ... -- filterPromise
/// GROUP BY ... -- groupPromise
/// HAVING ... -- havingExpressionPromise
/// ORDER BY ... -- ordering
/// LIMIT ... -- limit
private struct SQLQualifiedRelation {
/// All aliases, including aliases of joined relations
var allAliases: [TableAlias] {
joins.reduce(into: [source.alias].compactMap { $0 }) {
$0.append(contentsOf: $1.value.relation.allAliases)
}
}
/// The source
let source: SQLQualifiedSource
/// The selection from source, not including selection of joined relations
private var sourceSelectionPromise: DatabasePromise<[SQLSelection]>
var isDistinct: Bool
/// The full selection, including selection of joined relations
var selectionPromise: DatabasePromise<[SQLSelection]> {
DatabasePromise { db in
let selection = try sourceSelectionPromise.resolve(db)
return try joins.values.reduce(into: selection) { selection, join in
let joinedSelection = try join.relation.selectionPromise.resolve(db)
selection.append(contentsOf: joinedSelection)
}
}
}
let filterPromise: DatabasePromise<SQLExpression>?
/// The ordering of source, not including ordering of joined relations
private let sourceOrdering: SQLRelation.Ordering
/// The full ordering, including orderings of joined relations
var ordering: SQLRelation.Ordering {
joins.reduce(sourceOrdering) {
$0.appending($1.value.relation.ordering)
}
}
private(set) var joins: OrderedDictionary<String, SQLQualifiedJoin>
let groupPromise: DatabasePromise<[SQLExpression]>?
let havingExpressionPromise: DatabasePromise<SQLExpression>?
let limit: SQLLimit?
let ctes: OrderedDictionary<String, SQLCTE>
init(_ relation: SQLRelation) {
// Qualify the source, so that it be disambiguated with an SQL alias
// if needed (when a select query uses the same table several times).
// This disambiguation job will be actually performed by
// SQLGenerationContext, when the SQLSelectQueryGenerator which owns
// this SQLQualifiedRelation generates SQL.
source = SQLQualifiedSource(relation.source)
// Qualify all selection, filter, etc, so that all identifiers
// can be correctly disambiguated and qualified.
let sourceAlias = source.alias
sourceSelectionPromise = relation.selectionPromise.map {
$0.map { $0.qualified(with: sourceAlias) }
}
filterPromise = relation.filterPromise.map {
$0.map { $0.qualified(with: sourceAlias) }
}
sourceOrdering = relation.ordering.qualified(with: sourceAlias)
groupPromise = relation.groupPromise?.map {
$0.map { $0.qualified(with: sourceAlias) }
}
havingExpressionPromise = relation.havingExpressionPromise.map {
$0.map { $0.qualified(with: sourceAlias) }
}
// Turns relation children into joins. `including(all:)` children are
// discarded on the way.
joins = relation.children.compactMapValues { SQLQualifiedJoin($0) }
// Copy other flags
limit = relation.limit
isDistinct = relation.isDistinct
ctes = relation.allCTEs
}
/// See SQLQueryGenerator.rowAdapter(_:)
///
/// - parameter startIndex: The index of the leftmost selected column of
/// this relation in a full SQL query.
/// - parameter rootRelation: True iff the relation is at the root of a
/// SQLQueryGenerator (as opposed to the joined relations).
/// - returns: An optional tuple made of a RowAdapter and the index past the
/// rightmost selected column of this relation. Nil is returned if this
/// relations does not need any row adapter.
func rowAdapter(
_ context: SQLGenerationContext,
fromIndex startIndex: Int,
rootRelation: Bool) throws
-> (adapter: any RowAdapter, endIndex: Int)?
{
// Root relation && no join => no need for any adapter
if rootRelation && joins.isEmpty {
return nil
}
// The number of columns in source selection. Columns selected by joined
// relations are appended after.
let sourceSelectionWidth = try sourceSelectionPromise.resolve(context.db).columnCount(context)
// Recursively build adapters for each joined relation with a selection.
// Name them according to the join keys.
var endIndex = startIndex + sourceSelectionWidth
var scopes: [String: any RowAdapter] = [:]
for (key, join) in joins {
if let (joinAdapter, joinEndIndex) = try join
.relation
.rowAdapter(context, fromIndex: endIndex, rootRelation: false)
{
scopes[key] = joinAdapter
endIndex = joinEndIndex
}
}
// (Root relation || empty selection) && no included relation => no need for any adapter
if (rootRelation || sourceSelectionWidth == 0) && scopes.isEmpty {
return nil
}
// Build a RangeRowAdapter extended with the adapters of joined relations.
//
// // SELECT book.*, author.* FROM book JOIN author ON author.id = book.authorId
// let request = Book.including(required: Book.author)
// for row in try Row.fetchAll(db, request) {
//
// The RangeRowAdapter hides the columns appended by joined relations:
//
// row // [id:1, title:"Moby-Dick"]
// let book = try Book(row: row)
//
// Scopes give access to those joined relations:
//
// row.scopes["author"] // [id:12, name:"Herman Melville"]
// let author: Author = row["author"]
// }
let rangeAdapter = RangeRowAdapter(startIndex ..< (startIndex + sourceSelectionWidth))
let adapter = rangeAdapter.addingScopes(scopes)
return (adapter: adapter, endIndex: endIndex)
}
/// Sets the selection, removes all selections from joins, and clears the
/// `isDistinct` flag.
func selectOnly(_ selection: [SQLSelection]) -> Self {
let qualifiedSelection = selection.map {
$0.qualified(with: source.alias)
}
return with {
$0.sourceSelectionPromise = DatabasePromise(value: qualifiedSelection)
$0.isDistinct = false
$0.joins = $0.joins.mapValues { join in
join.with {
$0.relation = $0.relation.selectOnly([])
}
}
}
}
}
extension SQLQualifiedRelation: Refinable { }
/// A "qualified" source, where all tables are identified with a table alias.
private struct SQLQualifiedSource {
var tableName: String
var alias: TableAlias
init(_ source: SQLSource) {
self.tableName = source.tableName
self.alias = source.alias ?? TableAlias(tableName: source.tableName)
assert(alias.tableName == tableName)
}
func sql(_ context: SQLGenerationContext) throws -> String {
if let aliasName = context.aliasName(for: alias) {
return "\(tableName.quotedDatabaseIdentifier) \(aliasName.quotedDatabaseIdentifier)"
} else {
return "\(tableName.quotedDatabaseIdentifier)"
}
}
}
/// A "qualified" join, where all tables are identified with a table alias.
private struct SQLQualifiedJoin: Refinable {
enum Kind: String {
case leftJoin = "LEFT JOIN"
case innerJoin = "JOIN"
init?(_ kind: SQLRelation.Child.Kind) {
switch kind {
case .oneRequired:
self = .innerJoin
case .oneOptional:
self = .leftJoin
case .all, .bridge:
// Eager loading of to-many associations is not implemented with joins
return nil
}
}
}
var kind: Kind
var condition: SQLAssociationCondition
var relation: SQLQualifiedRelation
init?(_ child: SQLRelation.Child) {
guard let kind = Kind(child.kind) else {
return nil
}
self.kind = kind
self.condition = child.condition
self.relation = SQLQualifiedRelation(child.relation)
}
func sql(_ context: SQLGenerationContext, leftAlias: TableAlias) throws -> String {
try sql(context, leftAlias: leftAlias, allowingInnerJoin: true)
}
private func sql(
_ context: SQLGenerationContext,
leftAlias: TableAlias,
allowingInnerJoin allowsInnerJoin: Bool)
throws -> String
{
var allowsInnerJoin = allowsInnerJoin
switch self.kind {
case .innerJoin:
guard allowsInnerJoin else {
// TODO: chainOptionalRequired
//
// When we eventually implement this, make sure we both support:
// - joining(optional: assoc.joining(required: ...))
// - having(assoc.joining(required: ...).isEmpty)
fatalError("Not implemented: chaining a required association behind an optional association")
}
case .leftJoin:
allowsInnerJoin = false
}
// JOIN table...
var sql = try "\(kind.rawValue) \(relation.source.sql(context))"
// ... ON <join conditions> AND <other filters>
let rightAlias = relation.source.alias
var conditions: [SQLExpression] = []
if let expression = try condition.joinExpression(context.db,
leftAlias: leftAlias,
rightAlias: rightAlias)
{
conditions.append(expression)
}
if let filter = try relation.filterPromise?.resolve(context.db) {
conditions.append(filter.qualified(with: rightAlias))
}
if conditions.isEmpty == false {
sql += " ON "
sql += try conditions
.joined(operator: .and)
.sql(context)
}
for (_, join) in relation.joins {
// Right becomes left as we dig further
sql += try " \(join.sql(context, leftAlias: rightAlias, allowingInnerJoin: allowsInnerJoin))"
}
return sql
}
}
@@ -0,0 +1,84 @@
struct SQLTableAlterationGenerator {
private enum TableAlterationKind {
case addColumn(SQLColumnGenerator)
case addIndex(SQLIndexGenerator)
case renameColumn(old: String, new: String)
case dropColumn(String)
}
private var name: String
private var alterations: [TableAlterationKind] = []
func sql(_ db: Database) throws -> String {
var statements: [String] = []
for alteration in alterations {
switch alteration {
case let .addColumn(column):
var chunks: [String] = []
chunks.append("ALTER TABLE")
chunks.append(name.quotedDatabaseIdentifier)
chunks.append("ADD COLUMN")
let sql = try column.sql(db, tableName: name, primaryKeyColumns: {
try db.primaryKey(name).columnInfos.map { columnInfos in
columnInfos.map { SQLColumnDescriptor($0) }
}
})
chunks.append(sql)
let statement = chunks.joined(separator: " ")
statements.append(statement)
case let .addIndex(index):
try statements.append(index.sql(db))
case let .renameColumn(oldName, newName):
var chunks: [String] = []
chunks.append("ALTER TABLE")
chunks.append(name.quotedDatabaseIdentifier)
chunks.append("RENAME COLUMN")
chunks.append(oldName.quotedDatabaseIdentifier)
chunks.append("TO")
chunks.append(newName.quotedDatabaseIdentifier)
let statement = chunks.joined(separator: " ")
statements.append(statement)
case let .dropColumn(column):
var chunks: [String] = []
chunks.append("ALTER TABLE")
chunks.append(name.quotedDatabaseIdentifier)
chunks.append("DROP COLUMN")
chunks.append(column.quotedDatabaseIdentifier)
let statement = chunks.joined(separator: " ")
statements.append(statement)
}
}
return statements.joined(separator: "; ")
}
}
extension SQLTableAlterationGenerator {
init(_ tableAlteration: TableAlteration) {
self.name = tableAlteration.name
self.alterations = []
for alteration in tableAlteration.alterations {
switch alteration {
case let .add(column):
alterations.append(.addColumn(.columnDefinition(column)))
if let indexDefinition = column.indexDefinition(in: name) {
alterations.append(.addIndex(SQLIndexGenerator(index: indexDefinition)))
}
case let .addColumnLiteral(sql):
alterations.append(.addColumn(.columnLiteral(sql)))
case let .rename(old: oldName, new: newName):
alterations.append(.renameColumn(old: oldName, new: newName))
case let .drop(column):
alterations.append(.dropColumn(column))
}
}
}
}
@@ -0,0 +1,476 @@
struct SQLTableGenerator {
var name: String
var options: TableOptions
var columnGenerators: [SQLColumnGenerator]
/// Used for auto-referencing foreign keys: we need to know the columns
/// of the primary key before they exist in the database schema, hence
/// the name of "forward" primary key columns.
///
/// If nil, the primary key is the hidden rowID.
var forwardPrimaryKeyColumns: [SQLColumnDescriptor]?
var primaryKeyConstraint: KeyConstraint?
var uniqueKeyConstraints: [KeyConstraint]
var foreignKeyConstraints: [SQLForeignKeyConstraint]
var checkConstraints: [SQLExpression]
var literalConstraints: [SQL]
var indexGenerators: [SQLIndexGenerator]
struct KeyConstraint {
var columns: [String]
var conflictResolution: Database.ConflictResolution?
}
func sql(_ db: Database) throws -> String {
var statements: [String] = []
do {
var chunks: [String] = []
chunks.append("CREATE")
if options.contains(.temporary) {
chunks.append("TEMPORARY")
}
chunks.append("TABLE")
if options.contains(.ifNotExists) {
chunks.append("IF NOT EXISTS")
}
chunks.append(name.quotedDatabaseIdentifier)
do {
var items: [String] = []
try items.append(contentsOf: columnGenerators.map {
try $0.sql(db, tableName: name, primaryKeyColumns: { forwardPrimaryKeyColumns })
})
if let constraint = primaryKeyConstraint {
var chunks: [String] = []
chunks.append("PRIMARY KEY")
chunks.append("(\(constraint.columns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")))")
if let conflictResolution = constraint.conflictResolution {
chunks.append("ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
items.append(chunks.joined(separator: " "))
}
for constraint in uniqueKeyConstraints {
var chunks: [String] = []
chunks.append("UNIQUE")
chunks.append("(\(constraint.columns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")))")
if let conflictResolution = constraint.conflictResolution {
chunks.append("ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
items.append(chunks.joined(separator: " "))
}
for constraint in foreignKeyConstraints {
var chunks: [String] = []
chunks.append("FOREIGN KEY")
chunks.append("(\(constraint.columns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")))")
chunks.append("REFERENCES")
if let destinationColumns = constraint.destinationColumns {
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)(\
\(destinationColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", "))\
)
""")
} else if constraint.destinationTable.lowercased() == name.lowercased() {
// autoreference
let forwardPrimaryKeyColumns = forwardPrimaryKeyColumns ?? [.rowID]
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)(\
\(forwardPrimaryKeyColumns.map(\.name.quotedDatabaseIdentifier).joined(separator: ", "))\
)
""")
} else {
let primaryKey = try db.primaryKey(constraint.destinationTable)
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)(\
\(primaryKey.columns.map(\.quotedDatabaseIdentifier).joined(separator: ", "))\
)
""")
}
if let deleteAction = constraint.deleteAction {
chunks.append("ON DELETE")
chunks.append(deleteAction.rawValue)
}
if let updateAction = constraint.updateAction {
chunks.append("ON UPDATE")
chunks.append(updateAction.rawValue)
}
if constraint.isDeferred {
chunks.append("DEFERRABLE INITIALLY DEFERRED")
}
items.append(chunks.joined(separator: " "))
}
for checkExpression in checkConstraints {
var chunks: [String] = []
try chunks.append("CHECK (\(checkExpression.quotedSQL(db)))")
items.append(chunks.joined(separator: " "))
}
for literal in literalConstraints {
let context = SQLGenerationContext(db, argumentsSink: .literalValues)
try items.append(literal.sql(context))
}
chunks.append("(\(items.joined(separator: ", ")))")
}
var tableOptions: [String] = []
#if GRDBCUSTOMSQLITE || GRDBCIPHER
if options.contains(.strict) {
tableOptions.append("STRICT")
}
#else
if #available(iOS 15.4, macOS 12.4, tvOS 15.4, watchOS 8.5, *) { // SQLite 3.37+
if options.contains(.strict) {
tableOptions.append("STRICT")
}
}
#endif
if options.contains(.withoutRowID) {
tableOptions.append("WITHOUT ROWID")
}
if !tableOptions.isEmpty {
chunks.append(tableOptions.joined(separator: ", "))
}
statements.append(chunks.joined(separator: " "))
}
let indexStatements = try indexGenerators.map { try $0.sql(db) }
statements.append(contentsOf: indexStatements)
return statements.joined(separator: "; ")
}
private struct ForeignKeyGenerator {
var columnNames: [String]
var columnGenerators: [SQLColumnGenerator]
var foreignKeyConstraint: SQLForeignKeyConstraint?
var indexGenerator: SQLIndexGenerator?
}
}
extension SQLTableGenerator {
init(_ db: Database, table: TableDefinition) throws {
var indexOptions: IndexOptions = []
if table.options.contains(.ifNotExists) { indexOptions.insert(.ifNotExists) }
func makeKeyConstraint(
_ db: Database,
constraint: TableDefinition.KeyConstraint,
forwardPrimaryKey: SQLPrimaryKeyDescriptor)
throws -> SQLTableGenerator.KeyConstraint
{
try SQLTableGenerator.KeyConstraint(
columns: constraint.components.flatMap { component -> [String] in
switch component {
case let .columnName(columnName):
return [columnName]
case let .columnDefinition(column):
return [column.name]
case let .foreignKeyDefinition(foreignKey):
return try Self.makeForeignKeyGenerator(
db, foreignKey: foreignKey,
originTable: table.name,
forwardPrimaryKey: forwardPrimaryKey,
indexOptions: indexOptions).columnNames
}
},
conflictResolution: constraint.conflictResolution)
}
var forwardPrimaryKeyColumns: [SQLColumnDescriptor]?
if let primaryKeyConstraint = table.primaryKeyConstraint {
forwardPrimaryKeyColumns = try Self.forwardPrimaryKeyColumns(
db, primaryKeyConstraint: primaryKeyConstraint,
originTable: table.name)
} else {
for component in table.columnComponents {
if case let .columnDefinition(column) = component, column.primaryKey != nil {
forwardPrimaryKeyColumns = [SQLColumnDescriptor(column)]
break
}
}
}
let forwardPrimaryKey = SQLPrimaryKeyDescriptor(
tableName: table.name,
primaryKeyColumns: forwardPrimaryKeyColumns)
var columnGenerators: [SQLColumnGenerator] = []
var foreignKeyConstraints: [SQLForeignKeyConstraint] = []
var indexGenerators: [SQLIndexGenerator] = []
for component in table.columnComponents {
switch component {
case let .columnDefinition(column):
columnGenerators.append(.columnDefinition(column))
case let .columnLiteral(sql):
columnGenerators.append(.columnLiteral(sql))
case let .foreignKeyDefinition(foreignKey):
let fkGenerator = try Self.makeForeignKeyGenerator(
db, foreignKey: foreignKey,
originTable: table.name,
forwardPrimaryKey: forwardPrimaryKey,
indexOptions: indexOptions)
columnGenerators.append(contentsOf: fkGenerator.columnGenerators)
if let indexGenerator = fkGenerator.indexGenerator {
indexGenerators.append(indexGenerator)
}
if let foreignKeyConstraint = fkGenerator.foreignKeyConstraint {
foreignKeyConstraints.append(foreignKeyConstraint)
}
case let .foreignKeyConstraint(constraint):
foreignKeyConstraints.append(constraint)
}
}
for columnGenerator in columnGenerators {
if case let .columnDefinition(column) = columnGenerator,
let index = column.indexDefinition(in: table.name, options: indexOptions)
{
indexGenerators.append(SQLIndexGenerator(index: index))
}
}
try self.init(
name: table.name,
options: table.options,
columnGenerators: columnGenerators,
forwardPrimaryKeyColumns: forwardPrimaryKeyColumns,
primaryKeyConstraint: table.primaryKeyConstraint.map {
try makeKeyConstraint(db, constraint: $0, forwardPrimaryKey: forwardPrimaryKey)
},
uniqueKeyConstraints: table.uniqueKeyConstraints.map {
try makeKeyConstraint(db, constraint: $0, forwardPrimaryKey: forwardPrimaryKey)
},
foreignKeyConstraints: foreignKeyConstraints,
checkConstraints: table.checkConstraints,
literalConstraints: table.literalConstraints,
indexGenerators: indexGenerators)
}
private static func forwardPrimaryKeyColumns(
_ db: Database,
primaryKeyConstraint: TableDefinition.KeyConstraint,
originTable: String)
throws -> [SQLColumnDescriptor]?
{
var forwardPrimaryKeyColumns: [SQLColumnDescriptor] = []
for component in primaryKeyConstraint.components {
switch component {
case let .columnDefinition(column):
forwardPrimaryKeyColumns.append(SQLColumnDescriptor(column))
case let .foreignKeyDefinition(foreignKey):
let fkGenerator = try makeForeignKeyGenerator(
db, foreignKey: foreignKey,
originTable: originTable,
forwardPrimaryKey: nil, // not known yet, since we're building it
indexOptions: [])
for columnGenerator in fkGenerator.columnGenerators {
switch columnGenerator {
case let .columnDefinition(column):
forwardPrimaryKeyColumns.append(SQLColumnDescriptor(column))
case .columnLiteral:
// Unknown column name
return nil
}
}
case let .columnName(name):
forwardPrimaryKeyColumns.append(SQLColumnDescriptor(name: name, type: nil))
}
}
return forwardPrimaryKeyColumns
}
private static func makeForeignKeyGenerator(
_ db: Database,
foreignKey: ForeignKeyDefinition,
originTable: String,
forwardPrimaryKey: SQLPrimaryKeyDescriptor?,
indexOptions: IndexOptions)
throws -> ForeignKeyGenerator
{
let destinationPrimaryKey: SQLPrimaryKeyDescriptor
if let table = foreignKey.table {
if let forwardPrimaryKey,
originTable.lowercased() == table.lowercased()
{
// autoreference
destinationPrimaryKey = forwardPrimaryKey
} else {
destinationPrimaryKey = try foreignKey.primaryKey(db)
}
} else {
if let forwardPrimaryKey,
originTable.singularized.lowercased() == foreignKey.name.singularized.lowercased()
{
// autoreference
destinationPrimaryKey = forwardPrimaryKey
} else {
destinationPrimaryKey = try foreignKey.primaryKey(db)
}
}
guard let primaryKeyColumns = destinationPrimaryKey.primaryKeyColumns else {
// Destination table has an hidden rowID primary key
let columnName = foreignKey.name + "Id"
let column = ColumnDefinition(name: columnName, type: .integer).references(
destinationPrimaryKey.tableName,
onDelete: foreignKey.deleteAction,
onUpdate: foreignKey.updateAction,
deferred: foreignKey.isDeferred)
if let notNullConflictResolution = foreignKey.notNullConflictResolution {
column.notNull(onConflict: notNullConflictResolution)
}
switch foreignKey.indexing {
case nil:
break
case .index:
column.indexed()
case .unique:
column.unique()
}
return ForeignKeyGenerator(
columnNames: [columnName],
columnGenerators: [SQLColumnGenerator.columnDefinition(column)],
foreignKeyConstraint: nil,
indexGenerator: nil)
}
assert(!primaryKeyColumns.isEmpty)
let columnNames = primaryKeyColumns.map {
foreignKey.name + $0.name.uppercasingFirstCharacter
}
if primaryKeyColumns.count == 1 {
// Destination table has a single column primary key
let pkColumn = primaryKeyColumns[0]
let columnName = columnNames[0]
let column = ColumnDefinition(name: columnName, type: pkColumn.type).references(
destinationPrimaryKey.tableName,
column: pkColumn.name,
onDelete: foreignKey.deleteAction,
onUpdate: foreignKey.updateAction,
deferred: foreignKey.isDeferred)
if let notNullConflictResolution = foreignKey.notNullConflictResolution {
column.notNull(onConflict: notNullConflictResolution)
}
switch foreignKey.indexing {
case nil:
break
case .index:
column.indexed()
case .unique:
column.unique()
}
return ForeignKeyGenerator(
columnNames: [columnName],
columnGenerators: [SQLColumnGenerator.columnDefinition(column)],
foreignKeyConstraint: nil,
indexGenerator: nil)
} else {
// Destination table has a composite primary key
let columnGenerators = zip(primaryKeyColumns, columnNames).map { pkColumn, columnName in
let column = ColumnDefinition(name: columnName, type: pkColumn.type)
if let notNullConflictResolution = foreignKey.notNullConflictResolution {
column.notNull(onConflict: notNullConflictResolution)
}
return SQLColumnGenerator.columnDefinition(column)
}
let foreignKeyConstraint = SQLForeignKeyConstraint(
columns: columnNames,
destinationTable: destinationPrimaryKey.tableName,
destinationColumns: nil,
deleteAction: foreignKey.deleteAction,
updateAction: foreignKey.updateAction,
isDeferred: foreignKey.isDeferred)
let indexGenerator: SQLIndexGenerator?
switch foreignKey.indexing {
case nil:
indexGenerator = nil
case .index:
indexGenerator = SQLIndexGenerator(
name: Database.defaultIndexName(on: originTable, columns: columnNames),
table: originTable,
expressions: columnNames.map { .column($0) },
options: indexOptions,
condition: nil)
case .unique:
indexGenerator = SQLIndexGenerator(
name: Database.defaultIndexName(on: originTable, columns: columnNames),
table: originTable,
expressions: columnNames.map { .column($0) },
options: indexOptions.union([.unique]),
condition: nil)
}
return ForeignKeyGenerator(
columnNames: columnNames,
columnGenerators: columnGenerators,
foreignKeyConstraint: foreignKeyConstraint,
indexGenerator: indexGenerator)
}
}
}
struct SQLColumnDescriptor {
static let rowID = SQLColumnDescriptor(name: Column.rowID.name, type: .integer)
var name: String
var type: Database.ColumnType?
}
extension SQLColumnDescriptor {
init(_ column: ColumnInfo) {
self.init(
name: column.name,
type: column.columnType)
}
init(_ column: ColumnDefinition) {
self.init(name: column.name, type: column.type)
}
}
struct SQLForeignKeyConstraint {
var columns: [String]
var destinationTable: String
var destinationColumns: [String]?
var deleteAction: Database.ForeignKeyAction?
var updateAction: Database.ForeignKeyAction?
var isDeferred: Bool
}
struct SQLPrimaryKeyDescriptor {
/// The name of the forward-declared table
var tableName: String
/// If nil, the primary key is the hidden rowID.
var primaryKeyColumns: [SQLColumnDescriptor]?
}
extension SQLPrimaryKeyDescriptor {
static func find(_ db: Database, table: String) throws -> Self {
let columnInfos = try db.primaryKey(table).columnInfos
return SQLPrimaryKeyDescriptor(
tableName: table,
primaryKeyColumns: columnInfos.map { columnInfos in
columnInfos.map { SQLColumnDescriptor($0) }
})
}
}
@@ -0,0 +1,328 @@
extension SQLInterpolation {
// MARK: - TableRecord
/// Appends the table name of the record type.
///
/// // SELECT * FROM player
/// let request: SQLRequest<Player> = "SELECT * FROM \(Player.self)"
public mutating func appendInterpolation(_ table: (some TableRecord).Type) {
appendLiteral(table.databaseTableName.quotedDatabaseIdentifier)
}
/// Appends the table name of the record type.
///
/// // SELECT * FROM player
/// let request: SQLRequest<Player> = "SELECT * FROM \(Player.self)"
@_disfavoredOverload
public mutating func appendInterpolation(_ table: any TableRecord.Type) {
appendLiteral(table.databaseTableName.quotedDatabaseIdentifier)
}
/// Appends the table name.
///
/// // SELECT * FROM player
/// let playerTable = Table("player")
/// let request: SQLRequest<Player> = "SELECT * FROM \(playerTable)"
@_disfavoredOverload
public mutating func appendInterpolation<T>(_ table: Table<T>) {
appendLiteral(table.tableName.quotedDatabaseIdentifier)
}
/// Appends the table name of the record.
///
/// // INSERT INTO player ...
/// let player: Player = ...
/// let request: SQLRequest<Player> = "INSERT INTO \(tableOf: player) ..."
public mutating func appendInterpolation(tableOf record: some TableRecord) {
appendInterpolation(type(of: record))
}
/// Appends a quoted identifier.
///
/// // INSERT INTO "group" ...
/// let tableName = "group"
/// let request: SQLRequest<Player> = "INSERT INTO \(identifier: tableName) ..."
public mutating func appendInterpolation(identifier: String) {
appendLiteral(identifier.quotedDatabaseIdentifier)
}
/// Appends the table name of the record.
///
/// // INSERT INTO player ...
/// let player: Player = ...
/// let request: SQLRequest<Player> = "INSERT INTO \(tableOf: player) ..."
@_disfavoredOverload
public mutating func appendInterpolation(tableOf record: any TableRecord) {
appendInterpolation(type(of: record))
}
/// Appends the selection of the record type.
///
/// // SELECT * FROM player
/// let player: Player = ...
/// let request: SQLRequest<Player> = "SELECT \(columnsOf: Player.self) FROM player"
///
/// // SELECT p.* FROM player p
/// let player: Player = ...
/// let request: SQLRequest<Player> = "SELECT \(columnsOf: Player.self, tableAlias: "p") FROM player p"
public mutating func appendInterpolation(columnsOf recordType: (some TableRecord).Type, tableAlias: String? = nil) {
let alias = TableAlias(name: tableAlias ?? recordType.databaseTableName)
elements.append(contentsOf: recordType.databaseSelection
.map { CollectionOfOne(.selection($0.sqlSelection.qualified(with: alias))) }
.joined(separator: CollectionOfOne(.sql(", "))))
}
// MARK: - SQLSelectable
/// Appends the selectable SQL.
///
/// // SELECT * FROM player
/// let request: SQLRequest<Player> = """
/// SELECT \(AllColumns()) FROM player
/// """
public mutating func appendInterpolation(_ selection: some SQLSelectable) {
elements.append(.selection(selection.sqlSelection))
}
/// Appends the selectable SQL, or NULL if it is nil.
///
/// // SELECT * FROM player
/// let request: SQLRequest<Player> = """
/// SELECT \(AllColumns()) FROM player
/// """
@_disfavoredOverload
public mutating func appendInterpolation(_ selection: (any SQLSelectable)?) {
if let selection {
elements.append(.selection(selection.sqlSelection))
} else {
appendLiteral("NULL")
}
}
// MARK: - SQLOrderingTerm
/// Appends the ordering SQL.
///
/// // SELECT name FROM player ORDER BY name DESC
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player ORDER BY \(Column("name").desc)
/// """
public mutating func appendInterpolation(_ orderingTerm: some SQLOrderingTerm) {
elements.append(.ordering(orderingTerm.sqlOrdering))
}
/// Appends the ordering SQL.
///
/// // SELECT name FROM player ORDER BY name DESC
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player ORDER BY \(Column("name").desc)
/// """
@_disfavoredOverload
public mutating func appendInterpolation(_ orderingTerm: any SQLOrderingTerm) {
elements.append(.ordering(orderingTerm.sqlOrdering))
}
// MARK: - SQLExpressible
/// Appends the expression SQL.
///
/// // SELECT name FROM player
/// let request: SQLRequest<String> = """
/// SELECT \(Column("name")) FROM player
/// """
public mutating func appendInterpolation(_ expressible: some SQLExpressible
& SQLSelectable
& SQLOrderingTerm)
{
elements.append(.expression(expressible.sqlExpression))
}
/// Appends the expression SQL, or NULL if it is nil.
///
/// // SELECT name FROM player
/// let request: SQLRequest<String> = """
/// SELECT \(Column("name")) FROM player
/// """
@_disfavoredOverload
public mutating func appendInterpolation(_ expressible: (any SQLExpressible)?) {
if let expressible {
elements.append(.expression(expressible.sqlExpression))
} else {
appendLiteral("NULL")
}
}
// MARK: - CodingKey
/// Appends the name of the coding key.
///
/// // SELECT name FROM player
/// let request: SQLRequest<String> = "
/// SELECT \(CodingKey.name) FROM player
/// """
public mutating func appendInterpolation(_ key: some CodingKey) {
appendInterpolation(Column(key.stringValue))
}
/// Appends the name of the coding key.
///
/// // SELECT name FROM player
/// let request: SQLRequest<String> = "
/// SELECT \(CodingKey.name) FROM player
/// """
public mutating func appendInterpolation(_ key: some CodingKey
& SQLExpressible
& SQLSelectable
& SQLOrderingTerm)
{
appendInterpolation(Column(key.stringValue))
}
/// Appends the name of the coding key.
///
/// // SELECT name FROM player
/// let request: SQLRequest<String> = "
/// SELECT \(CodingKey.name) FROM player
/// """
@_disfavoredOverload
public mutating func appendInterpolation(_ key: any CodingKey) {
appendInterpolation(Column(key.stringValue))
}
// MARK: - FetchRequest
/// Appends the request SQL (not wrapped inside parentheses).
///
/// let subquery = Player.select(max(Column("score")))
/// // or
/// let subQuery: SQLRequest<Int> = "SELECT MAX(score) FROM player"
///
/// // SELECT name FROM player WHERE score = (SELECT MAX(score) FROM player)
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE score = (\(subquery))
/// """
public mutating func appendInterpolation(_ subquery: some SQLSubqueryable
& SQLExpressible
& SQLSelectable
& SQLOrderingTerm)
{
elements.append(.subquery(subquery.sqlSubquery))
}
// MARK: - Sequence
/// Appends a sequence of expressions, wrapped in parentheses.
///
/// // SELECT * FROM player WHERE id IN (?,?,?)
/// let ids = [1, 2, 3]
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE id IN \(ids)
/// """
///
/// If the sequence is empty, an empty subquery is appended:
///
/// // SELECT * FROM player WHERE id IN (SELECT NULL WHERE NULL)
/// let ids: [Int] = []
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE id IN \(ids)
/// """
public mutating func appendInterpolation<S>(_ sequence: S)
where S: Sequence, S.Element: SQLExpressible
{
let e: [SQL.Element] = sequence.map { .expression($0.sqlExpression) }
if e.isEmpty {
appendLiteral("(SELECT NULL WHERE NULL)")
} else {
appendLiteral("(")
elements.append(contentsOf: e.map(CollectionOfOne.init(_:)).joined(separator: CollectionOfOne(.sql(","))))
appendLiteral(")")
}
}
/// Appends a sequence of expressions, wrapped in parentheses.
///
/// // SELECT * FROM player WHERE a IN (b, c + 2)
/// let expressions = [Column("b"), Column("c") + 2]
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE a IN \(expressions)
/// """
///
/// If the sequence is empty, an empty subquery is appended:
///
/// // SELECT * FROM player WHERE a IN (SELECT NULL WHERE NULL)
/// let expressions: [SQLExpression] = []
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE a IN \(expressions)
/// """
public mutating func appendInterpolation<S>(_ sequence: S)
where S: Sequence, S.Element == any SQLExpressible
{
appendInterpolation(sequence.lazy.map(\.sqlExpression))
}
// When a value is both an expression and a sequence of expressions,
// favor the expression side. Use case: Foundation.Data interpolation.
public mutating func appendInterpolation<S>(_ expressible: S)
where S: SQLExpressible, S: Sequence, S.Element: SQLExpressible
{
elements.append(.expression(expressible.sqlExpression))
}
// MARK: - Common Table Expressions
/// Appends the table name of the common table expression.
///
/// // WITH "cte" AS (...) SELECT * FROM "cte"
/// let cte = CommonTableExpression(named: "cte", ...)
/// let request: SQLRequest<Row> = """
/// WITH \(definitionFor: cte) SELECT * FROM \(cte)
/// """
public mutating func appendInterpolation(_ cte: CommonTableExpression<some Any>) {
elements.append(.sql(cte.tableName.quotedDatabaseIdentifier))
}
/// Appends the definition of the common table expression.
///
/// // WITH "cte" AS (...) SELECT * FROM "cte"
/// let cte = CommonTableExpression(named: "cte", ...)
/// let request: SQLRequest<Row> = """
/// WITH \(definitionFor: cte) SELECT * FROM \(cte)
/// """
public mutating func appendInterpolation(definitionFor cte: CommonTableExpression<some Any>) {
elements.append(.sql(cte.tableName.quotedDatabaseIdentifier))
if let columns = cte.cte.columns, !columns.isEmpty {
let columnsSQL = "("
+ columns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")
+ ")"
elements.append(.sql(columnsSQL))
}
elements.append(.sql(" AS ("))
elements.append(.subquery(cte.cte.sqlSubquery))
elements.append(.sql(")"))
}
// MARK: - Collations
/// Appends the name of the collation.
///
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player
/// ORDER BY name COLLATING \(DatabaseCollation.localizedCaseInsensitiveCompare)
/// """
public mutating func appendInterpolation(_ collation: DatabaseCollation) {
elements.append(.sql(collation.name))
}
/// Appends the name of the collation.
///
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player
/// ORDER BY email COLLATING \(.nocase)
/// """
public mutating func appendInterpolation(_ collation: Database.CollationName) {
elements.append(.sql(collation.rawValue))
}
}
@@ -0,0 +1,572 @@
/// Describes a database column.
///
/// You get instances of `ColumnDefinition` when you create or alter a database
/// tables. For example:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("name", .text) // ColumnDefinition
/// }
///
/// try db.alter(table: "player") { t in
/// t.add(column: "score", .integer) // ColumnDefinition
/// }
/// ```
///
/// See ``TableDefinition/column(_:_:)`` and ``TableAlteration/add(column:_:)``.
///
/// Related SQLite documentation:
///
/// - <https://www.sqlite.org/lang_createtable.html>
/// - <https://www.sqlite.org/lang_altertable.html>
///
/// ## Topics
///
/// ### Foreign Keys
///
/// - ``references(_:column:onDelete:onUpdate:deferred:)``
///
/// ### Indexes
///
/// - ``indexed()``
/// - ``unique(onConflict:)``
///
/// ### Default value
///
/// - ``defaults(to:)``
/// - ``defaults(sql:)``
///
/// ### Collations
///
/// - ``collate(_:)-4dljx``
/// - ``collate(_:)-9ywza``
///
/// ### Generated Columns
///
/// - ``generatedAs(_:_:)``
/// - ``generatedAs(sql:_:)``
/// - ``GeneratedColumnQualification``
///
/// ### Other Constraints
///
/// - ``check(_:)``
/// - ``check(sql:)``
/// - ``notNull(onConflict:)``
///
/// ### Sunsetted Methods
///
/// Those are legacy interfaces that are preserved for backwards compatibility.
/// Their use is not recommended.
///
/// - ``primaryKey(onConflict:autoincrement:)``
public final class ColumnDefinition {
enum Indexing {
case index
case unique(Database.ConflictResolution)
}
struct ForeignKeyConstraint {
var destinationTable: String
var destinationColumn: String?
var deleteAction: Database.ForeignKeyAction?
var updateAction: Database.ForeignKeyAction?
var isDeferred: Bool
}
/// The kind of a generated column.
///
/// Related SQLite documentation: <https://sqlite.org/gencol.html#virtual_versus_stored_columns>
public enum GeneratedColumnQualification: Sendable {
/// A `VIRTUAL` generated column.
case virtual
/// A `STORED` generated column.
case stored
}
struct GeneratedColumnConstraint {
var expression: SQLExpression
var qualification: GeneratedColumnQualification
}
let name: String
let type: Database.ColumnType?
var primaryKey: (conflictResolution: Database.ConflictResolution?, autoincrement: Bool)?
var indexing: Indexing?
var notNullConflictResolution: Database.ConflictResolution?
var checkConstraints: [SQLExpression] = []
var foreignKeyConstraints: [ForeignKeyConstraint] = []
var defaultExpression: SQLExpression?
var collationName: String?
var generatedColumnConstraint: GeneratedColumnConstraint?
init(name: String, type: Database.ColumnType?) {
self.name = name
self.type = type
}
/// Adds a primary key constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // id TEXT NOT NULL PRIMARY KEY
/// // )
/// try db.create(table: "player") { t in
/// t.primaryKey("id", .text)
/// }
/// ```
///
/// - important: Make sure you add a not null constraint on your primary key
/// column, as in the above example, or SQLite will allow null values.
/// See <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
/// for more information.
///
/// - warning: This is a legacy interface that is preserved for backwards
/// compatibility. Use of this interface is not recommended: prefer
/// ``TableDefinition/primaryKey(_:_:onConflict:)``
/// instead.
///
/// - parameters:
/// - conflictResolution: An optional ``Database/ConflictResolution``.
/// - autoincrement: If true, the primary key is autoincremented.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func primaryKey(
onConflict conflictResolution: Database.ConflictResolution? = nil,
autoincrement: Bool = false)
-> Self
{
primaryKey = (conflictResolution: conflictResolution, autoincrement: autoincrement)
return self
}
/// Adds a not null constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // name TEXT NOT NULL
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text).notNull()
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#notnullconst>
///
/// - parameter conflictResolution: An optional ``Database/ConflictResolution``.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func notNull(onConflict conflictResolution: Database.ConflictResolution? = nil) -> Self {
notNullConflictResolution = conflictResolution ?? .abort
return self
}
/// Adds a unique constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // email TEXT UNIQUE
/// // )
/// try db.create(table: "player") { t in
/// t.column("email", .text).unique()
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#uniqueconst>
///
/// - parameter conflictResolution: An optional ``Database/ConflictResolution``.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func unique(onConflict conflictResolution: Database.ConflictResolution? = nil) -> Self {
indexing = .unique(conflictResolution ?? .abort)
return self
}
/// Adds an index.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(email TEXT);
/// // CREATE INDEX player_on_email ON player(email);
/// try db.create(table: "player") { t in
/// t.column("email", .text).indexed()
/// }
/// ```
///
/// The name of the created index is `<table>_on_<column>`, where `table`
/// and `column` are the names of the table and the column. See the
/// example above.
///
/// See also ``unique(onConflict:)``.
///
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func indexed() -> Self {
if case .none = indexing {
self.indexing = .index
}
return self
}
/// Adds a check constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // name TEXT CHECK (LENGTH(name) > 0)
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text).check { length($0) > 0 }
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#ckconst>
///
/// - parameter condition: A closure whose argument is a ``Column`` that
/// represents the defined column, and returns the expression to check.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func check(_ condition: (Column) -> any SQLExpressible) -> Self {
checkConstraints.append(condition(Column(name)).sqlExpression)
return self
}
/// Adds a check constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // name TEXT CHECK (LENGTH(name) > 0)
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text).check(sql: "LENGTH(name) > 0")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#ckconst>
///
/// - parameter sql: An SQL snippet.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func check(sql: String) -> Self {
checkConstraints.append(SQL(sql: sql).sqlExpression)
return self
}
/// Defines the default value.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // email TEXT DEFAULT 'Anonymous'
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text).defaults(to: "Anonymous")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#dfltval>
///
/// - parameter value: A ``DatabaseValueConvertible`` value.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func defaults(to value: some DatabaseValueConvertible) -> Self {
defaultExpression = value.sqlExpression
return self
}
/// Defines the default value.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // creationDate DATETIME DEFAULT CURRENT_TIMESTAMP
/// // )
/// try db.create(table: "player") { t in
/// t.column("creationDate", .DateTime).defaults(sql: "CURRENT_TIMESTAMP")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#dfltval>
///
/// - parameter sql: An SQL snippet.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func defaults(sql: String) -> Self {
defaultExpression = SQL(sql: sql).sqlExpression
return self
}
/// Defines the default collation.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // email TEXT COLLATE NOCASE
/// // )
/// try db.create(table: "player") { t in
/// t.column("email", .text).collate(.nocase)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html#collation>
///
/// - parameter collation: A ``Database/CollationName``.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func collate(_ collation: Database.CollationName) -> Self {
collationName = collation.rawValue
return self
}
/// Defines the default collation.
///
/// For example:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("name", .text).collate(.localizedCaseInsensitiveCompare)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html#collation>
///
/// - parameter collation: A ``DatabaseCollation``.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func collate(_ collation: DatabaseCollation) -> Self {
collationName = collation.name
return self
}
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// Defines the column as a generated column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // score INTEGER NOT NULL,
/// // bonus INTEGER NOT NULL,
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) VIRTUAL
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.column("score", .integer).notNull()
/// t.column("bonus", .integer).notNull()
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
/// }
/// ```
///
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
///
/// - parameters:
/// - sql: An SQL expression.
/// - qualification: The generated column's qualification, which
/// defaults to ``GeneratedColumnQualification/virtual``.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func generatedAs(
sql: String,
_ qualification: GeneratedColumnQualification = .virtual)
-> Self
{
let expression = SQL(sql: sql).sqlExpression
generatedColumnConstraint = GeneratedColumnConstraint(
expression: expression,
qualification: qualification)
return self
}
/// Defines the column as a generated column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // score INTEGER NOT NULL,
/// // bonus INTEGER NOT NULL,
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) VIRTUAL
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.column("score", .integer).notNull()
/// t.column("bonus", .integer).notNull()
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
/// }
/// ```
///
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
///
/// - parameters:
/// - expression: The generated expression.
/// - qualification: The generated column's qualification, which
/// defaults to ``GeneratedColumnQualification/virtual``.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func generatedAs(
_ expression: some SQLExpressible,
_ qualification: GeneratedColumnQualification = .virtual)
-> Self
{
generatedColumnConstraint = GeneratedColumnConstraint(
expression: expression.sqlExpression,
qualification: qualification)
return self
}
#else
/// Defines the column as a generated column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // score INTEGER NOT NULL,
/// // bonus INTEGER NOT NULL,
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) VIRTUAL
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.column("score", .integer).notNull()
/// t.column("bonus", .integer).notNull()
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
/// }
/// ```
///
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
///
/// - parameters:
/// - sql: An SQL expression.
/// - qualification: The generated column's qualification, which
/// defaults to ``GeneratedColumnQualification/virtual``.
/// - returns: `self` so that you can further refine the column definition.
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ (3.31 actually)
@discardableResult
public func generatedAs(
sql: String,
_ qualification: GeneratedColumnQualification = .virtual)
-> Self
{
let expression = SQL(sql: sql).sqlExpression
generatedColumnConstraint = GeneratedColumnConstraint(
expression: expression,
qualification: qualification)
return self
}
/// Defines the column as a generated column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // score INTEGER NOT NULL,
/// // bonus INTEGER NOT NULL,
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) VIRTUAL
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.column("score", .integer).notNull()
/// t.column("bonus", .integer).notNull()
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
/// }
/// ```
///
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
///
/// - parameters:
/// - expression: The generated expression.
/// - qualification: The generated column's qualification, which
/// defaults to ``GeneratedColumnQualification/virtual``.
/// - returns: `self` so that you can further refine the column definition.
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ (3.31 actually)
@discardableResult
public func generatedAs(
_ expression: some SQLExpressible,
_ qualification: GeneratedColumnQualification = .virtual)
-> Self
{
generatedColumnConstraint = GeneratedColumnConstraint(
expression: expression.sqlExpression,
qualification: qualification)
return self
}
#endif
/// Adds a foreign key constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE book(
/// // authorId INTEGER REFERENCES author(id) ON DELETE CASCADE
/// // )
/// try db.create(table: "book") { t in
/// t.column("authorId", .integer).references("author", onDelete: .cascade)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/foreignkeys.html>
///
/// - parameters:
/// - table: The referenced table.
/// - column: The referenced column in the referenced table. If not
/// specified, the column of the primary key of the referenced table
/// is used.
/// - deleteAction: Optional action when the referenced row is deleted.
/// - updateAction: Optional action when the referenced row is updated.
/// - isDeferred: A boolean value indicating whether the foreign key
/// constraint is deferred.
/// See <https://www.sqlite.org/foreignkeys.html#fk_deferred>.
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func references(
_ table: String,
column: String? = nil,
onDelete deleteAction: Database.ForeignKeyAction? = nil,
onUpdate updateAction: Database.ForeignKeyAction? = nil,
deferred isDeferred: Bool = false) -> Self
{
foreignKeyConstraints.append(ForeignKeyConstraint(
destinationTable: table,
destinationColumn: column,
deleteAction: deleteAction,
updateAction: updateAction,
isDeferred: isDeferred))
return self
}
func indexDefinition(in table: String, options: IndexOptions = []) -> IndexDefinition? {
switch indexing {
case .none: return nil
case .unique: return nil
case .index:
return IndexDefinition(
name: "\(table)_on_\(name)",
table: table,
expressions: [.column(name)],
options: options,
condition: nil)
}
}
}
// Explicit non-conformance to Sendable: `ColumnDefinition` is a mutable
// class and there is no known reason for making it thread-safe.
@available(*, unavailable)
extension ColumnDefinition: Sendable { }
@@ -0,0 +1,694 @@
extension Database {
// MARK: - Database Schema
/// Creates a database table.
///
/// For example:
///
/// ```swift
/// try db.create(table: "place") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.column("title", .text)
/// t.column("favorite", .boolean).notNull().default(false)
/// t.column("longitude", .double).notNull()
/// t.column("latitude", .double).notNull()
/// }
/// ```
///
/// Related SQLite documentation:
/// - <https://www.sqlite.org/lang_createtable.html>
/// - <https://www.sqlite.org/withoutrowid.html>
///
/// - warning: This is a legacy interface that is preserved for backwards
/// compatibility. Use of this interface is not recommended: prefer
/// ``create(table:options:body:)`` instead.
///
/// - parameters:
/// - name: The table name.
/// - temporary: If true, creates a temporary table.
/// - ifNotExists: If false (the default), an error is thrown if the
/// table already exists. Otherwise, the table is created unless it
/// already exists.
/// - withoutRowID: If true, uses WITHOUT ROWID optimization.
/// - body: A closure that defines table columns and constraints.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
@_disfavoredOverload
public func create(
table name: String,
temporary: Bool = false,
ifNotExists: Bool = false,
withoutRowID: Bool = false,
body: (TableDefinition) throws -> Void)
throws
{
var options: TableOptions = []
if temporary { options.insert(.temporary) }
if ifNotExists { options.insert(.ifNotExists) }
if withoutRowID { options.insert(.withoutRowID) }
try create(table: name, options: options, body: body)
}
/// Creates a database table.
///
/// ### Reference documentation
///
/// SQLite has many reference documents about table creation. They are a
/// great learning material:
///
/// - [CREATE TABLE](https://www.sqlite.org/lang_createtable.html)
/// - [Datatypes In SQLite](https://www.sqlite.org/datatype3.html)
/// - [SQLite Foreign Key Support](https://www.sqlite.org/foreignkeys.html)
/// - [The ON CONFLICT Clause](https://www.sqlite.org/lang_conflict.html)
/// - [Rowid Tables](https://www.sqlite.org/rowidtable.html)
/// - [The WITHOUT ROWID Optimization](https://www.sqlite.org/withoutrowid.html)
/// - [STRICT Tables](https://www.sqlite.org/stricttables.html)
///
/// ### Usage
///
/// ```swift
/// // CREATE TABLE place (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // title TEXT,
/// // isFavorite BOOLEAN NOT NULL DEFAULT 0,
/// // latitude DOUBLE NOT NULL,
/// // longitude DOUBLE NOT NULL
/// // )
/// try db.create(table: "place") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.column("title", .text)
/// t.column("isFavorite", .boolean).notNull().default(false)
/// t.column("longitude", .double).notNull()
/// t.column("latitude", .double).notNull()
/// }
/// ```
///
/// ### Configure table creation
///
/// Use the `options` parameter to configure table creation
/// (see ``TableOptions``):
///
/// ```swift
/// // CREATE TABLE player ( ... )
/// try db.create(table: "player") { t in ... }
///
/// // CREATE TEMPORARY TABLE player IF NOT EXISTS (
/// try db.create(table: "player", options: [.temporary, .ifNotExists]) { t in ... }
/// ```
///
/// ### Add columns
///
/// Add columns with their name and eventual type (`text`, `integer`,
/// `double`, `real`, `numeric`, `boolean`, `blob`, `date`, `datetime`
/// and `any`) - see ``Database/ColumnType``:
///
/// ```swift
/// // CREATE TABLE example (
/// // a,
/// // name TEXT,
/// // creationDate DATETIME,
/// try db.create(table: "example") { t in
/// t.column("a")
/// t.column("name", .text)
/// t.column("creationDate", .datetime)
/// ```
///
/// The `column()` method returns a ``ColumnDefinition`` that you can
/// further configure:
///
/// ### Not null constraints, default values
///
/// ```swift
/// // email TEXT NOT NULL,
/// t.column("email", .text).notNull()
///
/// // name TEXT DEFAULT 'O''Reilly',
/// t.column("name", .text).defaults(to: "O'Reilly")
///
/// // flag BOOLEAN NOT NULL DEFAULT 0,
/// t.column("flag", .boolean).notNull().defaults(to: false)
///
/// // creationDate DATETIME DEFAULT CURRENT_TIMESTAMP,
/// t.column("creationDate", .datetime).defaults(sql: "CURRENT_TIMESTAMP")
/// ```
///
/// ### Primary, unique, and foreign keys
///
/// Use an individual column as **primary**, **unique**, or **foreign key**.
/// When defining a foreign key, the referenced column is the primary key of
/// the referenced table (unless you specify otherwise):
///
/// ```swift
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// t.autoIncrementedPrimaryKey("id")
///
/// // uuid TEXT NOT NULL PRIMARY KEY,
/// t.primaryKey("uuid", .text)
///
/// // email TEXT UNIQUE,
/// t.column("email", .text)
/// .unique()
///
/// // countryCode TEXT REFERENCES country(code) ON DELETE CASCADE,
/// t.column("countryCode", .text)
/// .references("country", onDelete: .cascade)
/// ```
///
/// Primary, unique and foreign keys can also be added on several columns:
///
/// ```swift
/// // a INTEGER NOT NULL,
/// // b TEXT NOT NULL,
/// // PRIMARY KEY (a, b)
/// t.primaryKey {
/// t.column("a", .integer)
/// t.column("b", .text)
/// }
///
/// // a INTEGER NOT NULL,
/// // b TEXT NOT NULL,
/// // PRIMARY KEY (a, b)
/// t.column("a", .integer).notNull()
/// t.column("b", .text).notNull()
/// t.primaryKey(["a", "b"])
///
/// // a INTEGER,
/// // b TEXT,
/// // UNIQUE (a, b) ON CONFLICT REPLACE
/// t.column("a", .integer)
/// t.column("b", .text)
/// t.uniqueKey(["a", "b"], onConflict: .replace)
///
/// // a INTEGER,
/// // b TEXT,
/// // FOREIGN KEY (a, b) REFERENCES parents(c, d)
/// t.column("a", .integer)
/// t.column("b", .text)
/// t.foreignKey(["a", "b"], references: "parents")
/// ```
///
/// > Tip: when you need an integer primary key that automatically generates
/// unique values, it is recommended that you use the
/// ``TableDefinition/autoIncrementedPrimaryKey(_:onConflict:)`` method:
/// >
/// > ```swift
/// > try db.create(table: "example") { t in
/// > t.autoIncrementedPrimaryKey("id")
/// > ...
/// > }
/// > ```
/// >
/// > The reason for this recommendation is that auto-incremented primary
/// > keys forbid the reuse of ids. This prevents your app or
/// > <doc:DatabaseObservation> to think that a row was updated, when it was
/// > actually deleted and replaced. Depending on your application needs,
/// > this may be acceptable. But usually it is not.
///
/// ### Indexed columns
///
/// ```swift
/// t.column("score", .integer).indexed()
/// ```
///
/// For extra index options, see ``create(indexOn:columns:options:condition:)``.
///
/// ### Generated columns
///
/// See [Generated columns](https://sqlite.org/gencol.html) for
/// more information:
///
/// ```swift
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
/// ```
///
/// ### Integrity checks
///
/// SQLite will only let conforming rows in:
///
/// ```swift
/// // name TEXT CHECK (LENGTH(name) > 0)
/// t.column("name", .text).check { length($0) > 0 }
///
/// // score INTEGER CHECK (score > 0)
/// t.column("score", .integer).check(sql: "score > 0")
///
/// // CHECK (a + b < 10),
/// t.check(Column("a") + Column("b") < 10)
///
/// // CHECK (a + b < 10)
/// t.check(sql: "a + b < 10")
/// ```
///
/// ### Raw SQL columns and constraints
///
/// Columns and constraints can be defined with raw sql:
///
/// ```swift
/// t.column(sql: "name TEXT")
/// t.constraint(sql: "CHECK (a + b < 10)")
/// ```
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// let defaultName = "O'Reilly"
/// t.column(literal: "name TEXT DEFAULT \(defaultName)")
///
/// let forbiddenName = "admin"
/// t.constraint(literal: "CHECK (name <> \(forbiddenName))")
/// ```
///
/// - parameters:
/// - name: The table name.
/// - options: Table creation options.
/// - body: A closure that defines table columns and constraints.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(
table name: String,
options: TableOptions = [],
body: (TableDefinition) throws -> Void)
throws
{
let table = TableDefinition(
name: name,
options: options)
try body(table)
let generator = try SQLTableGenerator(self, table: table)
let sql = try generator.sql(self)
try execute(sql: sql)
}
/// Renames a database table.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func rename(table name: String, to newName: String) throws {
try execute(sql: "ALTER TABLE \(name.quotedDatabaseIdentifier) RENAME TO \(newName.quotedDatabaseIdentifier)")
}
/// Modifies a database table.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.add(column: "url", .text)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - parameters:
/// - name: The table name.
/// - body: A closure that defines table alterations.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func alter(table name: String, body: (TableAlteration) -> Void) throws {
let alteration = TableAlteration(name: name)
body(alteration)
let generator = SQLTableAlterationGenerator(alteration)
let sql = try generator.sql(self)
try execute(sql: sql)
}
/// Deletes a database table.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_droptable.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func drop(table name: String) throws {
try execute(sql: "DROP TABLE \(name.quotedDatabaseIdentifier)")
}
/// Creates a database view.
///
/// You can create a view with an ``SQLRequest``:
///
/// ```swift
/// // CREATE VIEW hero AS SELECT * FROM player WHERE isHero == 1
/// try db.create(view: "hero", as: SQLRequest(literal: """
/// SELECT * FROM player WHERE isHero == 1
/// """)
/// ```
///
/// You can also create a view with a ``QueryInterfaceRequest``:
///
/// ```swift
/// // CREATE VIEW hero AS SELECT * FROM player WHERE isHero == 1
/// try db.create(
/// view: "hero",
/// as: Player.filter(Column("isHero") == true))
/// ```
///
/// When creating views in <doc:Migrations>, it is not recommended to
/// use record types defined in the application. Instead of the `Player`
/// record type, prefer `Table("player")`:
///
/// ```swift
/// // RECOMMENDED IN MIGRATIONS
/// try db.create(
/// view: "hero",
/// as: Table("player").filter(Column("isHero") == true))
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createview.html>
///
/// - parameters:
/// - view: The view name.
/// - options: View creation options.
/// - columns: The columns of the view. If nil, the columns are the
/// columns of the request.
/// - request: The request that feeds the view.
public func create(
view name: String,
options: ViewOptions = [],
columns: [String]? = nil,
as request: SQLSubqueryable)
throws {
var literal: SQL = "CREATE "
if options.contains(.temporary) {
literal += "TEMPORARY "
}
literal += "VIEW "
if options.contains(.ifNotExists) {
literal += "IF NOT EXISTS "
}
literal += "\(identifier: name) "
if let columns {
literal += "("
literal += columns.map { "\(identifier: $0)" }.joined(separator: ", ")
literal += ") "
}
literal += "AS \(request)"
// CREATE VIEW does not support arguments, so make sure we use
// literal values.
let context = SQLGenerationContext(self, argumentsSink: .literalValues)
let sql = try literal.sql(context)
try execute(sql: sql)
}
/// Creates a database view.
///
/// For example:
///
/// ```swift
/// // CREATE VIEW hero AS SELECT * FROM player WHERE isHero == 1
/// try db.create(view: "hero", asLiteral: """
/// SELECT * FROM player WHERE isHero == 1
/// """)
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createview.html>
///
/// - parameters:
/// - view: The view name.
/// - options: View creation options.
/// - columns: The columns of the view. If nil, the columns are the
/// columns of the request.
/// - sqlLiteral: An `SQL` literal.
public func create(
view name: String,
options: ViewOptions = [],
columns: [String]? = nil,
asLiteral sqlLiteral: SQL)
throws {
try create(view: name, options: options, columns: columns, as: SQLRequest(literal: sqlLiteral))
}
/// Deletes a database view.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_dropview.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func drop(view name: String) throws {
try execute(sql: "DROP VIEW \(name.quotedDatabaseIdentifier)")
}
/// Creates an index on the specified table and columns.
///
/// For example:
///
/// ```swift
/// // CREATE INDEX index_player_on_email ON player(email)
/// try db.create(index: "index_player_on_email", on: "player", columns: ["email"])
/// ```
///
/// SQLite can also index expressions (<https://www.sqlite.org/expridx.html>)
/// and use specific collations. To create such an index, use
/// ``create(index:on:expressions:options:condition:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createindex.html>
///
/// - warning: This is a legacy interface that is preserved for backwards
/// compatibility. Use of this interface is not recommended: prefer
/// ``create(indexOn:columns:options:condition:)`` instead.
///
/// - parameters:
/// - name: The index name.
/// - table: The name of the indexed table.
/// - columns: The indexed columns.
/// - unique: If true, creates a unique index.
/// - ifNotExists: If true, no error is thrown if index already exists.
/// - condition: If not nil, creates a partial index
/// (see <https://www.sqlite.org/partialindex.html>).
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
@_disfavoredOverload
public func create(
index name: String,
on table: String,
columns: [String],
unique: Bool = false,
ifNotExists: Bool = false,
condition: (any SQLExpressible)? = nil)
throws
{
var options: IndexOptions = []
if ifNotExists { options.insert(.ifNotExists) }
if unique { options.insert(.unique) }
try create(index: name, on: table, columns: columns, options: options, condition: condition)
}
/// Creates an index on the specified table and columns.
///
/// For example:
///
/// ```swift
/// // CREATE INDEX index_player_on_email ON player(email)
/// try db.create(index: "index_player_on_email", on: "player", columns: ["email"])
/// ```
///
/// To create a unique index, specify the `.unique` option:
///
/// ```swift
/// // CREATE UNIQUE INDEX index_player_on_email ON player(email)
/// try db.create(index: "index_player_on_email", on: "player", columns: ["email"], options: .unique)
/// ```
///
/// SQLite can also index expressions (<https://www.sqlite.org/expridx.html>)
/// and use specific collations. To create such an index, use a raw SQL
/// query:
///
/// ```swift
/// try db.execute(sql: "CREATE INDEX ...")
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createindex.html>
///
/// - parameters:
/// - name: The index name.
/// - table: The name of the indexed table.
/// - columns: The indexed columns.
/// - options: Index creation options.
/// - condition: If not nil, creates a partial index
/// (see <https://www.sqlite.org/partialindex.html>).
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(
index name: String,
on table: String,
columns: [String],
options: IndexOptions = [],
condition: (any SQLExpressible)? = nil)
throws
{
let index = IndexDefinition(
name: name,
table: table,
expressions: columns.map { .column($0) },
options: options,
condition: condition?.sqlExpression)
let generator = SQLIndexGenerator(index: index)
let sql = try generator.sql(self)
try execute(sql: sql)
}
/// Creates an index on the specified table and expressions.
///
/// This method can generally create indexes on expressions (see
/// <https://www.sqlite.org/expridx.html>):
///
/// ```swift
/// // CREATE INDEX txy ON t(x+y)
/// try db.create(
/// index: "txy",
/// on: "t",
/// expressions: [Column("x") + Column("y")])
/// ```
///
/// In particular, you can specify the collation on indexed
/// columns (see <https://www.sqlite.org/lang_createindex.html#collations>):
///
/// ```swift
/// // CREATE INDEX index_player_name ON player(name COLLATE NOCASE)
/// try db.create(
/// index: "index_player_name",
/// on: "player",
/// expressions: [Column("name").collating(.nocase)])
/// ```
///
/// - parameters:
/// - name: The index name.
/// - table: The name of the indexed table.
/// - expressions: The indexed expressions.
/// - options: Index creation options.
/// - condition: If not nil, creates a partial index
/// (see <https://www.sqlite.org/partialindex.html>).
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(
index name: String,
on table: String,
expressions: [any SQLExpressible],
options: IndexOptions = [],
condition: (any SQLExpressible)? = nil)
throws
{
let index = IndexDefinition(
name: name,
table: table,
expressions: expressions.map { $0.sqlExpression },
options: options,
condition: condition?.sqlExpression)
let generator = SQLIndexGenerator(index: index)
let sql = try generator.sql(self)
try execute(sql: sql)
}
/// Creates an index with a default name on the specified table and columns.
///
/// The created index is named after the table and the column name(s):
///
/// ```swift
/// // CREATE INDEX index_player_on_email ON player(email)
/// try db.create(indexOn: "player", columns: ["email"])
/// ```
///
/// To create a unique index, specify the `.unique` option:
///
/// ```swift
/// // CREATE UNIQUE INDEX index_player_on_email ON player(email)
/// try db.create(indexOn: "player", columns: ["email"], options: .unique)
/// ```
///
/// In order to specify the index name, use
/// ``create(index:on:columns:options:condition:)`` instead.
///
/// SQLite can also index expressions (<https://www.sqlite.org/expridx.html>)
/// and use specific collations. To create such an index, use
/// ``create(index:on:expressions:options:condition:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createindex.html>
///
/// - parameters:
/// - table: The name of the indexed table.
/// - columns: The indexed columns.
/// - options: Index creation options.
/// - condition: If not nil, creates a partial index
/// (see <https://www.sqlite.org/partialindex.html>).
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(
indexOn table: String,
columns: [String],
options: IndexOptions = [],
condition: (any SQLExpressible)? = nil)
throws
{
try create(
index: Database.defaultIndexName(on: table, columns: columns),
on: table,
columns: columns,
options: options,
condition: condition)
}
/// Deletes a database index.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_dropindex.html>
///
/// - parameter name: The index name.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func drop(index name: String) throws {
try execute(sql: "DROP INDEX \(name.quotedDatabaseIdentifier)")
}
/// Deletes the database index on the specified table and columns
/// if exactly one such index exists.
///
/// - parameters:
/// - table: The name of the indexed table.
/// - columns: The indexed columns.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func drop(indexOn table: String, columns: [String]) throws {
let lowercasedColumns = columns.map { $0.lowercased() }
let indexes = try indexes(on: table).filter { index in
index.columns.map({ $0.lowercased() }) == lowercasedColumns
}
if let index = indexes.first, indexes.count == 1 {
try drop(index: index.name)
}
}
/// Deletes and recreates from scratch all indices that use this collation.
///
/// This method is useful when the definition of a collation sequence
/// has changed.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_reindex.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func reindex(collation: Database.CollationName) throws {
try execute(sql: "REINDEX \(collation.rawValue)")
}
/// Deletes and recreates from scratch all indices that use this collation.
///
/// This method is useful when the definition of a collation sequence
/// has changed.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_reindex.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func reindex(collation: DatabaseCollation) throws {
try reindex(collation: Database.CollationName(rawValue: collation.name))
}
}
/// View creation options
public struct ViewOptions: OptionSet, Sendable {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
/// Only creates the view if it does not already exist.
public static let ifNotExists = ViewOptions(rawValue: 1 << 0)
/// Creates a temporary view.
public static let temporary = ViewOptions(rawValue: 1 << 1)
}
@@ -0,0 +1,111 @@
/// Describes an association in the database schema.
///
/// You get instances of `ForeignKeyDefinition` when you create a database
/// tables. For example:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.belongsTo("team") // ForeignKeyDefinition
/// }
/// ```
///
/// See ``TableDefinition/belongsTo(_:inTable:onDelete:onUpdate:deferred:indexed:)``.
public final class ForeignKeyDefinition {
enum Indexing {
case index
case unique
}
var name: String
var table: String?
var deleteAction: Database.ForeignKeyAction?
var updateAction: Database.ForeignKeyAction?
var indexing: Indexing?
var isDeferred: Bool
var notNullConflictResolution: Database.ConflictResolution?
init(
name: String,
table: String?,
deleteAction: Database.ForeignKeyAction?,
updateAction: Database.ForeignKeyAction?,
isIndexed: Bool,
isDeferred: Bool)
{
self.name = name
self.table = table
self.deleteAction = deleteAction
self.updateAction = updateAction
self.indexing = isIndexed ? .index : nil
self.isDeferred = isDeferred
}
/// Adds a not null constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // teamId INTEGER NOT NULL REFERENCES team(id)
/// // )
/// try db.create(table: "player") { t in
/// t.belongsTo("team").notNull()
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#notnullconst>
///
/// - parameter conflictResolution: An optional ``Database/ConflictResolution``.
/// - returns: `self` so that you can further refine the definition of
/// the association.
@discardableResult
public func notNull(onConflict conflictResolution: Database.ConflictResolution? = nil) -> Self {
notNullConflictResolution = conflictResolution ?? .abort
return self
}
/// Adds a unique constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // teamId INTEGER UNIQUE REFERENCES team(id)
/// // )
/// try db.create(table: "player") { t in
/// t.belongsTo("team").unique()
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#uniqueconst>
///
/// - returns: `self` so that you can further refine the definition of
/// the association.
@discardableResult
public func unique() -> Self {
indexing = .unique
return self
}
func primaryKey(_ db: Database) throws -> SQLPrimaryKeyDescriptor {
if let table {
return try SQLPrimaryKeyDescriptor.find(db, table: table)
}
if try db.tableExists(name) {
return try SQLPrimaryKeyDescriptor.find(db, table: name)
}
let pluralizedName = name.pluralized
if try db.tableExists(pluralizedName) {
return try SQLPrimaryKeyDescriptor.find(db, table: pluralizedName)
}
throw DatabaseError.noSuchTable(name)
}
}
// Explicit non-conformance to Sendable: `ForeignKeyDefinition` is a mutable
// class and there is no known reason for making it thread-safe.
@available(*, unavailable)
extension ForeignKeyDefinition: Sendable { }
@@ -0,0 +1,26 @@
struct IndexDefinition {
let name: String
let table: String
let expressions: [SQLExpression]
let options: IndexOptions
let condition: SQLExpression?
}
/// Index creation options
public struct IndexOptions: OptionSet, Sendable {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
/// Only creates the index if it does not already exist.
public static let ifNotExists = IndexOptions(rawValue: 1 << 0)
/// Creates a unique index.
public static let unique = IndexOptions(rawValue: 1 << 1)
}
extension Database {
static func defaultIndexName(on table: String, columns: [String]) -> String {
"index_\(table)_on_\(columns.joined(separator: "_"))"
}
}
@@ -0,0 +1,168 @@
/// A `TableDefinition` lets you modify the components of a database table.
///
/// You don't create instances of this class. Instead, you use the `Database`
/// ``Database/alter(table:body:)`` method:
///
/// ```swift
/// try db.alter(table: "player") { t in // t is TableAlteration
/// t.add(column: "bonus", .integer)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
public final class TableAlteration {
let name: String
enum TableAlterationKind {
case add(ColumnDefinition)
case addColumnLiteral(SQL)
case rename(old: String, new: String)
case drop(String)
}
var alterations: [TableAlterationKind] = []
init(name: String) {
self.name = name
}
/// Appends a column.
///
/// For example:
///
/// ```swift
/// // ALTER TABLE player ADD COLUMN bonus integer
/// try db.alter(table: "player") { t in
/// t.add(column: "bonus", .integer)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - parameter name: the column name.
/// - parameter type: the column type.
/// - returns: An ColumnDefinition that allows you to refine the
/// column definition.
@discardableResult
public func add(column name: String, _ type: Database.ColumnType? = nil) -> ColumnDefinition {
let column = ColumnDefinition(name: name, type: type)
alterations.append(.add(column))
return column
}
/// Appends a column.
///
/// For example:
///
/// ```swift
/// // ALTER TABLE player ADD COLUMN bonus integer
/// try db.alter(table: "player") { t in
/// t.addColumn(sql: "bonus integer")
/// }
/// ```
public func addColumn(sql: String) {
alterations.append(.addColumnLiteral(SQL(sql: sql)))
}
/// Appends a column.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// // ALTER TABLE player ADD COLUMN name TEXT DEFAULT 'Anonymous'
/// try db.alter(table: "player") { t in
/// t.addColumn(literal: "name TEXT DEFAULT \(defaultName)")
/// }
/// ```
public func addColumn(literal: SQL) {
alterations.append(.addColumnLiteral(literal))
}
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// Renames a column.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.rename(column: "url", to: "homeURL")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - parameter name: the old name of the column.
/// - parameter newName: the new name of the column.
public func rename(column name: String, to newName: String) {
_rename(column: name, to: newName)
}
/// Drops a column.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.drop(column: "age")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - Parameter name: the name of the column to drop.
public func drop(column name: String) {
_drop(column: name)
}
#else
/// Renames a column.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.rename(column: "url", to: "homeURL")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - parameter name: the old name of the column.
/// - parameter newName: the new name of the column.
@available(iOS 13, tvOS 13, watchOS 6, *) // SQLite 3.25+
public func rename(column name: String, to newName: String) {
_rename(column: name, to: newName)
}
/// Drops a column.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.drop(column: "age")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - Parameter name: the name of the column to drop.
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
public func drop(column name: String) {
_drop(column: name)
}
#endif
private func _rename(column name: String, to newName: String) {
alterations.append(.rename(old: name, new: newName))
}
private func _drop(column name: String) {
alterations.append(.drop(name))
}
}
// Explicit non-conformance to Sendable: `TableAlteration` is a mutable
// class and there is no known reason for making it thread-safe.
@available(*, unavailable)
extension TableAlteration: Sendable { }
@@ -0,0 +1,760 @@
/// Table creation options.
public struct TableOptions: OptionSet, Sendable {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
/// Only creates the table if it does not already exist.
public static let ifNotExists = TableOptions(rawValue: 1 << 0)
/// Creates a temporary table.
public static let temporary = TableOptions(rawValue: 1 << 1)
/// Creates a [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) table.
///
/// Such tables can not be tracked with <doc:DatabaseObservation> tools.
public static let withoutRowID = TableOptions(rawValue: 1 << 2)
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// Creates a [STRICT](https://www.sqlite.org/stricttables.html) table.
public static let strict = TableOptions(rawValue: 1 << 3)
#else
/// Creates a [STRICT](https://www.sqlite.org/stricttables.html) table.
@available(iOS 15.4, macOS 12.4, tvOS 15.4, watchOS 8.5, *) // SQLite 3.37+
public static let strict = TableOptions(rawValue: 1 << 3)
#endif
}
/// A `TableDefinition` lets you define the components of a database table.
///
/// See the documentation of the `Database`
/// ``Database/create(table:options:body:)`` method for usage information:
///
/// ```swift
/// try db.create(table: "player") { t in // t is TableDefinition
/// t.autoIncrementedPrimaryKey("id")
/// t.column("name", .text).notNull()
/// }
/// ```
///
/// ## Topics
///
/// ### Define Columns
///
/// - ``column(_:_:)``
/// - ``column(literal:)``
/// - ``column(sql:)``
/// - ``ColumnDefinition``
///
/// ### Define the Primary Key
///
/// - ``autoIncrementedPrimaryKey(_:onConflict:)``
/// - ``primaryKey(_:_:onConflict:)``
/// - ``primaryKey(onConflict:body:)``
/// - ``primaryKey(_:onConflict:)``
///
/// ### Define a Foreign Key
///
/// - ``belongsTo(_:inTable:onDelete:onUpdate:deferred:indexed:)``
/// - ``foreignKey(_:references:columns:onDelete:onUpdate:deferred:)``
/// - ``ForeignKeyDefinition``
///
/// ### Define a Unique Key
///
/// - ``uniqueKey(_:onConflict:)``
///
/// ### Define Others Constraints
///
/// - ``check(_:)-6u1za``
/// - ``check(_:)-jpcg``
/// - ``check(sql:)``
/// - ``constraint(literal:)``
/// - ``constraint(sql:)``
public final class TableDefinition {
struct KeyConstraint {
enum Component {
case columnName(String)
case columnDefinition(ColumnDefinition)
case foreignKeyDefinition(ForeignKeyDefinition)
}
var components: [Component]
var conflictResolution: Database.ConflictResolution?
init(components: [Component], conflictResolution: Database.ConflictResolution?) {
self.components = components
self.conflictResolution = conflictResolution
}
init(columns: [String], conflictResolution: Database.ConflictResolution?) {
let components = columns.map { name in
Component.columnName(name)
}
self.init(components: components, conflictResolution: conflictResolution)
}
}
enum ColumnComponent {
case columnDefinition(ColumnDefinition)
case columnLiteral(SQL)
case foreignKeyDefinition(ForeignKeyDefinition)
case foreignKeyConstraint(SQLForeignKeyConstraint)
}
let name: String
let options: TableOptions
var columnComponents: [ColumnComponent] = []
var inPrimaryKeyBody = false
var primaryKeyConstraint: KeyConstraint?
var uniqueKeyConstraints: [KeyConstraint] = []
var checkConstraints: [SQLExpression] = []
var literalConstraints: [SQL] = []
init(name: String, options: TableOptions) {
self.name = name
self.options = options
}
/// Appends an auto-incremented primary key column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// }
/// ```
///
/// The auto-incremented primary key is an integer primary key that
/// automatically generates unused values when you do not explicitly
/// provide one, and prevents the reuse of ids over the lifetime of
/// the database.
///
/// Related SQLite documentation:
/// - <https://www.sqlite.org/lang_createtable.html#primkeyconst>
/// - <https://www.sqlite.org/lang_createtable.html#rowid>
///
/// - parameter conflictResolution: An optional conflict resolution
/// (see <https://www.sqlite.org/lang_conflict.html>).
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func autoIncrementedPrimaryKey(
_ name: String,
onConflict conflictResolution: Database.ConflictResolution? = nil)
-> ColumnDefinition
{
column(name, .integer).primaryKey(onConflict: conflictResolution, autoincrement: true)
}
/// Appends a primary key column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE country (
/// // isoCode TEXT NOT NULL PRIMARY KEY
/// // )
/// try db.create(table: "country") { t in
/// t.primaryKey("isoCode", .text)
/// }
/// ```
///
/// - parameter name: the column name.
/// - parameter type: the column type.
/// - returns: A ``ColumnDefinition`` that allows you to refine the
/// column definition.
@discardableResult
public func primaryKey(
_ name: String,
_ type: Database.ColumnType,
onConflict conflictResolution: Database.ConflictResolution? = nil)
-> ColumnDefinition
{
let pk = column(name, type).primaryKey(onConflict: conflictResolution)
if type == .integer {
// INTEGER PRIMARY KEY is always NOT NULL
return pk
} else {
// Add a not null constraint in order to fix an SQLite bug:
// <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
return pk.notNull()
}
}
/// Defines the primary key on wrapped columns.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE passport (
/// // citizenId INTEGER NOT NULL,
/// // countryCode TEXT NOT NULL,
/// // issueDate DATE NOT NULL,
/// // PRIMARY KEY (citizenId, countryCode)
/// // )
/// try db.create(table: "passport") { t in
/// t.primaryKey {
/// t.column("citizenId", .integer)
/// t.column("countryCode", .text)
/// }
/// t.column("issueDate", .date).notNull()
/// }
/// ```
///
/// A NOT NULL constraint is always added to the wrapped primary key columns.
public func primaryKey(
onConflict conflictResolution: Database.ConflictResolution? = nil,
body: () throws -> Void)
rethrows
{
guard primaryKeyConstraint == nil else {
// Programmer error
fatalError("can't define several primary keys")
}
primaryKeyConstraint = KeyConstraint(components: [], conflictResolution: conflictResolution)
let oldValue = inPrimaryKeyBody
inPrimaryKeyBody = true
defer { inPrimaryKeyBody = oldValue }
try body()
}
/// Appends a table column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // name TEXT
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#tablecoldef>
///
/// - parameter name: the column name.
/// - parameter type: the eventual column type.
/// - returns: A ``ColumnDefinition`` that allows you to refine the
/// column definition.
@discardableResult
public func column(_ name: String, _ type: Database.ColumnType? = nil) -> ColumnDefinition {
let column = ColumnDefinition(name: name, type: type)
columnComponents.append(.columnDefinition(column))
if inPrimaryKeyBody {
// Add a not null constraint in order to fix an SQLite bug:
// <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
column.notNull()
primaryKeyConstraint!.components.append(.columnDefinition(column))
}
return column
}
/// Appends a table column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // name TEXT
/// // )
/// try db.create(table: "player") { t in
/// t.column(sql: "name TEXT")
/// }
/// ```
public func column(sql: String) {
column(literal: SQL(sql: sql))
}
/// Appends a table column.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// // CREATE TABLE player (
/// // name TEXT DEFAULT 'Anonymous'
/// // )
/// let defaultName = "Anonymous"
/// try db.create(table: "player") { t in
/// t.column(literal: "name TEXT DEFAULT \(defaultName)")
/// }
/// ```
public func column(literal: SQL) {
GRDBPrecondition(!inPrimaryKeyBody, "Primary key columns can not be defined with raw SQL")
columnComponents.append(.columnLiteral(literal))
}
/// Adds a primary key constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE citizenship (
/// // citizenId INTEGER NOT NULL,
/// // countryCode TEXT NOT NULL,
/// // PRIMARY KEY (citizenId, countryCode)
/// // )
/// try db.create(table: "citizenship") { t in
/// t.column("citizenId", .integer).notNull()
/// t.column("countryCode", .text).notNull()
/// t.primaryKey(["citizenId", "countryCode"])
/// }
/// ```
///
/// - important: Make sure you add not null constraints on your primary key
/// columns, as in the above example, or SQLite will allow null values.
/// See <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
/// for more information.
///
/// - parameter columns: The primary key columns.
/// - parameter conflictResolution: An optional conflict resolution
/// (see <https://www.sqlite.org/lang_conflict.html>).
public func primaryKey(_ columns: [String], onConflict conflictResolution: Database.ConflictResolution? = nil) {
guard primaryKeyConstraint == nil else {
// Programmer error
fatalError("can't define several primary keys")
}
primaryKeyConstraint = KeyConstraint(columns: columns, conflictResolution: conflictResolution)
}
/// Adds a unique constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE place (
/// // latitude DOUBLE,
/// // longitude DOUBLE,
/// // UNIQUE (latitude, longitude)
/// // )
/// try db.create(table: "place") { t in
/// t.column("latitude", .double)
/// t.column("longitude", .double)
/// t.uniqueKey(["latitude", "longitude"])
/// }
/// ```
///
/// When defining a unique constraint on a single column, you can use the
/// ``ColumnDefinition/unique(onConflict:)`` shortcut:
///
/// ```swift
/// // CREATE TABLE player(
/// // email TEXT UNIQUE
/// // )
/// try db.create(table: "player") { t in
/// t.column("email", .text).unique()
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#uniqueconst>
///
/// - parameter columns: The unique key columns.
/// - parameter conflictResolution: An optional conflict resolution
/// (see <https://www.sqlite.org/lang_conflict.html>).
public func uniqueKey(_ columns: [String], onConflict conflictResolution: Database.ConflictResolution? = nil) {
uniqueKeyConstraints.append(KeyConstraint(columns: columns, conflictResolution: conflictResolution))
}
/// Adds a foreign key.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE passport (
/// // issueDate DATE NOT NULL,
/// // citizenId INTEGER NOT NULL,
/// // countryCode INTEGER NOT NULL,
/// // FOREIGN KEY (citizenId, countryCode)
/// // REFERENCES citizenship(citizenId, countryCode)
/// // ON DELETE CASCADE
/// // )
/// try db.create(table: "passport") { t in
/// t.column("issueDate", .date).notNull()
/// t.column("citizenId", .integer).notNull()
/// t.column("countryCode", .text).notNull()
/// t.foreignKey(["citizenId", "countryCode"], references: "citizenship", onDelete: .cascade)
/// }
/// ```
///
/// When defining a foreign key on a single column, you can use the
/// ``ColumnDefinition/references(_:column:onDelete:onUpdate:deferred:)``
/// shortcut:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("teamId", .integer).references("team", onDelete: .cascade)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/foreignkeys.html>
///
/// - parameters:
/// - columns: The foreign key columns.
/// - table: The referenced table.
/// - destinationColumns: The columns in the referenced table. If not
/// specified, the columns of the primary key of the referenced table
/// are used.
/// - deleteAction: Optional action when the referenced row is deleted.
/// - updateAction: Optional action when the referenced row is updated.
/// - isDeferred: A boolean value indicating whether the foreign key
/// constraint is deferred.
/// See <https://www.sqlite.org/foreignkeys.html#fk_deferred>.
public func foreignKey(
_ columns: [String],
references table: String,
columns destinationColumns: [String]? = nil,
onDelete deleteAction: Database.ForeignKeyAction? = nil,
onUpdate updateAction: Database.ForeignKeyAction? = nil,
deferred isDeferred: Bool = false)
{
let foreignKeyConstraint = SQLForeignKeyConstraint(
columns: columns,
destinationTable: table,
destinationColumns: destinationColumns,
deleteAction: deleteAction,
updateAction: updateAction,
isDeferred: isDeferred)
columnComponents.append(.foreignKeyConstraint(foreignKeyConstraint))
}
/// Declares an association to another table.
///
/// `belongsTo` appends as many columns as there are columns in the
/// primary key of the referenced table, and declares a foreign key that
/// guarantees schema integrity. All primary keys are supported,
/// including composite primary keys that span several columns, and the
/// hidden `rowid` column.
///
/// Added columns are prefixed with `name`, and end with the name of the
/// matching column in the primary key of the referenced table. In the
/// following example, `belongsTo("team")` adds a `teamId` column, and
/// `belongsTo("country")` adds a `countryCode` column:
///
/// ```swift
/// try db.create(table: "team") { t in
/// t.autoIncrementedPrimaryKey("id")
/// }
/// try db.create(table: "country") { t in
/// t.primaryKey("code", .text)
/// }
///
/// // CREATE TABLE player (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // teamId INTEGER REFERENCES team(id),
/// // countryCode TEXT NOT NULL REFERENCES country(code),
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("team")
/// t.belongsTo("country").notNull()
/// }
/// ```
///
/// When in doubt, you can check the names of the created columns:
///
/// ```swift
/// // Prints ["id", "teamId", "countryCode"]
/// try print(db.columns(in: "player").map(\.name))
/// ```
///
/// Singular names can refer to database tables whose name is plural:
///
/// ```swift
/// try db.create(table: "teams") { t in
/// t.autoIncrementedPrimaryKey("id")
/// }
/// try db.create(table: "countries") { t in
/// t.primaryKey("code", .text)
/// }
///
/// // CREATE TABLE players (
/// // teamId INTEGER REFERENCES teams(id),
/// // countryCode TEXT REFERENCES countries(code),
/// // )
/// try db.create(table: "players") { t in
/// t.belongsTo("team")
/// t.belongsTo("country")
/// }
/// ```
///
/// When the added columns should have a custom prefix, specify an
/// explicit table name:
///
/// ```swift
/// // CREATE TABLE player (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // captainId INTEGER REFERENCES player(id),
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("captain", inTable: "player")
/// }
///
/// // CREATE TABLE book (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // authorId INTEGER REFERENCES person(id),
/// // translatorId INTEGER REFERENCES person(id),
/// // title TEXT
/// // )
/// try db.create(table: "book") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("author", inTable: "person")
/// t.belongsTo("translator", inTable: "person")
/// t.column("title", .text)
/// }
/// ```
///
/// Specify foreign key actions:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.belongsTo("team", onDelete: .cascade)
/// t.belongsTo("captain", inTable: "player", onDelete: .setNull)
/// }
/// ```
///
/// The added columns are indexed by default. You can disable this
/// automatic index with the `indexed: false` option. You can also make
/// this index unique with ``ForeignKeyDefinition/unique()``:
///
/// ```swift
/// try db.create(table: "player") { t in
/// // teamId is not indexed
/// t.belongsTo("team", indexed: false)
///
/// // One single player per country
/// t.belongsTo("country").unique()
/// }
/// ```
///
/// For more precision in the definition of foreign keys, use instead
/// ``ColumnDefinition/references(_:column:onDelete:onUpdate:deferred:)``
/// or ``TableDefinition/foreignKey(_:references:columns:onDelete:onUpdate:deferred:)``.
/// For example:
///
/// ```swift
/// try db.create(table: "player") { t in
/// // This convenience method...
/// t.belongsTo("team")
///
/// // ... is equivalent to:
/// t.column("teamId", .integer)
/// .references("team")
/// .indexed()
///
/// // ... and is equivalent to:
/// t.column("teamId", .integer).indexed()
/// t.foreignKey(["teamId"], references: "team")
/// }
/// ```
///
/// See [Associations](https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md)
/// for more information about foreign keys and associations.
///
/// - parameters:
/// - name: The name of the foreign key, used as a prefix for the
/// added columns.
/// - table: The referenced table. If nil, the referenced table is
/// designated by the `name` parameter.
/// - deleteAction: Optional action when the referenced row
/// is deleted.
/// - updateAction: Optional action when the referenced row
/// is updated.
/// - isDeferred: A boolean value indicating whether the foreign key
/// constraint is deferred.
/// See <https://www.sqlite.org/foreignkeys.html#fk_deferred>.
/// - indexed: A boolean value indicating whether the foreign key is
/// indexed. It is true by default.
/// - returns: A ``ForeignKeyDefinition`` that allows you to refine the
/// foreign key.
@discardableResult
public func belongsTo(
_ name: String,
inTable table: String? = nil,
onDelete deleteAction: Database.ForeignKeyAction? = nil,
onUpdate updateAction: Database.ForeignKeyAction? = nil,
deferred isDeferred: Bool = false,
indexed: Bool = true)
-> ForeignKeyDefinition
{
let foreignKey = ForeignKeyDefinition(
name: name,
table: table,
deleteAction: deleteAction,
updateAction: updateAction,
isIndexed: indexed && !inPrimaryKeyBody,
isDeferred: isDeferred)
columnComponents.append(.foreignKeyDefinition(foreignKey))
if inPrimaryKeyBody {
// Add a not null constraint in order to fix an SQLite bug:
// <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
foreignKey.notNull()
primaryKeyConstraint!.components.append(.foreignKeyDefinition(foreignKey))
}
return foreignKey
}
/// Adds a check constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // personalPhone TEXT,
/// // workPhone TEXT,
/// // CHECK personalPhone IS NOT NULL OR workPhone IS NOT NULL
/// // )
/// try db.create(table: "player") { t in
/// t.column("personalPhone", .text)
/// t.column("workPhone", .text)
/// let personalPhone = Column("personalPhone")
/// let workPhone = Column("workPhone")
/// t.check(personalPhone != nil || workPhone != nil)
/// }
/// ```
///
/// When defining a check constraint on a single column, you can use the
/// ``ColumnDefinition/check(_:)`` shortcut:
///
/// ```swift
/// // CREATE TABLE player(
/// // name TEXT CHECK (LENGTH(name) > 0)
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text).check { length($0) > 0 }
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#ckconst>
///
/// - parameter condition: The checked condition.
@available(*, deprecated)
public func check(_ condition: some SQLExpressible) {
checkConstraints.append(condition.sqlExpression)
}
/// Adds a check constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // personalPhone TEXT,
/// // workPhone TEXT,
/// // CHECK personalPhone IS NOT NULL OR workPhone IS NOT NULL
/// // )
/// try db.create(table: "player") { t in
/// t.column("personalPhone", .text)
/// t.column("workPhone", .text)
/// let personalPhone = Column("personalPhone")
/// let workPhone = Column("workPhone")
/// t.check(personalPhone != nil || workPhone != nil)
/// }
/// ```
///
/// When defining a check constraint on a single column, you can use the
/// ``ColumnDefinition/check(_:)`` shortcut:
///
/// ```swift
/// // CREATE TABLE player(
/// // name TEXT CHECK (LENGTH(name) > 0)
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text).check { length($0) > 0 }
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#ckconst>
///
/// - parameter condition: The checked condition.
public func check(_ condition: some SQLSpecificExpressible) {
checkConstraints.append(condition.sqlExpression)
}
/// Adds a check constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // personalPhone TEXT,
/// // workPhone TEXT,
/// // CHECK personalPhone IS NOT NULL OR workPhone IS NOT NULL
/// // )
/// try db.create(table: "player") { t in
/// t.column("personalPhone", .text)
/// t.column("workPhone", .text)
/// t.check(sql: "personalPhone IS NOT NULL OR workPhone IS NOT NULL")
/// }
/// ```
///
/// When defining a check constraint on a single column, you can use the
/// ``ColumnDefinition/check(sql:)`` shortcut:
///
/// ```swift
/// // CREATE TABLE player(
/// // name TEXT CHECK (LENGTH(name) > 0)
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text).check(sql: "LENGTH(name) > 0")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#ckconst>
///
/// - parameter sql: An SQL snippet
public func check(sql: String) {
checkConstraints.append(SQL(sql: sql).sqlExpression)
}
/// Appends a table constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // score INTEGER,
/// // CHECK (score >= 0)
/// // )
/// try db.create(table: "player") { t in
/// t.column("score", .integer)
/// t.constraint(sql: "CHECK (score >= 0)")
/// }
/// ```
public func constraint(sql: String) {
literalConstraints.append(SQL(sql: sql))
}
/// Appends a table constraint.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// // CREATE TABLE player (
/// // score INTEGER,
/// // CHECK (score >= 0)
/// // )
/// let minScore = 0
/// try db.create(table: "player") { t in
/// t.column("score", .integer)
/// t.constraint(literal: "CHECK (score >= \(minScore))")
/// }
/// ```
public func constraint(literal: SQL) {
literalConstraints.append(literal)
}
}
// Explicit non-conformance to Sendable: `TableDefinition` is a mutable
// class and there is no known reason for making it thread-safe.
@available(*, unavailable)
extension TableDefinition: Sendable { }
@@ -0,0 +1,159 @@
/// The protocol for SQLite virtual table modules.
///
/// The protocol can define a DSL for the
/// ``Database/create(virtualTable:ifNotExists:using:_:)`` `Database` method:
///
/// ```swift
/// let module = ...
/// try db.create(virtualTable: "item", using: module) { t in
/// ...
/// }
/// ```
///
/// GRDB ships with three concrete classes that implement this protocol:
/// ``FTS3``, ``FTS4`` and `FTS5`.
///
/// ## Topics
///
/// ### Configuration Virtual Table Creation
///
/// - ``VirtualTableConfiguration``
public protocol VirtualTableModule {
/// The type of the argument in the
/// ``Database/create(virtualTable:ifNotExists:using:_:)`` closure.
///
/// For example:
///
/// ```swift
/// try db.create(virtualTable: "item", using: module) { t in
/// // t is TableDefinition
/// }
/// ```
associatedtype TableDefinition
/// The name of the module.
var moduleName: String { get }
/// Returns a table definition that is passed as the argument in the
/// ``Database/create(virtualTable:ifNotExists:using:_:)`` closure.
///
/// For example:
///
/// ```swift
/// try db.create(virtualTable: "item", using: module) { t in
/// // t is the result of makeTableDefinition(configuration:)
/// }
/// ```
func makeTableDefinition(configuration: VirtualTableConfiguration) -> TableDefinition
/// Returns the module arguments for the `CREATE VIRTUAL TABLE` query.
func moduleArguments(for definition: TableDefinition, in db: Database) throws -> [String]
/// Execute any relevant database statement after the virtual table has
/// been created.
func database(_ db: Database, didCreate tableName: String, using definition: TableDefinition) throws
}
public struct VirtualTableConfiguration {
/// If true, existing objects must not be replaced, or generate any error
/// (even if they do not match the objects that would be created otherwise.)
var ifNotExists: Bool
}
extension Database {
// MARK: - Database Schema
/// Creates a virtual database table.
///
/// For example:
///
/// ```swift
/// // CREATE VIRTUAL TABLE vocabulary USING spellfix1
/// try db.create(virtualTable: "vocabulary", using: "spellfix1")
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html>
///
/// - parameters:
/// - name: The table name.
/// - ifNotExists: If false (the default), an error is thrown if the
/// table already exists. Otherwise, the table is created unless it
/// already exists.
/// - module: The name of an SQLite virtual table module.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(virtualTable name: String, ifNotExists: Bool = false, using module: String) throws {
var chunks: [String] = []
chunks.append("CREATE VIRTUAL TABLE")
if ifNotExists {
chunks.append("IF NOT EXISTS")
}
chunks.append(name.quotedDatabaseIdentifier)
chunks.append("USING")
chunks.append(module)
let sql = chunks.joined(separator: " ")
try execute(sql: sql)
}
/// Creates a virtual database table.
///
/// The type of the argument of the `body` function depends on the type of
/// the `module` argument: refer to this module's documentation.
///
/// You can use this method to create full-text virtual tables:
///
/// ```swift
/// try db.create(virtualTable: "book", using: FTS4()) { t in
/// t.column("title")
/// t.column("author")
/// t.column("body")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html>
///
/// - parameters:
/// - name: The table name.
/// - ifNotExists: If false (the default), an error is thrown if the
/// table already exists. Otherwise, the table is created unless it
/// already exists.
/// - module: a virtual module.
/// - body: An optional closure that defines the virtual table.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create<Module: VirtualTableModule>(
virtualTable tableName: String,
ifNotExists: Bool = false,
using module: Module,
_ body: ((Module.TableDefinition) throws -> Void)? = nil)
throws
{
// Define virtual table
let configuration = VirtualTableConfiguration(ifNotExists: ifNotExists)
let definition = module.makeTableDefinition(configuration: configuration)
if let body {
try body(definition)
}
// Create virtual table
var chunks: [String] = []
chunks.append("CREATE VIRTUAL TABLE")
if ifNotExists {
chunks.append("IF NOT EXISTS")
}
chunks.append(tableName.quotedDatabaseIdentifier)
chunks.append("USING")
let arguments = try module.moduleArguments(for: definition, in: self)
if arguments.isEmpty {
chunks.append(module.moduleName)
} else {
chunks.append(module.moduleName + "(" + arguments.joined(separator: ", ") + ")")
}
let sql = chunks.joined(separator: " ")
try inSavepoint {
try execute(sql: sql)
try module.database(self, didCreate: tableName, using: definition)
return .commit
}
}
}
@@ -0,0 +1,845 @@
// MARK: - Associations to TableRecord
extension TableRecord {
/// Creates a ``BelongsToAssociation`` between `Self` and the
/// destination `TableRecord` type.
///
/// For example:
///
/// ```swift
/// struct Author: TableRecord { }
/// struct Book: TableRecord {
/// static let author = belongsTo(Author.self)
/// }
/// ```
///
/// The association lets you define requests that involve both the source
/// and the destination type.
///
/// For example, we can fetch all books with their author:
///
/// ```swift
/// struct BookInfo: FetchableRecord, Decodable {
/// var book: Book
/// var author: Author
/// }
///
/// try dbQueue.read { db in
/// let request = Book
/// .including(required: Book.author)
/// .asRequest(of: BookInfo.self)
/// let bookInfos = try request.fetchAll(db)
/// for bookInfo in bookInfos {
/// print("\(bookInfo.book.title) by \(bookInfo.author.name)")
/// }
/// }
/// ```
///
/// The association can also help fetching associated records:
///
/// ```swift
/// try dbQueue.read { db in
/// let book: Book = ...
/// let author: Author? = book
/// .request(for: Book.author)
/// .fetchOne(db)
/// }
/// ```
///
/// For more information about this association,
/// see ``BelongsToAssociation``.
///
/// Methods that build requests involving associations are defined in the
/// ``JoinableRequest`` protocol.
///
/// - parameters:
/// - destination: The record type at the other side of the association.
/// - key: The association key. By default, it
/// is `Destination.databaseTableName`.
/// - foreignKey: An eventual foreign key. You need to provide one when
/// no foreign key exists to the destination table, or several foreign
/// keys exist.
public static func belongsTo<Destination>(
_ destination: Destination.Type,
key: String? = nil,
using foreignKey: ForeignKey? = nil)
-> BelongsToAssociation<Self, Destination>
where Destination: TableRecord
{
BelongsToAssociation(
to: Destination.relationForAll,
key: key,
using: foreignKey)
}
/// Creates a ``HasManyAssociation`` between `Self` and the
/// destination `TableRecord` type.
///
/// For example:
///
/// ```swift
/// struct Book: TableRecord { }
/// struct Author: TableRecord {
/// static let books = hasMany(Book.self)
/// }
/// ```
///
/// The association lets you define requests that involve both the source
/// and the destination type.
///
/// For example, we can fetch all authors with all their books:
///
/// ```swift
/// struct AuthorInfo: FetchableRecord, Decodable {
/// var author: Author
/// var books: [Book]
/// }
///
/// try dbQueue.read { db in
/// let request = Author
/// .including(all: Author.books)
/// .asRequest(of: AuthorInfo.self)
/// let authorInfos = try request.fetchAll(db)
/// for authorInfo in authorInfos {
/// print("\(authorInfo.author.name) wrote \(authorInfo.books.count) books")
/// }
/// }
/// ```
///
/// The association can also help fetching associated records:
///
/// ```swift
/// try dbQueue.read { db in
/// let author: Author = ...
/// let books: [Book] = author
/// .request(for: Author.books)
/// .fetchAll(db)
/// }
/// ```
///
/// For more information about this association,
/// see ``HasManyAssociation``.
///
/// Methods that build requests involving associations are defined in the
/// ``JoinableRequest`` protocol.
///
/// - parameters:
/// - destination: The record type at the other side of the association.
/// - key: The association key. By default, it
/// is `Destination.databaseTableName`.
/// - foreignKey: An eventual foreign key. You need to provide one when
/// no foreign key exists to the destination table, or several foreign
/// keys exist.
public static func hasMany<Destination>(
_ destination: Destination.Type,
key: String? = nil,
using foreignKey: ForeignKey? = nil)
-> HasManyAssociation<Self, Destination>
where Destination: TableRecord
{
HasManyAssociation(
to: Destination.relationForAll,
key: key,
using: foreignKey)
}
/// Creates a ``HasOneAssociation`` between `Self` and the
/// destination `TableRecord` type.
///
/// For example:
///
/// ```swift
/// struct Demographics: TableRecord { }
/// struct Country: TableRecord {
/// static let demographics = hasOne(Demographics.self)
/// }
/// ```
///
/// The association lets you define requests that involve both the source
/// and the destination type.
///
/// For example, we can fetch all countries with their eventual demographics:
///
/// ```swift
/// struct CountryInfo: FetchableRecord, Decodable {
/// var country: Country
/// var demographics: Demographics?
/// }
///
/// try dbQueue.read { db in
/// let request = Country
/// .including(optional: Country.demographics)
/// .asRequest(of: CountryInfo.self)
/// let countryInfos = try request.fetchAll(db)
/// for countryInfo in countryInfos {
/// if let demographics = countryInfo.demographics {
/// print("""
/// \(countryInfo.country.name) has \
/// \(demographics.population) citizens.
/// """)
/// }
/// }
/// }
/// ```
///
/// The association can also help fetching associated records:
///
/// ```swift
/// try dbQueue.read { db in
/// let country: Country = ...
/// let demographics: Demographics? = country
/// .request(for: Country.demographics)
/// .fetchOne(db)
/// }
/// ```
///
/// For more information about this association,
/// see ``HasOneAssociation``.
///
/// Methods that build requests involving associations are defined in the
/// ``JoinableRequest`` protocol.
///
/// - parameters:
/// - destination: The record type at the other side of the association.
/// - key: The association key. By default, it
/// is `Destination.databaseTableName`.
/// - foreignKey: An eventual foreign key. You need to provide one when
/// no foreign key exists to the destination table, or several foreign
/// keys exist.
public static func hasOne<Destination>(
_ destination: Destination.Type,
key: String? = nil,
using foreignKey: ForeignKey? = nil)
-> HasOneAssociation<Self, Destination>
where Destination: TableRecord
{
HasOneAssociation(
to: Destination.relationForAll,
key: key,
using: foreignKey)
}
}
// MARK: - Associations to Table
extension TableRecord {
/// Creates a ``BelongsToAssociation`` between `Self` and the
/// destination `Table`.
///
/// For more information, see ``TableRecord/belongsTo(_:key:using:)-13t5r``.
///
/// - parameters:
/// - destination: The table at the other side of the association.
/// - key: The association key. By default, it
/// is `destination.tableName`.
/// - foreignKey: An eventual foreign key. You need to provide one when
/// no foreign key exists to the destination table, or several foreign
/// keys exist.
public static func belongsTo<Destination>(
_ destination: Table<Destination>,
key: String? = nil,
using foreignKey: ForeignKey? = nil)
-> BelongsToAssociation<Self, Destination>
{
BelongsToAssociation(
to: destination.relationForAll,
key: key,
using: foreignKey)
}
/// Creates a ``HasManyAssociation`` between `Self` and the
/// destination `Table`.
///
/// For more information, see ``TableRecord/hasMany(_:key:using:)-45axo``.
///
/// - parameters:
/// - destination: The table at the other side of the association.
/// - key: The association key. By default, it
/// is `destination.tableName`.
/// - foreignKey: An eventual foreign key. You need to provide one when
/// no foreign key exists to the destination table, or several foreign
/// keys exist.
public static func hasMany<Destination>(
_ destination: Table<Destination>,
key: String? = nil,
using foreignKey: ForeignKey? = nil)
-> HasManyAssociation<Self, Destination>
{
HasManyAssociation(
to: destination.relationForAll,
key: key,
using: foreignKey)
}
/// Creates a ``HasOneAssociation`` between `Self` and the
/// destination `Table`.
///
/// For more information, see ``TableRecord/hasOne(_:key:using:)-4g9tm``.
///
/// - parameters:
/// - destination: The table at the other side of the association.
/// - key: The association key. By default, it
/// is `destination.tableName`.
/// - foreignKey: An eventual foreign key. You need to provide one when
/// no foreign key exists to the destination table, or several foreign
/// keys exist.
public static func hasOne<Destination>(
_ destination: Table<Destination>,
key: String? = nil,
using foreignKey: ForeignKey? = nil)
-> HasOneAssociation<Self, Destination>
{
HasOneAssociation(
to: destination.relationForAll,
key: key,
using: foreignKey)
}
}
// MARK: - Associations to CommonTableExpression
extension TableRecord {
/// Creates an association to a common table expression.
///
/// The key of the returned association is the table name of the common
/// table expression.
///
/// 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
/// .with(latestMessageCTE)
/// .including(optional: latestMessageAssociation)
/// ```
///
/// - parameter cte: A common table expression.
/// - parameter condition: A function that returns the joining clause.
/// - parameter left: A `TableAlias` for the left table.
/// - parameter right: A `TableAlias` for the right table.
/// - returns: An association to the common table expression.
public static func association<Destination>(
to cte: CommonTableExpression<Destination>,
on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible)
-> JoinAssociation<Self, Destination>
{
JoinAssociation(
to: cte.relationForAll,
condition: .expression { condition($0, $1).sqlExpression })
}
/// Creates an association to a common table expression.
///
/// The key of the returned association is the table name of the common
/// table expression.
///
/// - parameter cte: A common table expression.
/// - returns: An association to the common table expression.
public static func association<Destination>(
to cte: CommonTableExpression<Destination>)
-> JoinAssociation<Self, Destination>
{
JoinAssociation(to: cte.relationForAll, condition: .none)
}
}
// MARK: - "Through" Associations
extension TableRecord {
/// Creates a ``HasManyThroughAssociation`` between `Self` and the
/// destination `TableRecord` type.
///
/// For example:
///
/// ```swift
/// struct Citizen: TableRecord { }
///
/// struct Passport: TableRecord {
/// static let citizen = belongsTo(Citizen.self)
/// }
///
/// struct Country: TableRecord {
/// static let passports = hasMany(Passport.self)
/// static let citizens = hasMany(Citizen.self,
/// through: passports,
/// using: Passport.citizen)
/// }
/// ```
///
/// The association lets you define requests that involve both the source
/// and the destination type.
///
/// For example, we can fetch all countries with all their citizens:
///
/// ```swift
/// struct CountryInfo: FetchableRecord, Decodable {
/// var country: Country
/// var citizens: [Citizen]
/// }
///
/// try dbQueue.read { db in
/// let request = Country
/// .including(all: Country.citizens)
/// .asRequest(of: CountryInfo.self)
/// let countryInfos = try request.fetchAll(db)
/// for countryInfo in countryInfos {
/// print("\(countryInfo.country.name) has \(countryInfo.citizens.count) citizens")
/// }
/// }
/// ```
///
/// The association can also help fetching associated records:
///
/// ```swift
/// try dbQueue.read { db in
/// let country: Country = ...
/// let citizens: [Citizen] = country
/// .request(for: Country.citizens)
/// .fetchAll(db)
/// }
/// ```
///
/// For more information about this association,
/// see ``HasManyThroughAssociation``.
///
/// Methods that build requests involving associations are defined in the
/// ``JoinableRequest`` protocol.
///
/// - parameters:
/// - destination: The record type at the other side of the association.
/// - pivot: An association from `Self` to the intermediate type.
/// - target: A target association from the intermediate type to the
/// destination type.
/// - key: The association key. By default, it is the key of the target.
public static func hasMany<Pivot, Target>(
_ destination: Target.RowDecoder.Type,
through pivot: Pivot,
using target: Target,
key: String? = nil)
-> HasManyThroughAssociation<Self, Target.RowDecoder>
where Pivot: Association,
Target: Association,
Pivot.OriginRowDecoder == Self,
Pivot.RowDecoder == Target.OriginRowDecoder
{
let association = HasManyThroughAssociation(through: pivot, using: target)
if let key {
return association.forKey(key)
} else {
return association
}
}
/// Creates a ``HasOneThroughAssociation`` between `Self` and the
/// destination `TableRecord` type.
///
/// For example:
///
/// ```swift
/// struct Address: TableRecord { }
///
/// struct Library: TableRecord {
/// static let address = hasOne(Address.self)
/// }
///
/// struct Book: TableRecord {
/// static let library = belongsTo(Library.self)
/// static let returnAddress = hasOne(Address.self,
/// through: library,
/// using: Library.address,
/// key: "returnAddress")
/// }
/// ```
///
/// The association lets you define requests that involve both the source
/// and the destination type.
///
/// For example, we can fetch all books with their return address:
///
/// ```swift
/// struct BookInfo: FetchableRecord, Decodable {
/// var book: Book
/// var returnAddress: Address
/// }
///
/// try dbQueue.read { db in
/// let request = Book
/// .including(required: Book.returnAddress)
/// .asRequest(of: BookInfo.self)
/// let bookInfos = try request.fetchAll(db)
/// for bookInfo in bookInfos {
/// print("\(bookInfo.book.title) must return to \(bookInfo.returnAddress)")
/// }
/// }
/// ```
///
/// The association can also help fetching associated records:
///
/// ```swift
/// try dbQueue.read { db in
/// let book: Book = ...
/// let returnAddress: Address? = book
/// .request(for: Book.returnAddress)
/// .fetchOne(db)
/// }
/// ```
///
/// For more information about this association,
/// see ``HasOneThroughAssociation``.
///
/// Methods that build requests involving associations are defined in the
/// ``JoinableRequest`` protocol.
///
/// - parameters:
/// - destination: The record type at the other side of the association.
/// - pivot: An association from Self to the intermediate type.
/// - target: A target association from the intermediate type to the
/// destination type.
/// - key: The association key. By default, it is the key of the target.
public static func hasOne<Pivot, Target>(
_ destination: Target.RowDecoder.Type,
through pivot: Pivot,
using target: Target,
key: String? = nil)
-> HasOneThroughAssociation<Self, Target.RowDecoder>
where Pivot: AssociationToOne,
Target: AssociationToOne,
Pivot.OriginRowDecoder == Self,
Pivot.RowDecoder == Target.OriginRowDecoder
{
let association = HasOneThroughAssociation(through: pivot, using: target)
if let key {
return association.forKey(key)
} else {
return association
}
}
}
// MARK: - Request for associated records
extension TableRecord where Self: EncodableRecord {
/// Returns a request for the associated record(s).
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord, FetchableRecord { }
/// struct Team: TableRecord, EncodableRecord {
/// static let players = hasMany(Player.self)
/// }
///
/// try dbQueue.read { db in
/// let team: Team = ...
/// let players: [Player] = try team
/// .request(for: Team.players)
/// .fetchAll(db)
/// }
/// ```
public func request<A: Association>(for association: A)
-> QueryInterfaceRequest<A.RowDecoder>
where A.OriginRowDecoder == Self
{
switch association._sqlAssociation.pivot.condition {
case .expression:
// TODO: find a use case?
fatalError("Not implemented: request association without any foreign key")
case let .foreignKey(foreignKey):
let destinationRelation = association
._sqlAssociation
.with {
$0.pivot.relation = $0.pivot.relation.filterWhenConnected { db in
// Filter the pivot on self
try foreignKey
.joinMapping(db, from: Self.databaseTableName)
.joinExpression(leftRows: [PersistenceContainer(db, self)])
}
}
.destinationRelation()
return QueryInterfaceRequest(relation: destinationRelation)
}
}
}
// MARK: - Joining Methods
extension TableRecord {
/// 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
/// .including(all: Author.books)
/// .asRequest(of: AuthorInfo.self)
/// .fetchAll(db)
/// ```
public static func including<A: AssociationToMany>(all association: A)
-> QueryInterfaceRequest<Self>
where A.OriginRowDecoder == Self
{
all().including(all: association)
}
/// 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
/// .including(optional: Book.author)
/// .asRequest(of: BookInfo.self)
/// .fetchAll(db)
/// ```
public static func including<A: Association>(optional association: A)
-> QueryInterfaceRequest<Self>
where A.OriginRowDecoder == Self
{
all().including(optional: association)
}
/// 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
/// .including(required: Book.author)
/// .asRequest(of: BookInfo.self)
/// .fetchAll(db)
/// ```
public static func including<A: Association>(required association: A)
-> QueryInterfaceRequest<Self>
where A.OriginRowDecoder == Self
{
all().including(required: association)
}
/// Returns a request that joins each record of this request to its
/// eventual associated record.
public static func joining<A: Association>(optional association: A)
-> QueryInterfaceRequest<Self>
where A.OriginRowDecoder == Self
{
all().joining(optional: association)
}
/// 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
/// .joining(required: frenchAuthors)
/// .fetchAll(db)
/// ```
public static func joining<A: Association>(required association: A)
-> QueryInterfaceRequest<Self>
where A.OriginRowDecoder == Self
{
all().joining(required: association)
}
/// Returns a request with the columns of the eventual associated record
/// appended to the record selection.
///
/// The record selection is determined by
/// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns.
///
/// For example:
///
/// ```swift
/// // SELECT player.*, team.color
/// // FROM player LEFT JOIN team ...
/// let teamColor = Player.team.select(Column("color"))
/// let request = Player.annotated(withOptional: teamColor)
/// ```
///
/// See ``JoinableRequest/annotated(withOptional:)`` for more information.
public static func annotated<A: Association>(withOptional association: A)
-> QueryInterfaceRequest<Self>
where A.OriginRowDecoder == Self
{
all().annotated(withOptional: association)
}
/// Returns a request with the columns of the associated record appended to
/// the record selection. Records that do not have an associated record
/// are discarded.
///
/// The record selection is determined by
/// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns.
///
/// For example:
///
/// ```swift
/// // SELECT player.*, team.color
/// // FROM player JOIN team ...
/// let teamColor = Player.team.select(Column("color"))
/// let request = Player.annotated(withRequired: teamColor)
/// ```
///
/// See ``JoinableRequest/annotated(withRequired:)`` for more information.
public static func annotated<A: Association>(withRequired association: A)
-> QueryInterfaceRequest<Self>
where A.OriginRowDecoder == Self
{
all().annotated(withRequired: association)
}
}
// MARK: - Aggregates
extension TableRecord {
/// Returns a request with the given association aggregates appended to
/// the record selection.
///
/// The record selection is determined by
/// ``TableRecord/databaseSelection-7iphs``, which defaults to all 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
/// .annotated(with: Author.books.count)
/// .asRequest(of: AuthorInfo.self)
/// .fetchAll(db)
/// ```
public static func annotated(with aggregates: AssociationAggregate<Self>...) -> QueryInterfaceRequest<Self> {
all().annotated(with: aggregates)
}
/// Returns a request with the given association aggregates appended to
/// the record selection.
///
/// The record selection is determined by
/// ``TableRecord/databaseSelection-7iphs``, which defaults to all 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
/// .annotated(with: [Author.books.count])
/// .asRequest(of: AuthorInfo.self)
/// .fetchAll(db)
/// ```
public static func annotated(with aggregates: [AssociationAggregate<Self>]) -> QueryInterfaceRequest<Self> {
all().annotated(with: aggregates)
}
/// Returns a request filtered according to the provided
/// 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
/// .having(Author.books.count > 5)
/// .fetchAll(db)
/// ```
public static func having(_ predicate: AssociationAggregate<Self>) -> QueryInterfaceRequest<Self> {
all().having(predicate)
}
}
@@ -0,0 +1,659 @@
extension TableRecord {
// MARK: Request Derivation
static var relationForAll: SQLRelation {
.all(fromTable: databaseTableName, selection: { _ in databaseSelection.map(\.sqlSelection) })
}
/// Returns a request for all records in the table.
///
/// The record selection is determined by
/// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// try dbQueue.read { db in
/// // SELECT * FROM player
/// let request = Player.all()
/// }
public static func all() -> QueryInterfaceRequest<Self> {
QueryInterfaceRequest(relation: relationForAll)
}
/// Returns an empty request that fetches no record.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord, FetchableRecord { }
///
/// try dbQueue.read { db in
/// let request = Player.none()
/// let players = try request.fetchAll(db) // empty array
/// }
public static func none() -> QueryInterfaceRequest<Self> {
all().none() // don't laugh
}
/// Returns a request that selects the provided result columns.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT id, score FROM player
/// let request = Player.select(Column("id"), Column("score"))
/// ```
public static func select(_ selection: any SQLSelectable...) -> QueryInterfaceRequest<Self> {
all().select(selection)
}
/// Returns a request that selects the provided result columns.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT id, score FROM player
/// let request = Player.select([Column("id"), Column("score")])
/// ```
public static func select(_ selection: [any SQLSelectable]) -> QueryInterfaceRequest<Self> {
all().select(selection)
}
/// Returns a request that selects the provided SQL string.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT id, name FROM player
/// let request = Player.select(sql: "id, name")
///
/// // SELECT id, IFNULL(name, 'Anonymous') FROM player
/// let defaultName = "Anonymous"
/// let request = Player.select(sql: "id, IFNULL(name, ?)", arguments: [defaultName])
/// ```
public static func select(
sql: String,
arguments: StatementArguments = StatementArguments())
-> QueryInterfaceRequest<Self>
{
all().select(SQL(sql: sql, arguments: arguments))
}
/// Returns a request that selects the provided ``SQL`` literal.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT id, IFNULL(name, 'Anonymous') FROM player
/// let defaultName = "Anonymous"
/// let request = Player.select(literal: "id, IFNULL(name, \(defaultName))")
/// ```
public static func select(literal sqlLiteral: SQL) -> QueryInterfaceRequest<Self> {
all().select(sqlLiteral)
}
/// Returns a request that selects the provided result columns, and defines
/// the type of decoded rows.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// let minScore = min(Column("score"))
/// let maxScore = max(Column("score"))
///
/// // SELECT MAX(score) FROM player
/// let request = Player.select([maxScore], as: Int.self)
/// let maxScore = try request.fetchOne(db) // Int?
///
/// // SELECT MIN(score), MAX(score) FROM player
/// let request = Player.select([minScore, maxScore], as: Row.self)
/// if let row = try request.fetchOne(db) {
/// let minScore: Int = row[0]
/// let maxScore: Int = row[1]
/// }
/// ```
public static func select<RowDecoder>(
_ selection: [any SQLSelectable],
as type: RowDecoder.Type = RowDecoder.self)
-> QueryInterfaceRequest<RowDecoder>
{
all().select(selection, as: type)
}
/// Returns a request that selects the provided result columns, and defines
/// the type of decoded rows.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// let minScore = min(Column("score"))
/// let maxScore = max(Column("score"))
///
/// // SELECT MAX(score) FROM player
/// let request = Player.select(maxScore, as: Int.self)
/// let maxScore = try request.fetchOne(db) // Int?
///
/// // SELECT MIN(score), MAX(score) FROM player
/// let request = Player.select(minScore, maxScore, as: Row.self)
/// if let row = try request.fetchOne(db) {
/// let minScore: Int = row[0]
/// let maxScore: Int = row[1]
/// }
/// ```
public static func select<RowDecoder>(
_ selection: any SQLSelectable...,
as type: RowDecoder.Type = RowDecoder.self)
-> QueryInterfaceRequest<RowDecoder>
{
all().select(selection, as: type)
}
/// Returns a request that selects the provided SQL string, and defines the
/// type of decoded rows.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT name FROM player
/// let request = Player.select(sql: "name", as: String.self)
/// let names = try request.fetchAll(db) // [String]
///
/// // SELECT IFNULL(name, 'Anonymous') FROM player
/// let defaultName = "Anonymous"
/// let request = Player.select(sql: "IFNULL(name, ?)", arguments: [defaultName], as: String.self)
/// let names = try request.fetchAll(db) // [String]
/// ```
public static func select<RowDecoder>(
sql: String,
arguments: StatementArguments = StatementArguments(),
as type: RowDecoder.Type = RowDecoder.self)
-> QueryInterfaceRequest<RowDecoder>
{
all().select(SQL(sql: sql, arguments: arguments), as: type)
}
/// Returns a request that selects the provided ``SQL`` literal, and defines
/// the type of decoded rows.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT IFNULL(name, 'Anonymous') FROM player
/// let defaultName = "Anonymous"
/// let request = Player.select(literal: "IFNULL(name, \(defaultName))", as: String.self)
/// let names = try request.fetchAll(db) // [String]
/// ```
public static func select<RowDecoder>(
literal sqlLiteral: SQL,
as type: RowDecoder.Type = RowDecoder.self)
-> QueryInterfaceRequest<RowDecoder>
{
all().select(sqlLiteral, as: type)
}
/// Returns a request that selects the primary key.
///
/// All primary keys are supported:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Country: TableRecord { }
/// struct Citizenship: TableRecord { }
///
/// // SELECT id FROM player WHERE ...
/// let request = try Player.selectPrimaryKey(as: Int64.self)
/// let ids = try request.fetchAll(db) // [Int64]
///
/// // SELECT code FROM country WHERE ...
/// let request = try Country.selectPrimaryKey(as: String.self)
/// let countryCodes = try request.fetchAll(db) // [String]
///
/// // SELECT citizenId, countryCode FROM citizenship WHERE ...
/// let request = try Citizenship.selectPrimaryKey(as: Row.self)
/// let rows = try request.fetchAll(db) // [Row]
/// ```
///
/// For composite primary keys, you can define a ``FetchableRecord`` type:
///
/// ```swift
/// extension Citizenship {
/// struct ID: Decodable, FetchableRecord {
/// var citizenId: Int64
/// var countryCode: String
/// }
/// }
/// let request = try Citizenship.selectPrimaryKey(as: Citizenship.ID.self)
/// let ids = try request.fetchAll(db) // [Citizenship.ID]
/// ```
public static func selectPrimaryKey<PrimaryKey>(as type: PrimaryKey.Type = PrimaryKey.self)
-> QueryInterfaceRequest<PrimaryKey>
{
all().selectPrimaryKey(as: type)
}
/// Returns a request with the provided result columns appended to the
/// record selection.
///
/// The record selection is determined by
/// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT *, score + bonus AS totalScore FROM player
/// let totalScore = (Column("score") + Column("bonus")).forKey("totalScore")
/// let request = Player.annotated(with: [totalScore])
/// ```
public static func annotated(with selection: [any SQLSelectable]) -> QueryInterfaceRequest<Self> {
all().annotated(with: selection)
}
/// Returns a request with the provided result columns appended to the
/// record selection.
///
/// The record selection is determined by
/// ``TableRecord/databaseSelection-7iphs``, which defaults to all columns.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT *, score + bonus AS totalScore FROM player
/// let totalScore = (Column("score") + Column("bonus")).forKey("totalScore")
/// let request = Player.annotated(with: totalScore)
/// ```
public static func annotated(with selection: any SQLSelectable...) -> QueryInterfaceRequest<Self> {
all().annotated(with: selection)
}
// Accept SQLSpecificExpressible instead of SQLExpressible, so that we
// prevent the `Player.filter(42)` misuse.
// See https://github.com/groue/GRDB.swift/pull/864
/// Returns a request filtered with a boolean SQL expression.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT * FROM player WHERE name = 'O''Brien'
/// let name = "O'Brien"
/// let request = Player.filter(Column("name") == name)
/// ```
public static func filter(_ predicate: some SQLSpecificExpressible) -> QueryInterfaceRequest<Self> {
all().filter(predicate)
}
/// Returns a request filtered by primary key.
///
/// All single-column primary keys are supported:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Country: TableRecord { }
///
/// // SELECT * FROM player WHERE id = 1
/// let request = Player.filter(key: 1)
///
/// // SELECT * FROM country WHERE code = 'FR'
/// let request = Country.filter(key: "FR")
/// ```
///
/// - parameter key: A primary key
public static func filter(key: some DatabaseValueConvertible) -> QueryInterfaceRequest<Self> {
all().filter(key: key)
}
/// Returns a request filtered by primary key.
///
/// All single-column primary keys are supported:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Country: TableRecord { }
///
/// // SELECT * FROM player WHERE id = IN (1, 2, 3)
/// let request = Player.filter(keys: [1, 2, 3])
///
/// // SELECT * FROM country WHERE code = IN ('FR', 'US')
/// let request = Country.filter(keys: ["FR", "US"])
/// ```
///
/// - parameter keys: A collection of primary keys
public static func filter<Keys>(keys: Keys)
-> QueryInterfaceRequest<Self>
where Keys: Sequence, Keys.Element: DatabaseValueConvertible
{
all().filter(keys: keys)
}
/// Returns a request filtered by primary or unique key.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Citizenship: TableRecord { }
///
/// // SELECT * FROM player WHERE id = 1
/// let request = Player.filter(key: ["id": 1])
///
/// // SELECT * FROM player WHERE email = 'arthur@example.com'
/// let request = Player.filter(key: ["email": "arthur@example.com"])
///
/// // SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
/// let request = Citizenship.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 static func filter(key: [String: (any DatabaseValueConvertible)?]?) -> QueryInterfaceRequest<Self> {
all().filter(key: key)
}
/// Returns a request filtered by primary or unique key.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Citizenship: TableRecord { }
///
/// // SELECT * FROM player WHERE id = 1
/// let request = Player.filter(keys: [["id": 1]])
///
/// // SELECT * FROM player WHERE email = 'arthur@example.com'
/// let request = Player.filter(keys: [["email": "arthur@example.com"]])
///
/// // SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
/// let request = Citizenship.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 static func filter(keys: [[String: (any DatabaseValueConvertible)?]]) -> QueryInterfaceRequest<Self> {
all().filter(keys: keys)
}
/// Returns a request filtered with an SQL string.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT * FROM player WHERE name = 'O''Brien'
/// let name = "O'Brien"
/// let request = Player.filter(sql: "name = ?", arguments: [name])
/// ```
public static func filter(
sql: String,
arguments: StatementArguments = StatementArguments())
-> QueryInterfaceRequest<Self>
{
filter(SQL(sql: sql, arguments: arguments))
}
/// Returns a request filtered 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
/// struct Player: TableRecord { }
///
/// // SELECT * FROM player WHERE name = 'O''Brien'
/// let name = "O'Brien"
/// let request = Player.filter(literal: "name = \(name)")
/// ```
public static func filter(literal sqlLiteral: SQL) -> QueryInterfaceRequest<Self> {
// NOT TESTED
all().filter(sqlLiteral)
}
/// Returns a request sorted according to the given SQL ordering terms.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.order(Column("score").desc, Column("name"))
/// ```
public static func order(_ orderings: any SQLOrderingTerm...) -> QueryInterfaceRequest<Self> {
all().order(orderings)
}
/// Returns a request sorted according to the given SQL ordering terms.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.order([Column("score").desc, Column("name")])
/// ```
public static func order(_ orderings: [any SQLOrderingTerm]) -> QueryInterfaceRequest<Self> {
all().order(orderings)
}
/// Returns a request sorted by primary key.
///
/// All primary keys are supported:
///
/// ```swift
/// struct Player: TableRecord { }
/// struct Country: TableRecord { }
/// struct Citizenship: TableRecord { }
///
/// // SELECT * FROM player ORDER BY id
/// let request = Player.orderByPrimaryKey()
///
/// // SELECT * FROM country ORDER BY code
/// let request = Country.orderByPrimaryKey()
///
/// // SELECT * FROM citizenship ORDER BY citizenId, countryCode
/// let request = Citizenship.orderByPrimaryKey()
/// ```
public static func orderByPrimaryKey() -> QueryInterfaceRequest<Self> {
all().orderByPrimaryKey()
}
/// Returns a request sorted according to the given SQL string.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.order(sql: "score DESC, name")
/// ```
public static func order(
sql: String,
arguments: StatementArguments = StatementArguments())
-> QueryInterfaceRequest<Self>
{
all().order(SQL(sql: sql, arguments: arguments))
}
/// Returns a request sorted according to the given ``SQL`` literal.
///
/// For example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT * FROM player ORDER BY score DESC, name
/// let request = Player.order(literal: "score DESC, name")
/// ```
public static func order(literal sqlLiteral: SQL) -> QueryInterfaceRequest<Self> {
all().order(sqlLiteral)
}
/// Returns a limited request.
///
/// The returned request fetches `limit` rows, starting at `offset`. For
/// example:
///
/// ```swift
/// struct Player: TableRecord { }
///
/// // SELECT * FROM player LIMIT 10
/// let request = Player.limit(10)
///
/// // SELECT * FROM player LIMIT 10 OFFSET 20
/// let request = Player.limit(10, offset: 20)
/// ```
public static func limit(_ limit: Int, offset: Int? = nil) -> QueryInterfaceRequest<Self> {
all().limit(limit, offset: offset)
}
/// Returns a request that can be referred to with the provided alias.
///
/// Use this method when you need to refer to this table from
/// another request.
///
/// For example, the request below fetches posthumous books:
///
/// ```swift
/// struct Author: TableRecord { }
/// struct Book: TableRecord {
/// static let author = belongsTo(Author.self)
/// }
///
/// // SELECT book.*
/// // FROM book
/// // JOIN author ON author.id = book.authorId
/// // AND author.deathDate <= book.publishDate
/// let bookAlias = TableAlias()
/// let request = Book
/// .aliased(bookAlias)
/// .joining(required: Book.author.filter(Column("deathDate") <= bookAlias[Column("publishDate")])
/// ```
///
/// See ``TableRequest/aliased(_:)`` for more information.
public static func aliased(_ alias: TableAlias) -> QueryInterfaceRequest<Self> {
all().aliased(alias)
}
/// Returns a request that embeds a common table expression.
///
/// 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
/// .with(latestMessageCTE)
/// .including(optional: latestMessageAssociation)
/// ```
public static func with<RowDecoder>(_ cte: CommonTableExpression<RowDecoder>) -> QueryInterfaceRequest<Self> {
all().with(cte)
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible {
/// Returns a request filtered by primary key.
///
/// All single-column primary keys are supported:
///
/// ```swift
/// struct Player: TableRecord, Identifiable {
/// var id: Int64
/// }
/// struct Country: TableRecord, Identifiable {
/// var id: String
/// }
///
/// // SELECT * FROM player WHERE id = 1
/// let request = Player.filter(id: 1)
///
/// // SELECT * FROM country WHERE code = 'FR'
/// let request = Country.filter(id: "FR")
/// ```
///
/// - parameter id: A primary key
public static func filter(id: ID) -> QueryInterfaceRequest<Self> {
all().filter(id: id)
}
/// Returns a request filtered by primary key.
///
/// All single-column primary keys are supported:
///
/// ```swift
/// struct Player: TableRecord, Identifiable {
/// var id: Int64
/// }
/// struct Country: TableRecord, Identifiable {
/// var id: String
/// }
///
/// // SELECT * FROM player WHERE id = IN (1, 2, 3)
/// let request = Player.filter(ids: [1, 2, 3])
///
/// // SELECT * FROM country WHERE code = IN ('FR', 'US')
/// let request = Country.filter(ids: ["FR", "US"])
/// ```
///
/// - parameter ids: A collection of primary keys
public static func filter<IDS>(ids: IDS) -> QueryInterfaceRequest<Self>
where IDS: Collection, IDS.Element == ID
{
all().filter(ids: ids)
}
}