add swiftUI code
This commit is contained in:
@@ -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
Reference in New Issue
Block a user