add swiftUI code
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
+314
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+883
@@ -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)
|
||||
}
|
||||
+94
@@ -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
|
||||
}
|
||||
+93
@@ -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
|
||||
}
|
||||
+66
@@ -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
|
||||
}
|
||||
+100
@@ -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
|
||||
}
|
||||
+47
@@ -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
|
||||
}
|
||||
+22
@@ -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
|
||||
}
|
||||
+482
@@ -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)
|
||||
}
|
||||
}
|
||||
+1698
File diff suppressed because it is too large
Load Diff
+1570
File diff suppressed because it is too large
Load Diff
@@ -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
+177
@@ -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
+145
@@ -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: " ")
|
||||
}
|
||||
}
|
||||
+567
@@ -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
|
||||
}
|
||||
}
|
||||
+42
@@ -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
|
||||
}
|
||||
}
|
||||
+825
@@ -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
|
||||
}
|
||||
}
|
||||
+84
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+476
@@ -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) }
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
+328
@@ -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 { }
|
||||
+694
@@ -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)
|
||||
}
|
||||
+111
@@ -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 { }
|
||||
+159
@@ -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)
|
||||
}
|
||||
}
|
||||
+659
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user