1699 lines
60 KiB
Swift
1699 lines
60 KiB
Swift
// QueryInterfaceRequest is the type of requests generated by `Table`
|
|
// and `TableRecord`:
|
|
//
|
|
// struct Player: TableRecord { ... }
|
|
// let playerRequest = Player.all() // QueryInterfaceRequest<Player>
|
|
//
|
|
// It wraps an SQLQuery, and has an attached type.
|
|
//
|
|
// The attached RowDecoder type helps decoding raw database values:
|
|
//
|
|
// try dbQueue.read { db in
|
|
// try playerRequest.fetchAll(db) // [Player]
|
|
// }
|
|
//
|
|
// RowDecoder also helps the compiler validate associated requests:
|
|
//
|
|
// playerRequest.including(required: Player.team) // OK
|
|
// fruitRequest.including(required: Player.team) // Does not compile
|
|
|
|
/// A request that builds SQL queries with Swift.
|
|
///
|
|
/// You build a `QueryInterfaceRequest` from a ``TableRecord`` type, or a
|
|
/// ``Table`` instance. For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord, FetchableRecord, DecodableRecord { }
|
|
///
|
|
/// try dbQueue.read { db in
|
|
/// // SELECT * FROM player
|
|
/// // WHERE name = 'O''Reilly'
|
|
/// // ORDER BY score DESC
|
|
/// let request = Player
|
|
/// .filter(Column("name") == "O'Reilly")
|
|
/// .order(Column("score").desc)
|
|
/// let players: [Player] = try request.fetchAll(db)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Most features of `QueryInterfaceRequest` come from the protocols it
|
|
/// conforms to. In particular:
|
|
///
|
|
/// - **Fetching methods** are defined by ``FetchRequest``.
|
|
/// - **Request building methods** are defined by ``DerivableRequest``.
|
|
///
|
|
/// ## Topics
|
|
///
|
|
/// ### Instance Methods
|
|
///
|
|
/// - ``isEmpty(_:)``
|
|
/// - ``limit(_:offset:)``
|
|
///
|
|
/// ### Changing The Type of Fetched Results
|
|
///
|
|
/// - ``asRequest(of:)``
|
|
/// - ``select(_:as:)-282xc``
|
|
/// - ``select(_:as:)-3o8qw``
|
|
/// - ``select(literal:as:)``
|
|
/// - ``select(sql:arguments:as:)``
|
|
/// - ``selectPrimaryKey(as:)``
|
|
///
|
|
/// ### Batch Delete
|
|
///
|
|
/// - ``deleteAll(_:)``
|
|
/// - ``deleteAndFetchIds(_:)``
|
|
/// - ``deleteAndFetchCursor(_:)``
|
|
/// - ``deleteAndFetchAll(_:)``
|
|
/// - ``deleteAndFetchSet(_:)``
|
|
/// - ``deleteAndFetchStatement(_:selection:)``
|
|
///
|
|
/// ### Batch Update
|
|
///
|
|
/// - ``updateAll(_:onConflict:_:)-9r4v``
|
|
/// - ``updateAll(_:onConflict:_:)-49qg8``
|
|
/// - ``updateAndFetchCursor(_:onConflict:_:)``
|
|
/// - ``updateAndFetchAll(_:onConflict:_:)``
|
|
/// - ``updateAndFetchSet(_:onConflict:_:)``
|
|
/// - ``updateAndFetchStatement(_:onConflict:_:selection:)``
|
|
/// - ``ColumnAssignment``
|
|
public struct QueryInterfaceRequest<RowDecoder> {
|
|
var relation: SQLRelation
|
|
}
|
|
|
|
extension QueryInterfaceRequest: Refinable { }
|
|
|
|
extension QueryInterfaceRequest: FetchRequest {
|
|
public var sqlSubquery: SQLSubquery {
|
|
.relation(relation)
|
|
}
|
|
|
|
public func fetchCount(_ db: Database) throws -> Int {
|
|
try relation.fetchCount(db)
|
|
}
|
|
|
|
public func makePreparedRequest(
|
|
_ db: Database,
|
|
forSingleResult singleResult: Bool = false)
|
|
throws -> PreparedRequest
|
|
{
|
|
let generator = SQLQueryGenerator(relation: relation, forSingleResult: singleResult)
|
|
var preparedRequest = try generator.makePreparedRequest(db)
|
|
let associations = relation.prefetchedAssociations
|
|
if associations.isEmpty == false {
|
|
// Eager loading of prefetched associations
|
|
preparedRequest.supplementaryFetch = { [relation] db, rows, willExecuteSupplementaryRequest in
|
|
try prefetch(
|
|
db,
|
|
associations: associations,
|
|
from: relation,
|
|
into: rows,
|
|
willExecuteSupplementaryRequest: willExecuteSupplementaryRequest)
|
|
}
|
|
}
|
|
return preparedRequest
|
|
}
|
|
}
|
|
|
|
// MARK: - Request Derivation
|
|
|
|
extension QueryInterfaceRequest: SelectionRequest {
|
|
public func selectWhenConnected(
|
|
_ selection: @escaping (Database) throws -> [any SQLSelectable])
|
|
-> Self
|
|
{
|
|
with {
|
|
$0.relation = $0.relation.selectWhenConnected { db in
|
|
try selection(db).map(\.sqlSelection)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Defines the result columns, and defines the type of decoded rows.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let minScore = min(Column("score"))
|
|
/// let maxScore = max(Column("score"))
|
|
///
|
|
/// // SELECT MAX(score) FROM player
|
|
/// let request = Player.all()
|
|
/// .select([maxScore], as: Int.self)
|
|
/// let maxScore = try request.fetchOne(db) // Int?
|
|
///
|
|
/// // SELECT MIN(score), MAX(score) FROM player
|
|
/// let request = Player.all()
|
|
/// .select([minScore, maxScore], as: Row.self)
|
|
/// if let row = try request.fetchOne(db) {
|
|
/// let minScore: Int = row[0]
|
|
/// let maxScore: Int = row[1]
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Any previous selection is discarded.
|
|
public func select<T>(_ selection: [any SQLSelectable], as type: T.Type = T.self)
|
|
-> QueryInterfaceRequest<T>
|
|
{
|
|
select(selection).asRequest(of: T.self)
|
|
}
|
|
|
|
/// Defines the result columns, and defines the type of decoded rows.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let minScore = min(Column("score"))
|
|
/// let maxScore = max(Column("score"))
|
|
///
|
|
/// // SELECT MAX(score) FROM player
|
|
/// let request = Player.all()
|
|
/// .select(maxScore, as: Int.self)
|
|
/// let maxScore = try request.fetchOne(db) // Int?
|
|
///
|
|
/// // SELECT MIN(score), MAX(score) FROM player
|
|
/// let request = Player.all()
|
|
/// .select(minScore, maxScore, as: Row.self)
|
|
/// if let row = try request.fetchOne(db) {
|
|
/// let minScore: Int = row[0]
|
|
/// let maxScore: Int = row[1]
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Any previous selection is discarded.
|
|
public func select<T>(_ selection: any SQLSelectable..., as type: T.Type = T.self)
|
|
-> QueryInterfaceRequest<T>
|
|
{
|
|
select(selection, as: type)
|
|
}
|
|
|
|
/// Defines the result columns with an SQL string, and defines the type of
|
|
/// decoded rows.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // SELECT name FROM player
|
|
/// let request = Player.all()
|
|
/// .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.all()
|
|
/// .select(sql: "IFNULL(name, ?)", arguments: [defaultName], as: String.self)
|
|
/// let names = try request.fetchAll(db) // [String]
|
|
/// ```
|
|
///
|
|
/// Any previous selection is discarded.
|
|
public func select<T>(
|
|
sql: String,
|
|
arguments: StatementArguments = StatementArguments(),
|
|
as type: T.Type = T.self)
|
|
-> QueryInterfaceRequest<T>
|
|
{
|
|
select(SQL(sql: sql, arguments: arguments), as: type)
|
|
}
|
|
|
|
/// Defines the result columns with an ``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
|
|
/// // SELECT IFNULL(name, 'Anonymous') FROM player
|
|
/// let defaultName = "Anonymous"
|
|
/// let request = Player.all()
|
|
/// .select(literal: "IFNULL(name, \(defaultName))", as: String.self)
|
|
/// let names = try request.fetchAll(db) // [String]
|
|
/// ```
|
|
///
|
|
/// Any previous selection is discarded.
|
|
public func select<T>(
|
|
literal sqlLiteral: SQL,
|
|
as type: T.Type = T.self)
|
|
-> QueryInterfaceRequest<T>
|
|
{
|
|
select(sqlLiteral, as: type)
|
|
}
|
|
|
|
/// Selects the primary key.
|
|
///
|
|
/// All primary keys are supported:
|
|
///
|
|
/// ```swift
|
|
/// // SELECT id FROM player WHERE ...
|
|
/// let request = try Player.all().selectPrimaryKey(as: Int64.self)
|
|
/// let ids = try request.fetchAll(db) // [Int64]
|
|
///
|
|
/// // SELECT code FROM country WHERE ...
|
|
/// let request = try Country.all().selectPrimaryKey(as: String.self)
|
|
/// let countryCodes = try request.fetchAll(db) // [String]
|
|
///
|
|
/// // SELECT citizenId, countryCode FROM citizenship WHERE ...
|
|
/// let request = try Citizenship.all().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.all().selectPrimaryKey(as: Citizenship.ID.self)
|
|
/// let ids = try request.fetchAll(db) // [Citizenship.ID]
|
|
/// ```
|
|
///
|
|
/// Any previous selection is discarded.
|
|
public func selectPrimaryKey<PrimaryKey>(as type: PrimaryKey.Type = PrimaryKey.self)
|
|
-> QueryInterfaceRequest<PrimaryKey>
|
|
{
|
|
with { request in
|
|
let tableName = request.relation.source.tableName
|
|
request.relation = request.relation.selectWhenConnected { db in
|
|
try db.primaryKey(tableName).columns.map { Column($0).sqlSelection }
|
|
}
|
|
}
|
|
.asRequest(of: PrimaryKey.self)
|
|
}
|
|
|
|
public func annotatedWhenConnected(
|
|
with selection: @escaping (Database) throws -> [any SQLSelectable])
|
|
-> Self
|
|
{
|
|
with {
|
|
$0.relation = $0.relation.annotatedWhenConnected { db in
|
|
try selection(db).map(\.sqlSelection)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension QueryInterfaceRequest: FilteredRequest {
|
|
public func filterWhenConnected(
|
|
_ predicate: @escaping (Database) throws -> any SQLExpressible)
|
|
-> Self
|
|
{
|
|
with {
|
|
$0.relation = $0.relation.filterWhenConnected { db in
|
|
try predicate(db).sqlExpression
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension QueryInterfaceRequest: OrderedRequest {
|
|
public func orderWhenConnected(
|
|
_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm])
|
|
-> Self
|
|
{
|
|
with {
|
|
$0.relation = $0.relation.orderWhenConnected { db in
|
|
try orderings(db).map(\.sqlOrdering)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creates a request that reverses applied orderings.
|
|
///
|
|
/// // SELECT * FROM player ORDER BY name DESC
|
|
/// var request = Player.all().order(Column("name"))
|
|
/// request = request.reversed()
|
|
///
|
|
/// If no ordering was applied, the returned request is identical.
|
|
///
|
|
/// // SELECT * FROM player
|
|
/// var request = Player.all()
|
|
/// request = request.reversed()
|
|
public func reversed() -> Self {
|
|
with {
|
|
$0.relation = $0.relation.reversed()
|
|
}
|
|
}
|
|
|
|
/// Creates a request without any ordering.
|
|
///
|
|
/// // SELECT * FROM player
|
|
/// var request = Player.all().order(Column("name"))
|
|
/// request = request.unordered()
|
|
public func unordered() -> Self {
|
|
with {
|
|
$0.relation = $0.relation.unordered()
|
|
}
|
|
}
|
|
|
|
public func withStableOrder() -> QueryInterfaceRequest<RowDecoder> {
|
|
with {
|
|
$0.relation = $0.relation.withStableOrder()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension QueryInterfaceRequest: AggregatingRequest {
|
|
public func groupWhenConnected(
|
|
_ expressions: @escaping (Database) throws -> [any SQLExpressible])
|
|
-> Self
|
|
{
|
|
with {
|
|
$0.relation = $0.relation.groupWhenConnected { db in
|
|
try expressions(db).map(\.sqlExpression)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func havingWhenConnected(
|
|
_ predicate: @escaping (Database) throws -> any SQLExpressible)
|
|
-> Self
|
|
{
|
|
with {
|
|
$0.relation = $0.relation.havingWhenConnected { db in
|
|
try predicate(db).sqlExpression
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension QueryInterfaceRequest: JoinableRequest {
|
|
public func _including(all association: _SQLAssociation) -> Self {
|
|
with {
|
|
$0.relation = $0.relation._including(all: association)
|
|
}
|
|
}
|
|
|
|
public func _including(optional association: _SQLAssociation) -> Self {
|
|
with {
|
|
$0.relation = $0.relation._including(optional: association)
|
|
}
|
|
}
|
|
|
|
public func _including(required association: _SQLAssociation) -> Self {
|
|
with {
|
|
$0.relation = $0.relation._including(required: association)
|
|
}
|
|
}
|
|
|
|
public func _joining(optional association: _SQLAssociation) -> Self {
|
|
with {
|
|
$0.relation = $0.relation._joining(optional: association)
|
|
}
|
|
}
|
|
|
|
public func _joining(required association: _SQLAssociation) -> Self {
|
|
with {
|
|
$0.relation = $0.relation._joining(required: association)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension QueryInterfaceRequest: TableRequest {
|
|
public var databaseTableName: String {
|
|
relation.source.tableName
|
|
}
|
|
|
|
public func aliased(_ alias: TableAlias) -> Self {
|
|
with {
|
|
$0.relation = $0.relation.aliased(alias)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension QueryInterfaceRequest: DerivableRequest {
|
|
public func distinct() -> Self {
|
|
with {
|
|
$0.relation.isDistinct = true
|
|
}
|
|
}
|
|
|
|
/// Returns a limited request.
|
|
///
|
|
/// The returned request fetches `limit` rows, starting at `offset`. For
|
|
/// example:
|
|
///
|
|
/// ```swift
|
|
/// // SELECT * FROM player LIMIT 10
|
|
/// let request = Player.all().limit(10)
|
|
///
|
|
/// // SELECT * FROM player LIMIT 10 OFFSET 20
|
|
/// let request = Player.all().limit(10, offset: 20)
|
|
/// ```
|
|
///
|
|
/// Any previously applied limit is discarded.
|
|
public func limit(_ limit: Int, offset: Int? = nil) -> Self {
|
|
with {
|
|
$0.relation.limit = SQLLimit(limit: limit, offset: offset)
|
|
}
|
|
}
|
|
|
|
public func with<T>(_ cte: CommonTableExpression<T>) -> Self {
|
|
with {
|
|
$0.relation.ctes[cte.tableName] = cte.cte
|
|
}
|
|
}
|
|
}
|
|
|
|
extension QueryInterfaceRequest {
|
|
/// Returns a request that performs an identical database query, but decodes
|
|
/// database rows with `type`.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.read { db in
|
|
/// let maxScore: Int? = try Player
|
|
/// .select(max(scoreColumn))
|
|
/// .asRequest(of: Int.self) // <--
|
|
/// .fetchOne(db)
|
|
/// }
|
|
/// ```
|
|
public func asRequest<T>(of type: T.Type) -> QueryInterfaceRequest<T> {
|
|
QueryInterfaceRequest<T>(relation: relation)
|
|
}
|
|
}
|
|
|
|
// MARK: - Check Existence
|
|
|
|
extension QueryInterfaceRequest {
|
|
/// Returns whether the requests does not match any row in the database.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.read { db in
|
|
/// let arthurIsMissing = try Player
|
|
/// .filter(Column("name") == "Arthur")
|
|
/// .isEmpty(db)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameter db: A database connection.
|
|
public func isEmpty(_ db: Database) throws -> Bool {
|
|
try !SQLRequest("SELECT \(exists())").fetchOne(db)!
|
|
}
|
|
}
|
|
|
|
// MARK: - Batch Delete
|
|
|
|
extension QueryInterfaceRequest {
|
|
/// Deletes matching rows, and returns the number of deleted rows.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: The number of deleted rows
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@discardableResult
|
|
public func deleteAll(_ db: Database) throws -> Int {
|
|
let statement = try SQLQueryGenerator(relation: relation).makeDeleteStatement(db)
|
|
try statement.execute()
|
|
return db.changesCount
|
|
}
|
|
}
|
|
|
|
// MARK: - Batch Delete and Fetch
|
|
|
|
extension QueryInterfaceRequest {
|
|
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
|
/// Returns a `DELETE RETURNING` prepared statement.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Delete all players and return their names
|
|
/// // DELETE FROM player RETURNING name
|
|
/// let request = Player.all()
|
|
/// let statement = try request.deleteAndFetchStatement(db, selection: [Column("name")])
|
|
/// let deletedNames = try String.fetchSet(statement)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter selection: The returned columns (must not be empty).
|
|
/// - returns: A prepared statement.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `selection` is not empty.
|
|
public func deleteAndFetchStatement(
|
|
_ db: Database,
|
|
selection: [any SQLSelectable])
|
|
throws -> Statement
|
|
{
|
|
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
|
return try SQLQueryGenerator(relation: relation).makeDeleteStatement(db, selection: selection)
|
|
}
|
|
|
|
/// Returns a cursor over the records deleted by a
|
|
/// `DELETE RETURNING` statement.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all deleted players
|
|
/// // DELETE FROM player RETURNING *
|
|
/// let request = Player.all()
|
|
/// let players = try request.deleteAndFetchCursor(db)
|
|
/// while let player = try players.next() {
|
|
/// print("Player \(player) was deleted")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: A ``RecordCursor`` over the deleted records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
public func deleteAndFetchCursor(_ db: Database)
|
|
throws -> RecordCursor<RowDecoder>
|
|
where RowDecoder: FetchableRecord & TableRecord
|
|
{
|
|
let statement = try deleteAndFetchStatement(db, selection: RowDecoder.databaseSelection)
|
|
return try RowDecoder.fetchCursor(statement)
|
|
}
|
|
|
|
/// Executes a `DELETE RETURNING` statement and returns the array of
|
|
/// deleted records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all deleted players
|
|
/// // DELETE FROM player RETURNING *
|
|
/// let request = Player.all()
|
|
/// let deletedPlayers = try request.deleteAndFetchAll(db)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: An array of deleted records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
public func deleteAndFetchAll(_ db: Database)
|
|
throws -> [RowDecoder]
|
|
where RowDecoder: FetchableRecord & TableRecord
|
|
{
|
|
try Array(deleteAndFetchCursor(db))
|
|
}
|
|
|
|
/// Executes a `DELETE RETURNING` statement and returns the set of
|
|
/// deleted records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all deleted players
|
|
/// // DELETE FROM player RETURNING *
|
|
/// let request = Player.all()
|
|
/// let deletedPlayers = try request.deleteAndFetchSet(db)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: A set of deleted records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
public func deleteAndFetchSet(_ db: Database)
|
|
throws -> Set<RowDecoder>
|
|
where RowDecoder: FetchableRecord & TableRecord & Hashable
|
|
{
|
|
try Set(deleteAndFetchCursor(db))
|
|
}
|
|
|
|
/// Executes a `DELETE RETURNING` statement and returns the set of
|
|
/// deleted ids.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch the ids of deleted players
|
|
/// // DELETE FROM player RETURNING id
|
|
/// let request = Player.all()
|
|
/// let deletedPlayerIds = try request.deleteAndFetchIds(db)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: A set of deleted ids.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) // Identifiable
|
|
public func deleteAndFetchIds(_ db: Database)
|
|
throws -> Set<RowDecoder.ID>
|
|
where RowDecoder: TableRecord & Identifiable,
|
|
RowDecoder.ID: Hashable & DatabaseValueConvertible & StatementColumnConvertible
|
|
{
|
|
let primaryKey = try db.primaryKey(RowDecoder.databaseTableName)
|
|
GRDBPrecondition(
|
|
primaryKey.columns.count == 1,
|
|
"Fetching id requires a single-column primary key in the table \(databaseTableName)")
|
|
|
|
let statement = try deleteAndFetchStatement(db, selection: [Column(primaryKey.columns[0])])
|
|
|
|
return try RowDecoder.ID.fetchSet(statement)
|
|
}
|
|
#else
|
|
/// Returns a `DELETE RETURNING` prepared statement.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Delete all players and return their names
|
|
/// // DELETE FROM player RETURNING name
|
|
/// let request = Player.all()
|
|
/// let statement = try request.deleteAndFetchStatement(db, selection: [Column("name")])
|
|
/// let deletedNames = try String.fetchSet(statement)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter selection: The returned columns (must not be empty).
|
|
/// - returns: A prepared statement.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `selection` is not empty.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func deleteAndFetchStatement(
|
|
_ db: Database,
|
|
selection: [any SQLSelectable])
|
|
throws -> Statement
|
|
{
|
|
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
|
return try SQLQueryGenerator(relation: relation).makeDeleteStatement(db, selection: selection)
|
|
}
|
|
|
|
/// Returns a cursor over the records deleted by a
|
|
/// `DELETE RETURNING` statement.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all deleted players
|
|
/// // DELETE FROM player RETURNING *
|
|
/// let request = Player.all()
|
|
/// let players = try request.deleteAndFetchCursor(db)
|
|
/// while let player = try players.next() {
|
|
/// print("Player \(player) was deleted")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: A ``RecordCursor`` over the deleted records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func deleteAndFetchCursor(_ db: Database)
|
|
throws -> RecordCursor<RowDecoder>
|
|
where RowDecoder: FetchableRecord & TableRecord
|
|
{
|
|
let statement = try deleteAndFetchStatement(db, selection: RowDecoder.databaseSelection)
|
|
return try RowDecoder.fetchCursor(statement)
|
|
}
|
|
|
|
/// Executes a `DELETE RETURNING` statement and returns the array of
|
|
/// deleted records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all deleted players
|
|
/// // DELETE FROM player RETURNING *
|
|
/// let request = Player.all()
|
|
/// let deletedPlayers = try request.deleteAndFetchAll(db)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: An array of deleted records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func deleteAndFetchAll(_ db: Database)
|
|
throws -> [RowDecoder]
|
|
where RowDecoder: FetchableRecord & TableRecord
|
|
{
|
|
try Array(deleteAndFetchCursor(db))
|
|
}
|
|
|
|
/// Executes a `DELETE RETURNING` statement and returns the set of
|
|
/// deleted records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all deleted players
|
|
/// // DELETE FROM player RETURNING *
|
|
/// let request = Player.all()
|
|
/// let deletedPlayers = try request.deleteAndFetchSet(db)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: A set of deleted records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func deleteAndFetchSet(_ db: Database)
|
|
throws -> Set<RowDecoder>
|
|
where RowDecoder: FetchableRecord & TableRecord & Hashable
|
|
{
|
|
try Set(deleteAndFetchCursor(db))
|
|
}
|
|
|
|
/// Executes a `DELETE RETURNING` statement and returns the set of
|
|
/// deleted ids.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch the ids of deleted players
|
|
/// // DELETE FROM player RETURNING id
|
|
/// let request = Player.all()
|
|
/// let deletedPlayerIds = try request.deleteAndFetchIds(db)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: A set of deleted ids.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func deleteAndFetchIds(_ db: Database)
|
|
throws -> Set<RowDecoder.ID>
|
|
where RowDecoder: TableRecord & Identifiable,
|
|
RowDecoder.ID: Hashable & DatabaseValueConvertible & StatementColumnConvertible
|
|
{
|
|
let primaryKey = try db.primaryKey(RowDecoder.databaseTableName)
|
|
GRDBPrecondition(
|
|
primaryKey.columns.count == 1,
|
|
"Fetching id requires a single-column primary key in the table \(databaseTableName)")
|
|
|
|
let statement = try deleteAndFetchStatement(db, selection: [Column(primaryKey.columns[0])])
|
|
|
|
return try RowDecoder.ID.fetchSet(statement)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Batch Update
|
|
|
|
extension QueryInterfaceRequest {
|
|
/// The conflict resolution to use for batch updates
|
|
private var defaultConflictResolutionForUpdate: Database.ConflictResolution {
|
|
// In order to look for the default conflict resolution, we perform a
|
|
// runtime check for MutablePersistableRecord, and look for a
|
|
// user-defined default. Such dynamic dispatch is unusual in GRDB, but
|
|
// static dispatch is likely to create bad surprises in generic contexts.
|
|
if let recordType = RowDecoder.self as? any MutablePersistableRecord.Type {
|
|
return recordType.persistenceConflictPolicy.conflictResolutionForUpdate
|
|
} else {
|
|
return .abort
|
|
}
|
|
}
|
|
|
|
/// Updates matching rows, and returns the number of updated rows.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = 0
|
|
/// let request = Player.all()
|
|
/// try request.updateAll(db, [Column("score").set(to: 0)])
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments.
|
|
/// - returns: The number of updated rows.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@discardableResult
|
|
public func updateAll(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment]) throws -> Int
|
|
{
|
|
let conflictResolution = conflictResolution ?? defaultConflictResolutionForUpdate
|
|
guard let updateStatement = try SQLQueryGenerator(relation: relation).makeUpdateStatement(
|
|
db,
|
|
conflictResolution: conflictResolution,
|
|
assignments: assignments) else
|
|
{
|
|
// database not hit
|
|
return 0
|
|
}
|
|
try updateStatement.execute()
|
|
return db.changesCount
|
|
}
|
|
|
|
/// Updates matching rows, and returns the number of updated rows.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = 0
|
|
/// let request = Player.all()
|
|
/// try request.updateAll(db, Column("score").set(to: 0))
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: Column assignments.
|
|
/// - returns: The number of updated rows.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@discardableResult
|
|
public func updateAll(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: ColumnAssignment...)
|
|
throws -> Int
|
|
{
|
|
try updateAll(db, onConflict: conflictResolution, assignments)
|
|
}
|
|
}
|
|
|
|
// MARK: - Batch Update and Fetch
|
|
|
|
extension QueryInterfaceRequest {
|
|
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
|
/// Returns an `UPDATE RETURNING` prepared statement.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all updated scores
|
|
/// // UPDATE player SET score = score + 10 RETURNING score
|
|
/// let request = Player.all()
|
|
/// let statement = try request.updateAndFetchStatement(
|
|
/// db, [Column("score") += 10],
|
|
/// select: [Column("score")])
|
|
/// let updatedScores = try Int.fetchAll(statement)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments
|
|
/// (must not be empty).
|
|
/// - parameter selection: The returned columns (must not be empty).
|
|
/// - returns: A prepared statement.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `selection` and `assignments` are not empty.
|
|
public func updateAndFetchStatement(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment],
|
|
selection: [any SQLSelectable])
|
|
throws -> Statement
|
|
{
|
|
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
|
|
|
let conflictResolution = conflictResolution ?? defaultConflictResolutionForUpdate
|
|
guard let updateStatement = try SQLQueryGenerator(relation: relation).makeUpdateStatement(
|
|
db,
|
|
conflictResolution: conflictResolution,
|
|
assignments: assignments,
|
|
selection: selection)
|
|
else {
|
|
fatalError("Invalid empty assignments")
|
|
}
|
|
|
|
return updateStatement
|
|
}
|
|
|
|
/// Returns a cursor over the records updated by an
|
|
/// `UPDATE RETURNING` statement.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all updated players
|
|
/// // UPDATE player SET score = score + 10 RETURNING *
|
|
/// let request = Player.all()
|
|
/// let players = try request.updateAndFetchCursor(db, [Column("score") += 10])
|
|
/// while let player = try players.next() {
|
|
/// print("Player \(player) was updated")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments.
|
|
/// - returns: A cursor over the updated records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `assignments` is not empty.
|
|
public func updateAndFetchCursor(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment])
|
|
throws -> RecordCursor<RowDecoder>
|
|
where RowDecoder: FetchableRecord & TableRecord
|
|
{
|
|
let statement = try updateAndFetchStatement(
|
|
db,
|
|
onConflict: conflictResolution,
|
|
assignments,
|
|
selection: RowDecoder.databaseSelection)
|
|
return try RowDecoder.fetchCursor(statement)
|
|
}
|
|
|
|
/// Execute an `UPDATE RETURNING` statement and returns the array of
|
|
/// updated records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all updated players
|
|
/// // UPDATE player SET score = score + 10 RETURNING *
|
|
/// let request = Player.all()
|
|
/// let updatedPlayers = try request.updateAndFetchAll(db, [Column("score") += 10])
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments.
|
|
/// - returns: An array of updated records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `assignments` is not empty.
|
|
public func updateAndFetchAll(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment])
|
|
throws -> [RowDecoder]
|
|
where RowDecoder: FetchableRecord & TableRecord
|
|
{
|
|
try Array(updateAndFetchCursor(db, onConflict: conflictResolution, assignments))
|
|
}
|
|
|
|
/// Execute an `UPDATE RETURNING` statement and returns the set of
|
|
/// updated records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all updated players
|
|
/// // UPDATE player SET score = score + 10 RETURNING *
|
|
/// let request = Player.all()
|
|
/// let updatedPlayers = try request.updateAndFetchSet(db, [Column("score") += 10])
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments.
|
|
/// - returns: A set of updated records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `assignments` is not empty.
|
|
public func updateAndFetchSet(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment])
|
|
throws -> Set<RowDecoder>
|
|
where RowDecoder: FetchableRecord & TableRecord & Hashable
|
|
{
|
|
try Set(updateAndFetchCursor(db, onConflict: conflictResolution, assignments))
|
|
}
|
|
#else
|
|
/// Returns an `UPDATE RETURNING` prepared statement.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all updated scores
|
|
/// // UPDATE player SET score = score + 10 RETURNING score
|
|
/// let request = Player.all()
|
|
/// let statement = try request.updateAndFetchStatement(
|
|
/// db, [Column("score") += 10],
|
|
/// select: [Column("score")])
|
|
/// let updatedScores = try Int.fetchAll(statement)
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments
|
|
/// (must not be empty).
|
|
/// - parameter selection: The returned columns (must not be empty).
|
|
/// - returns: A prepared statement.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `selection` and `assignments` are not empty.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func updateAndFetchStatement(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment],
|
|
selection: [any SQLSelectable])
|
|
throws -> Statement
|
|
{
|
|
GRDBPrecondition(!selection.isEmpty, "Invalid empty selection")
|
|
|
|
let conflictResolution = conflictResolution ?? defaultConflictResolutionForUpdate
|
|
guard let updateStatement = try SQLQueryGenerator(relation: relation).makeUpdateStatement(
|
|
db,
|
|
conflictResolution: conflictResolution,
|
|
assignments: assignments,
|
|
selection: selection)
|
|
else {
|
|
fatalError("Invalid empty assignments")
|
|
}
|
|
|
|
return updateStatement
|
|
}
|
|
|
|
/// Returns a cursor over the records updated by an
|
|
/// `UPDATE RETURNING` statement.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all updated players
|
|
/// // UPDATE player SET score = score + 10 RETURNING *
|
|
/// let request = Player.all()
|
|
/// let players = try request.updateAndFetchCursor(db, [Column("score") += 10])
|
|
/// while let player = try players.next() {
|
|
/// print("Player \(player) was updated")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments.
|
|
/// - returns: A cursor over the updated records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `assignments` is not empty.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func updateAndFetchCursor(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment])
|
|
throws -> RecordCursor<RowDecoder>
|
|
where RowDecoder: FetchableRecord & TableRecord
|
|
{
|
|
let statement = try updateAndFetchStatement(
|
|
db,
|
|
onConflict: conflictResolution,
|
|
assignments,
|
|
selection: RowDecoder.databaseSelection)
|
|
return try RowDecoder.fetchCursor(statement)
|
|
}
|
|
|
|
/// Execute an `UPDATE RETURNING` statement and returns the array of
|
|
/// updated records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all updated players
|
|
/// // UPDATE player SET score = score + 10 RETURNING *
|
|
/// let request = Player.all()
|
|
/// let updatedPlayers = try request.updateAndFetchAll(db, [Column("score") += 10])
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments.
|
|
/// - returns: An array of updated records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `assignments` is not empty.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func updateAndFetchAll(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment])
|
|
throws -> [RowDecoder]
|
|
where RowDecoder: FetchableRecord & TableRecord
|
|
{
|
|
try Array(updateAndFetchCursor(db, onConflict: conflictResolution, assignments))
|
|
}
|
|
|
|
/// Execute an `UPDATE RETURNING` statement and returns the set of
|
|
/// updated records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // Fetch all updated players
|
|
/// // UPDATE player SET score = score + 10 RETURNING *
|
|
/// let request = Player.all()
|
|
/// let updatedPlayers = try request.updateAndFetchSet(db, [Column("score") += 10])
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you check the documentation of the `RETURNING`
|
|
/// clause, which describes important limitations and caveats:
|
|
/// <https://www.sqlite.org/lang_returning.html#limitations_and_caveats>.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution.
|
|
/// - parameter assignments: An array of column assignments.
|
|
/// - returns: A set of updated records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
/// - precondition: `assignments` is not empty.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
|
|
public func updateAndFetchSet(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment])
|
|
throws -> Set<RowDecoder>
|
|
where RowDecoder: FetchableRecord & TableRecord & Hashable
|
|
{
|
|
try Set(updateAndFetchCursor(db, onConflict: conflictResolution, assignments))
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// MARK: - ColumnAssignment
|
|
|
|
/// A `ColumnAssignment` assigns a value to a column.
|
|
///
|
|
/// You create an assignment from a column and an assignment method or operator,
|
|
/// such as ``ColumnExpression/set(to:)`` or `+=`:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = 0
|
|
/// let assignment = Column("score").set(to: 0)
|
|
/// try Player.updateAll(db, assignment)
|
|
/// }
|
|
/// ```
|
|
public struct ColumnAssignment {
|
|
var columnName: String
|
|
|
|
/// If nil, this is a "don't assign" assignment.
|
|
var value: SQLExpression?
|
|
|
|
init(columnName: String, value: SQLExpression? = nil) {
|
|
self.columnName = columnName
|
|
self.value = value
|
|
}
|
|
|
|
/// If nil, there's nothing to assign to.
|
|
func sql(_ context: SQLGenerationContext) throws -> String? {
|
|
guard let value else { return nil }
|
|
return try Column(columnName).sqlExpression.sql(context) + " = " + value.sql(context)
|
|
}
|
|
}
|
|
|
|
extension ColumnExpression {
|
|
/// Returns an assignment of this column to an SQL expression.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("valid").set(to: true)
|
|
/// Column("score").set(to: 0)
|
|
/// Column("score").set(to: nil)
|
|
/// Column("score").set(to: Column("score") + Column("bonus"))
|
|
/// ```
|
|
///
|
|
/// For convenience, the last assignment can also be written as:
|
|
///
|
|
/// ```swift
|
|
/// Column("score") += Column("bonus")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = 0
|
|
/// try Player.updateAll(db, Column("score").set(to: 0))
|
|
/// }
|
|
/// ```
|
|
public func set(to value: (any SQLExpressible)?) -> ColumnAssignment {
|
|
ColumnAssignment(columnName: name, value: value?.sqlExpression ?? .null)
|
|
}
|
|
|
|
/// An assignment that does not modify this column.
|
|
public var noOverwrite: ColumnAssignment {
|
|
ColumnAssignment(columnName: name, value: nil)
|
|
}
|
|
}
|
|
|
|
extension ColumnExpression {
|
|
/// Creates an assignment that adds an SQL expression.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("score") += 1
|
|
/// Column("score") += Column("bonus")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = score + 1
|
|
/// try Player.updateAll(db, Column("score") += 1)
|
|
/// }
|
|
/// ```
|
|
public static func += (column: Self, value: some SQLExpressible) -> ColumnAssignment {
|
|
column.set(to: column + value)
|
|
}
|
|
|
|
/// Creates an assignment that subtracts an SQL expression.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("score") -= 1
|
|
/// Column("score") -= Column("bonus")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = score - 1
|
|
/// try Player.updateAll(db, Column("score") -= 1)
|
|
/// }
|
|
/// ```
|
|
public static func -= (column: Self, value: some SQLExpressible) -> ColumnAssignment {
|
|
column.set(to: column - value)
|
|
}
|
|
|
|
/// Creates an assignment that multiplies by an SQL expression.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("score") *= 2
|
|
/// Column("score") *= Column("factor")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = score * 2
|
|
/// try Player.updateAll(db, Column("score") *= 2)
|
|
/// }
|
|
/// ```
|
|
public static func *= (column: Self, value: some SQLExpressible) -> ColumnAssignment {
|
|
column.set(to: column * value)
|
|
}
|
|
|
|
/// Creates an assignment that divides by an SQL expression.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("score") /= 2
|
|
/// Column("score") /= Column("factor")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = score / 2
|
|
/// try Player.updateAll(db, Column("score") /= 2)
|
|
/// }
|
|
/// ```
|
|
public static func /= (column: Self, value: some SQLExpressible) -> ColumnAssignment {
|
|
column.set(to: column / value)
|
|
}
|
|
|
|
/// Creates an assignment that applies a bitwise and.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("mask") &= 2
|
|
/// Column("mask") &= Column("other")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = score & 2
|
|
/// try Player.updateAll(db, Column("mask") &= 2)
|
|
/// }
|
|
/// ```
|
|
public static func &= (column: Self, value: some SQLExpressible) -> ColumnAssignment {
|
|
column.set(to: column & value)
|
|
}
|
|
|
|
/// Creates an assignment that applies a bitwise or.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("mask") |= 2
|
|
/// Column("mask") |= Column("other")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = score | 2
|
|
/// try Player.updateAll(db, Column("mask") |= 2)
|
|
/// }
|
|
/// ```
|
|
public static func |= (column: Self, value: some SQLExpressible) -> ColumnAssignment {
|
|
column.set(to: column | value)
|
|
}
|
|
|
|
/// Creates an assignment that applies a bitwise left shift.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("mask") <<= 2
|
|
/// Column("mask") <<= Column("other")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = score << 2
|
|
/// try Player.updateAll(db, Column("mask") <<= 2)
|
|
/// }
|
|
/// ```
|
|
public static func <<= (column: Self, value: some SQLExpressible) -> ColumnAssignment {
|
|
column.set(to: column << value)
|
|
}
|
|
|
|
/// Creates an assignment that applies a bitwise right shift.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// Column("mask") >>= 2
|
|
/// Column("mask") >>= Column("other")
|
|
/// ```
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = score >> 2
|
|
/// try Player.updateAll(db, Column("mask") >>= 2)
|
|
/// }
|
|
/// ```
|
|
public static func >>= (column: Self, value: some SQLExpressible) -> ColumnAssignment {
|
|
column.set(to: column >> value)
|
|
}
|
|
}
|
|
|
|
// MARK: - Eager loading of hasMany associations
|
|
|
|
// CAUTION: Keep this code in sync with prefetchedRegion(_:_:)
|
|
/// Append rows from prefetched associations into the `originRows` argument.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter associations: Prefetched associations.
|
|
/// - parameter originRows: The rows that need to be extended with prefetched rows.
|
|
/// - parameter originQuery: The query that was used to fetch `originRows`.
|
|
/// - parameter willExecuteSupplementaryRequest: A closure executed before a
|
|
/// supplementary fetch is performed.
|
|
private func prefetch(
|
|
_ db: Database,
|
|
associations: [_SQLAssociation],
|
|
from originRelation: SQLRelation,
|
|
into originRows: [Row],
|
|
willExecuteSupplementaryRequest: WillExecuteSupplementaryRequest?) throws
|
|
{
|
|
guard let firstOriginRow = originRows.first else {
|
|
// No rows -> no prefetch
|
|
return
|
|
}
|
|
|
|
for association in associations {
|
|
switch association.pivot.condition {
|
|
case .expression:
|
|
// Likely a GRDB bug: such condition only exist for CTEs, which
|
|
// are not prefetched with including(all:)
|
|
fatalError("Not implemented: prefetch association without any foreign key")
|
|
|
|
case let .foreignKey(pivotForeignKey):
|
|
let originTable = originRelation.source.tableName
|
|
let pivotMapping = try pivotForeignKey.joinMapping(db, from: originTable)
|
|
let pivotColumns = pivotMapping.map(\.right)
|
|
let leftColumns = pivotMapping.map(\.left)
|
|
|
|
// We want to avoid the "Expression tree is too large" SQLite error
|
|
// when the foreign key contains several columns, and there are many
|
|
// base rows that overflow SQLITE_LIMIT_EXPR_DEPTH:
|
|
// https://github.com/groue/GRDB.swift/issues/871
|
|
//
|
|
// -- May be too complex for the SQLite engine
|
|
// SELECT * FROM child
|
|
// WHERE (a = ? AND b = ?)
|
|
// OR (a = ? AND b = ?)
|
|
// OR ...
|
|
//
|
|
// Instead, we do not inject any value from the base rows in
|
|
// the prefetch request. Instead, we directly inject the base
|
|
// request as a common table expression (CTE):
|
|
//
|
|
// WITH grdb_base AS (SELECT a, b FROM parent)
|
|
// SELECT * FROM child
|
|
// WHERE (a, b) IN grdb_base
|
|
let usesCommonTableExpression = pivotMapping.count > 1
|
|
|
|
let prefetchRequest: QueryInterfaceRequest<Row>
|
|
if usesCommonTableExpression {
|
|
// HasMany: Author.including(all: Author.books)
|
|
//
|
|
// WITH grdb_base AS (SELECT a, b FROM author)
|
|
// SELECT book.*, book.authorId AS grdb_authorId
|
|
// FROM book
|
|
// WHERE (book.a, book.b) IN grdb_base
|
|
//
|
|
// HasManyThrough: Citizen.including(all: Citizen.countries)
|
|
//
|
|
// WITH grdb_base AS (SELECT a, b FROM citizen)
|
|
// SELECT country.*, passport.citizenId AS grdb_citizenId
|
|
// FROM country
|
|
// JOIN passport ON passport.countryCode = country.code
|
|
// AND (passport.a, passport.b) IN grdb_base
|
|
//
|
|
// In the CTE, ordering and including(all:) children are
|
|
// useless, and we only need to select pivot columns:
|
|
let originRelation = originRelation
|
|
.unorderedUnlessLimited() // only preserve ordering in the CTE if limited
|
|
.removingPrefetchedAssociations()
|
|
.selectOnly(leftColumns.map { SQLExpression.column($0).sqlSelection })
|
|
let originCTE = CommonTableExpression(
|
|
named: "grdb_base",
|
|
request: SQLSubquery.relation(originRelation))
|
|
let pivotRowValue = SQLExpression.rowValue(pivotColumns.map(SQLExpression.column))!
|
|
let pivotFilter = originCTE.contains(pivotRowValue)
|
|
|
|
prefetchRequest = makePrefetchRequest(
|
|
for: association,
|
|
filteringPivotWith: pivotFilter,
|
|
annotatedWith: pivotColumns)
|
|
.with(originCTE)
|
|
} else {
|
|
// HasMany: Author.including(all: Author.books)
|
|
//
|
|
// SELECT *, authorId AS grdb_authorId
|
|
// FROM book
|
|
// WHERE authorId IN (1, 2, 3)
|
|
//
|
|
// HasManyThrough: Citizen.including(all: Citizen.countries)
|
|
//
|
|
// SELECT country.*, passport.citizenId AS grdb_citizenId
|
|
// FROM country
|
|
// JOIN passport ON passport.countryCode = country.code
|
|
// AND passport.citizenId IN (1, 2, 3)
|
|
let pivotFilter = pivotMapping.joinExpression(leftRows: originRows)
|
|
|
|
prefetchRequest = makePrefetchRequest(
|
|
for: association,
|
|
filteringPivotWith: pivotFilter,
|
|
annotatedWith: pivotColumns)
|
|
}
|
|
|
|
if let willExecuteSupplementaryRequest {
|
|
// Support for `Database.dumpRequest`
|
|
try willExecuteSupplementaryRequest(.init(prefetchRequest), association.keyPath)
|
|
}
|
|
let prefetchedRows = try prefetchRequest.fetchAll(db)
|
|
let prefetchedGroups = prefetchedRows.grouped(byDatabaseValuesOnColumns: pivotColumns.map { "grdb_\($0)" })
|
|
let groupingIndexes = firstOriginRow.indexes(forColumns: leftColumns)
|
|
|
|
for row in originRows {
|
|
let groupingKey = groupingIndexes.map { row.impl.databaseValue(atUncheckedIndex: $0) }
|
|
let prefetchedRows = prefetchedGroups[groupingKey, default: []]
|
|
row.prefetchedRows.setRows(prefetchedRows, forKeyPath: association.keyPath)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a request for prefetched rows.
|
|
///
|
|
/// - parameter association: The prefetched association.
|
|
/// - parameter pivotFilter: The expression that filters the pivot of
|
|
/// the association.
|
|
/// - parameter pivotColumns: The pivot columns that annotate the
|
|
/// returned request.
|
|
func makePrefetchRequest(
|
|
for association: _SQLAssociation,
|
|
filteringPivotWith pivotFilter: SQLExpression,
|
|
annotatedWith pivotColumns: [String])
|
|
-> QueryInterfaceRequest<Row>
|
|
{
|
|
// We annotate prefetched rows with pivot columns, so that we can
|
|
// group them.
|
|
//
|
|
// Those pivot columns are necessary when we prefetch
|
|
// indirect associations:
|
|
//
|
|
// // SELECT country.*, passport.citizenId AS grdb_citizenId
|
|
// // -- ^ the necessary pivot column
|
|
// // FROM country
|
|
// // JOIN passport ON passport.countryCode = country.code
|
|
// // AND passport.citizenId IN (1, 2, 3)
|
|
// Citizen.including(all: Citizen.countries)
|
|
//
|
|
// Those pivot columns are redundant when we prefetch direct
|
|
// associations (maybe we'll remove this redundancy later):
|
|
//
|
|
// // SELECT *, authorId AS grdb_authorId
|
|
// // -- ^ the redundant pivot column
|
|
// // FROM book
|
|
// // WHERE authorId IN (1, 2, 3)
|
|
// Author.including(all: Author.books)
|
|
let pivotAlias = TableAlias()
|
|
|
|
let prefetchRelation = association
|
|
.with {
|
|
$0.pivot.relation = $0.pivot.relation
|
|
.aliased(pivotAlias)
|
|
.filter(pivotFilter)
|
|
}
|
|
.destinationRelation()
|
|
.annotated(with: pivotColumns.map { pivotAlias[$0].forKey("grdb_\($0)") })
|
|
|
|
return QueryInterfaceRequest<Row>(relation: prefetchRelation)
|
|
}
|
|
|
|
// CAUTION: Keep this code in sync with prefetch(_:associations:in:)
|
|
/// Returns the region of prefetched associations
|
|
func prefetchedRegion(
|
|
_ db: Database,
|
|
associations: [_SQLAssociation],
|
|
from originTable: String)
|
|
throws -> DatabaseRegion
|
|
{
|
|
try associations.reduce(into: DatabaseRegion()) { (region, association) in
|
|
switch association.pivot.condition {
|
|
case .expression:
|
|
// Likely a GRDB bug: such condition only exist for CTEs, which
|
|
// are not prefetched with including(all:)
|
|
fatalError("Not implemented: prefetch association without any foreign key")
|
|
|
|
case let .foreignKey(pivotForeignKey):
|
|
let pivotMapping = try pivotForeignKey.joinMapping(db, from: originTable)
|
|
let prefetchRegion = try prefetchedRegion(db, association: association, pivotMapping: pivotMapping)
|
|
region.formUnion(prefetchRegion)
|
|
}
|
|
}
|
|
}
|
|
|
|
// CAUTION: Keep this code in sync with prefetch(_:associations:in:)
|
|
func prefetchedRegion(
|
|
_ db: Database,
|
|
association: _SQLAssociation,
|
|
pivotMapping: JoinMapping)
|
|
throws -> DatabaseRegion
|
|
{
|
|
// Filter the pivot on a `DummyRow` in order to make sure all join
|
|
// condition columns are made visible to SQLite, and present in the
|
|
// selected region:
|
|
// ... JOIN right ON right.leftId = ?
|
|
// ^ content of the DummyRow
|
|
let pivotFilter = pivotMapping.joinExpression(leftRows: [DummyRow()])
|
|
|
|
let prefetchRelation = association
|
|
.with {
|
|
$0.pivot.relation = $0.pivot.relation.filter(pivotFilter)
|
|
}
|
|
.destinationRelation()
|
|
|
|
return try SQLQueryGenerator(relation: prefetchRelation)
|
|
.makeStatement(db)
|
|
.databaseRegion // contains region of nested associations
|
|
}
|
|
|
|
extension [Row] {
|
|
/// - precondition: Columns all exist in all rows. All rows have the same
|
|
/// columnns, in the same order.
|
|
fileprivate func grouped(byDatabaseValuesOnColumns columns: [String]) -> [[DatabaseValue]: [Row]] {
|
|
guard let firstRow = first else {
|
|
return [:]
|
|
}
|
|
let indexes = firstRow.indexes(forColumns: columns)
|
|
return Dictionary(grouping: self, by: { row in
|
|
indexes.map { row.impl.databaseValue(atUncheckedIndex: $0) }
|
|
})
|
|
}
|
|
}
|
|
|
|
extension Row {
|
|
/// - precondition: Columns all exist in the row.
|
|
fileprivate func indexes(forColumns columns: [String]) -> [Int] {
|
|
columns.map { column in
|
|
guard let index = index(forColumn: column) else {
|
|
fatalError("Column \(column) is not selected")
|
|
}
|
|
return index
|
|
}
|
|
}
|
|
}
|