761 lines
24 KiB
Swift
761 lines
24 KiB
Swift
import Foundation
|
|
|
|
/// A type that builds database queries with the Swift language instead of SQL.
|
|
///
|
|
/// A `TableRecord` type is tied to one database table, and can build SQL
|
|
/// queries on that table.
|
|
///
|
|
/// To build SQL queries that involve several tables, define some ``Association``
|
|
/// between two `TableRecord` types.
|
|
///
|
|
/// Most of the time, your record types will get `TableRecord` conformance
|
|
/// through the ``MutablePersistableRecord`` or ``PersistableRecord`` protocols,
|
|
/// which provide persistence methods.
|
|
///
|
|
/// ## Topics
|
|
///
|
|
/// ### Configuring the Generated SQL
|
|
///
|
|
/// - ``databaseTableName-3tcw2``
|
|
/// - ``databaseSelection-7iphs``
|
|
/// - ``numberOfSelectedColumns(_:)``
|
|
///
|
|
/// ### Counting Records
|
|
///
|
|
/// - ``fetchCount(_:)``
|
|
///
|
|
/// ### Testing for Record Existence
|
|
///
|
|
/// - ``exists(_:id:)``
|
|
/// - ``exists(_:key:)-60hf2``
|
|
/// - ``exists(_:key:)-6ha6``
|
|
///
|
|
/// ### Throwing Record Not Found Errors
|
|
///
|
|
/// - ``recordNotFound(_:id:)``
|
|
/// - ``recordNotFound(_:key:)``
|
|
/// - ``recordNotFound(key:)``
|
|
///
|
|
/// ### Deleting Records
|
|
///
|
|
/// - ``deleteAll(_:)``
|
|
/// - ``deleteAll(_:ids:)``
|
|
/// - ``deleteAll(_:keys:)-jbkm``
|
|
/// - ``deleteAll(_:keys:)-5s1jg``
|
|
/// - ``deleteOne(_:id:)``
|
|
/// - ``deleteOne(_:key:)-413u8``
|
|
/// - ``deleteOne(_:key:)-5pdh5``
|
|
///
|
|
/// ### Updating Records
|
|
///
|
|
/// - ``updateAll(_:onConflict:_:)-7vv9x``
|
|
/// - ``updateAll(_:onConflict:_:)-7atfw``
|
|
///
|
|
/// ### Building Query Interface Requests
|
|
///
|
|
/// `TableRecord` provide convenience access to most ``DerivableRequest`` and
|
|
/// ``QueryInterfaceRequest`` methods as static methods on the type itself.
|
|
///
|
|
/// - ``aliased(_:)``
|
|
/// - ``all()``
|
|
/// - ``annotated(with:)-3zi1n``
|
|
/// - ``annotated(with:)-4xoen``
|
|
/// - ``annotated(with:)-8ce7u``
|
|
/// - ``annotated(with:)-79389``
|
|
/// - ``annotated(withOptional:)``
|
|
/// - ``annotated(withRequired:)``
|
|
/// - ``filter(_:)``
|
|
/// - ``filter(id:)``
|
|
/// - ``filter(ids:)``
|
|
/// - ``filter(key:)-9ey53``
|
|
/// - ``filter(key:)-34lau``
|
|
/// - ``filter(keys:)-4hq8y``
|
|
/// - ``filter(keys:)-7skw1``
|
|
/// - ``filter(literal:)``
|
|
/// - ``filter(sql:arguments:)``
|
|
/// - ``having(_:)``
|
|
/// - ``including(all:)``
|
|
/// - ``including(optional:)``
|
|
/// - ``including(required:)``
|
|
/// - ``joining(optional:)``
|
|
/// - ``joining(required:)``
|
|
/// - ``limit(_:offset:)``
|
|
/// - ``matching(_:)-22m4o``
|
|
/// - ``matching(_:)-1t8ph``
|
|
/// - ``none()``
|
|
/// - ``order(_:)-9rc11``
|
|
/// - ``order(_:)-2033k``
|
|
/// - ``order(literal:)``
|
|
/// - ``order(sql:arguments:)``
|
|
/// - ``orderByPrimaryKey()``
|
|
/// - ``request(for:)``
|
|
/// - ``select(_:)-1gvtj``
|
|
/// - ``select(_:)-5oylt``
|
|
/// - ``select(_:as:)-1puz3``
|
|
/// - ``select(_:as:)-tjh0``
|
|
/// - ``select(literal:)``
|
|
/// - ``select(literal:as:)``
|
|
/// - ``select(sql:arguments:)``
|
|
/// - ``select(sql:arguments:as:)``
|
|
/// - ``selectPrimaryKey(as:)``
|
|
/// - ``with(_:)``
|
|
///
|
|
/// ### Defining Associations
|
|
///
|
|
/// - ``association(to:)``
|
|
/// - ``association(to:on:)``
|
|
/// - ``belongsTo(_:key:using:)-13t5r``
|
|
/// - ``belongsTo(_:key:using:)-81six``
|
|
/// - ``hasMany(_:key:using:)-45axo``
|
|
/// - ``hasMany(_:key:using:)-10d4k``
|
|
/// - ``hasMany(_:through:using:key:)``
|
|
/// - ``hasOne(_:key:using:)-4g9tm``
|
|
/// - ``hasOne(_:key:using:)-4v5xa``
|
|
/// - ``hasOne(_:through:using:key:)``
|
|
public protocol TableRecord {
|
|
/// The name of the database table used to build SQL queries.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord {
|
|
/// static var databaseTableName = "player"
|
|
/// }
|
|
///
|
|
/// // SELECT * FROM player
|
|
/// try Player.fetchAll(db)
|
|
/// ```
|
|
static var databaseTableName: String { get }
|
|
|
|
/// The columns selected by the record.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord {
|
|
/// static let databaseSelection: [any SQLSelectable] = [AllColumns()]
|
|
/// }
|
|
///
|
|
/// struct PartialPlayer: TableRecord {
|
|
/// static let databaseTableName = "player"
|
|
/// static let databaseSelection: [any SQLSelectable] = [
|
|
/// Column("id"),
|
|
/// Column("name"),
|
|
/// ]
|
|
/// }
|
|
///
|
|
/// // SELECT * FROM player
|
|
/// try Player.fetchAll(db)
|
|
///
|
|
/// // SELECT id, name FROM player
|
|
/// try PartialPlayer.fetchAll(db)
|
|
/// ```
|
|
///
|
|
/// > Important: Make sure the `databaseSelection` property is
|
|
/// > explicitly declared as `[any SQLSelectable]`. If it is not, the
|
|
/// > Swift compiler may silently miss the protocol requirement,
|
|
/// > resulting in sticky `SELECT *` requests.
|
|
static var databaseSelection: [any SQLSelectable] { get }
|
|
}
|
|
|
|
extension TableRecord {
|
|
|
|
/// The default name of the database table used to build requests.
|
|
///
|
|
/// - Player -> "player"
|
|
/// - Place -> "place"
|
|
/// - PostalAddress -> "postalAddress"
|
|
/// - HTTPRequest -> "httpRequest"
|
|
/// - TOEFL -> "toefl"
|
|
static var defaultDatabaseTableName: String {
|
|
if let cached = defaultDatabaseTableNameCache.object(forKey: "\(Self.self)" as NSString) {
|
|
return cached as String
|
|
}
|
|
let typeName = "\(Self.self)".replacingOccurrences(of: "(.)\\b.*$", with: "$1", options: [.regularExpression])
|
|
let initial = typeName.replacingOccurrences(of: "^([A-Z]+).*$", with: "$1", options: [.regularExpression])
|
|
let tableName: String
|
|
switch initial.count {
|
|
case typeName.count:
|
|
tableName = initial.lowercased()
|
|
case 0:
|
|
tableName = typeName
|
|
case 1:
|
|
tableName = initial.lowercased() + typeName.dropFirst()
|
|
default:
|
|
tableName = initial.dropLast().lowercased() + typeName.dropFirst(initial.count - 1)
|
|
}
|
|
defaultDatabaseTableNameCache.setObject(tableName as NSString, forKey: "\(Self.self)" as NSString)
|
|
return tableName
|
|
}
|
|
|
|
/// The default name of the database table is derived from the name of
|
|
/// the type.
|
|
///
|
|
/// - `Player` -> "player"
|
|
/// - `Place` -> "place"
|
|
/// - `PostalAddress` -> "postalAddress"
|
|
/// - `HTTPRequest` -> "httpRequest"
|
|
/// - `TOEFL` -> "toefl"
|
|
public static var databaseTableName: String {
|
|
defaultDatabaseTableName
|
|
}
|
|
|
|
/// The default selection is all columns: `[AllColumns()]`.
|
|
public static var databaseSelection: [any SQLSelectable] {
|
|
[AllColumns()]
|
|
}
|
|
}
|
|
|
|
extension TableRecord {
|
|
|
|
// MARK: - Counting All
|
|
|
|
/// Returns the number of records in the database table.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
///
|
|
/// try dbQueue.read { db in
|
|
/// // SELECT COUNT(*) FROM player
|
|
/// let count = try Player.fetchCount(db)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameter db: A database connection.
|
|
public static func fetchCount(_ db: Database) throws -> Int {
|
|
try all().fetchCount(db)
|
|
}
|
|
}
|
|
|
|
extension TableRecord {
|
|
|
|
// MARK: - SQL Generation
|
|
|
|
/// Returns the number of selected columns.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
///
|
|
/// struct PartialPlayer: TableRecord {
|
|
/// static let databaseTableName = "player"
|
|
/// static let databaseSelection = [Column("id"), Column("name")]
|
|
/// }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// try db.create(table: "player") { t in
|
|
/// t.autoIncrementedPrimaryKey("id")
|
|
/// t.column("name", .text)
|
|
/// t.column("score", .integer)
|
|
/// }
|
|
///
|
|
/// try Player.numberOfSelectedColumns(db) // 3
|
|
/// try PartialPlayer.numberOfSelectedColumns(db) // 2
|
|
/// }
|
|
/// ```
|
|
public static func numberOfSelectedColumns(_ db: Database) throws -> Int {
|
|
// The alias makes it possible to count the columns in `SELECT *`:
|
|
let alias = TableAlias(tableName: databaseTableName)
|
|
let context = SQLGenerationContext(db)
|
|
return try databaseSelection
|
|
.map { $0.sqlSelection.qualified(with: alias) }
|
|
.columnCount(context)
|
|
}
|
|
}
|
|
|
|
// MARK: - Batch Delete
|
|
|
|
extension TableRecord {
|
|
|
|
/// Deletes all records, and returns the number of deleted records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // DELETE FROM player
|
|
/// let count = try Player.deleteAll(db)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: The number of deleted records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@discardableResult
|
|
public static func deleteAll(_ db: Database) throws -> Int {
|
|
try all().deleteAll(db)
|
|
}
|
|
}
|
|
|
|
// MARK: - Check Existence by Single-Column Primary Key
|
|
|
|
extension TableRecord {
|
|
/// Returns whether a record exists for this primary key.
|
|
///
|
|
/// All single-column primary keys are supported:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
/// struct Country: TableRecord { }
|
|
///
|
|
/// try dbQueue.read { db in
|
|
/// let playerExists = try Player.exists(db, key: 1)
|
|
/// let countryExists = try Country.exists(db, key: "FR")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - key: A primary key value.
|
|
/// - returns: Whether a record exists for this primary key.
|
|
public static func exists(_ db: Database, key: some DatabaseValueConvertible) throws -> Bool {
|
|
try !filter(key: key).isEmpty(db)
|
|
}
|
|
}
|
|
|
|
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
|
extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible {
|
|
/// Returns whether a record exists for this primary key.
|
|
///
|
|
/// All single-column primary keys are supported:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord, Identifiable {
|
|
/// var id: Int64
|
|
/// }
|
|
/// struct Country: TableRecord, Identifiable {
|
|
/// var id: String
|
|
/// }
|
|
///
|
|
/// try dbQueue.read { db in
|
|
/// let playerExists = try Player.exists(db, id: 1)
|
|
/// let countryExists = try Country.exists(db, id: "FR")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - id: A primary key value.
|
|
/// - returns: Whether a record exists for this primary key.
|
|
public static func exists(_ db: Database, id: ID) throws -> Bool {
|
|
if id.databaseValue.isNull {
|
|
// Don't hit the database
|
|
return false
|
|
}
|
|
return try !filter(id: id).isEmpty(db)
|
|
}
|
|
}
|
|
|
|
// MARK: - Check Existence by Key
|
|
|
|
extension TableRecord {
|
|
/// Returns whether a record exists for this primary or unique key.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
/// struct Citizenship: TableRecord { }
|
|
///
|
|
/// try dbQueue.read { db in
|
|
/// let playerExists = Player.exists(db, key: ["id": 1])
|
|
/// let playerExists = Player.exists(db, key: ["email": "arthur@example.com"])
|
|
/// let citizenshipExists = Citizenship.exists(db, key: [
|
|
/// "citizenId": 1,
|
|
/// "countryCode": "FR",
|
|
/// ])
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// A fatal error is raised if no unique index exists on a subset of the
|
|
/// key columns.
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - key: A key dictionary.
|
|
/// - returns: Whether a record exists for this key.
|
|
public static func exists(_ db: Database, key: [String: (any DatabaseValueConvertible)?]) throws -> Bool {
|
|
try !filter(key: key).isEmpty(db)
|
|
}
|
|
}
|
|
|
|
// MARK: - Deleting by Single-Column Primary Key
|
|
|
|
extension TableRecord {
|
|
/// Deletes records identified by their primary keys, and returns the number
|
|
/// of deleted records.
|
|
///
|
|
/// All single-column primary keys are supported:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
/// struct Country: TableRecord { }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // DELETE FROM player WHERE id IN (1, 2, 3)
|
|
/// try Player.deleteAll(db, keys: [1, 2, 3])
|
|
///
|
|
/// // DELETE FROM country WHERE code IN ('FR', 'US')
|
|
/// try Country.deleteAll(db, keys: ["FR", "US"])
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - keys: A sequence of primary keys.
|
|
/// - returns: The number of deleted records.
|
|
@discardableResult
|
|
public static func deleteAll<Keys>(_ db: Database, keys: Keys)
|
|
throws -> Int
|
|
where Keys: Sequence, Keys.Element: DatabaseValueConvertible
|
|
{
|
|
let keys = Array(keys)
|
|
if keys.isEmpty {
|
|
// Avoid hitting the database
|
|
return 0
|
|
}
|
|
return try filter(keys: keys).deleteAll(db)
|
|
}
|
|
|
|
/// Deletes the record identified by its primary key, and returns whether a
|
|
/// record was deleted.
|
|
///
|
|
/// All single-column primary keys are supported:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
/// struct Country: TableRecord { }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // DELETE FROM player WHERE id = 1
|
|
/// try Player.deleteOne(db, key: 1)
|
|
///
|
|
/// // DELETE FROM country WHERE code = 'FR'
|
|
/// try Country.deleteOne(db, key: "FR")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - key: A primary key value.
|
|
/// - returns: Whether a record was deleted.
|
|
@discardableResult
|
|
public static func deleteOne(_ db: Database, key: some DatabaseValueConvertible) throws -> Bool {
|
|
if key.databaseValue.isNull {
|
|
// Don't hit the database
|
|
return false
|
|
}
|
|
return try deleteAll(db, keys: [key]) > 0
|
|
}
|
|
}
|
|
|
|
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
|
extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible {
|
|
/// Deletes records identified by their primary keys, and returns the number
|
|
/// of deleted records.
|
|
///
|
|
/// All single-column primary keys are supported:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord, Identifiable {
|
|
/// var id: Int64
|
|
/// }
|
|
/// struct Country: TableRecord, Identifiable {
|
|
/// var id: String
|
|
/// }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // DELETE FROM player WHERE id IN (1, 2, 3)
|
|
/// try Player.deleteAll(db, ids: [1, 2, 3])
|
|
///
|
|
/// // DELETE FROM country WHERE code IN ('FR', 'US')
|
|
/// try Country.deleteAll(db, ids: ["FR", "US"])
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - ids: A collection of primary keys.
|
|
/// - returns: The number of deleted records.
|
|
@discardableResult
|
|
public static func deleteAll<IDS>(_ db: Database, ids: IDS) throws -> Int
|
|
where IDS: Collection, IDS.Element == ID
|
|
{
|
|
if ids.isEmpty {
|
|
// Avoid hitting the database
|
|
return 0
|
|
}
|
|
return try filter(ids: ids).deleteAll(db)
|
|
}
|
|
|
|
/// Deletes the record identified by its primary key, and returns whether a
|
|
/// record was deleted.
|
|
///
|
|
/// All single-column primary keys are supported:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord, Identifiable {
|
|
/// var id: Int64
|
|
/// }
|
|
/// struct Country: TableRecord, Identifiable {
|
|
/// var id: String
|
|
/// }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // DELETE FROM player WHERE id = 1
|
|
/// try Player.deleteOne(db, id: 1)
|
|
///
|
|
/// // DELETE FROM country WHERE code = 'FR'
|
|
/// try Country.deleteOne(db, id: "FR")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - id: A primary key value.
|
|
/// - returns: Whether a record was deleted.
|
|
@discardableResult
|
|
public static func deleteOne(_ db: Database, id: ID) throws -> Bool {
|
|
try deleteAll(db, ids: [id]) > 0
|
|
}
|
|
}
|
|
|
|
// MARK: - Deleting by Key
|
|
|
|
extension TableRecord {
|
|
/// Deletes records identified by their primary or unique keys, and returns
|
|
/// the number of deleted records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
/// struct Citizenship: TableRecord { }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // DELETE FROM player WHERE id = 1
|
|
/// try Player.deleteAll(db, keys: [["id": 1]])
|
|
///
|
|
/// // DELETE FROM player WHERE email = 'arthur@example.com'
|
|
/// try Player.deleteAll(db, keys: [["email": "arthur@example.com"]])
|
|
///
|
|
/// // DELETE FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
|
|
/// try Citizenship.deleteAll(db, keys: [
|
|
/// ["citizenId": 1, "countryCode": "FR"],
|
|
/// ])
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// A fatal error is raised if no unique index exists on a subset of the
|
|
/// key columns.
|
|
///
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - keys: An array of key dictionaries.
|
|
/// - returns: The number of deleted records.
|
|
@discardableResult
|
|
public static func deleteAll(_ db: Database, keys: [[String: (any DatabaseValueConvertible)?]]) throws -> Int {
|
|
if keys.isEmpty {
|
|
// Avoid hitting the database
|
|
return 0
|
|
}
|
|
return try filter(keys: keys).deleteAll(db)
|
|
}
|
|
|
|
/// Deletes the record identified by its primary or unique key, and returns
|
|
/// whether a record was deleted.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
/// struct Citizenship: TableRecord { }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // DELETE FROM player WHERE id = 1
|
|
/// try Player.deleteOne(db, key: ["id": 1])
|
|
///
|
|
/// // DELETE FROM player WHERE email = 'arthur@example.com'
|
|
/// try Player.deleteOne(db, key: ["email": "arthur@example.com"])
|
|
///
|
|
/// // DELETE FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
|
|
/// try Citizenship.deleteOne(db, key: [
|
|
/// "citizenId": 1,
|
|
/// "countryCode": "FR",
|
|
/// ])
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// A fatal error is raised if no unique index exists on a subset of the
|
|
/// key columns.
|
|
/// - parameters:
|
|
/// - db: A database connection.
|
|
/// - key: A key dictionary.
|
|
/// - returns: Whether a record was deleted.
|
|
@discardableResult
|
|
public static func deleteOne(_ db: Database, key: [String: (any DatabaseValueConvertible)?]) throws -> Bool {
|
|
try deleteAll(db, keys: [key]) > 0
|
|
}
|
|
}
|
|
|
|
// MARK: - Batch Update
|
|
|
|
extension TableRecord {
|
|
|
|
/// Updates all records, and returns the number of updated records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = 0
|
|
/// try Player.updateAll(db, [Column("score").set(to: 0)])
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution,
|
|
/// defaulting to the record's persistenceConflictPolicy.
|
|
/// - parameter assignments: An array of column assignments.
|
|
/// - returns: The number of updated records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@discardableResult
|
|
public static func updateAll(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: [ColumnAssignment])
|
|
throws -> Int
|
|
{
|
|
try all().updateAll(db, onConflict: conflictResolution, assignments)
|
|
}
|
|
|
|
/// Updates all records, and returns the number of updated records.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// struct Player: TableRecord { }
|
|
///
|
|
/// try dbQueue.write { db in
|
|
/// // UPDATE player SET score = 0
|
|
/// try Player.updateAll(db, Column("score").set(to: 0))
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - parameter conflictResolution: A policy for conflict resolution,
|
|
/// defaulting to the record's persistenceConflictPolicy.
|
|
/// - parameter assignments: Column assignments.
|
|
/// - returns: The number of updated records.
|
|
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
|
@discardableResult
|
|
public static func updateAll(
|
|
_ db: Database,
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
_ assignments: ColumnAssignment...)
|
|
throws -> Int
|
|
{
|
|
try updateAll(db, onConflict: conflictResolution, assignments)
|
|
}
|
|
}
|
|
|
|
// MARK: - RecordError
|
|
|
|
/// A record error.
|
|
///
|
|
/// `RecordError` is thrown by ``MutablePersistableRecord`` types when an
|
|
/// `update` method could not find any row to update:
|
|
///
|
|
/// ```swift
|
|
/// do {
|
|
/// try player.update(db)
|
|
/// } catch let RecordError.recordNotFound(databaseTableName: table, key: key) {
|
|
/// print("Key \(key) was not found in table \(table).")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// `RecordError` is also thrown by ``FetchableRecord`` types when a
|
|
/// `find` method does not find any record:
|
|
///
|
|
/// ```swift
|
|
/// do {
|
|
/// let player = try Player.find(db, id: 42)
|
|
/// } catch let RecordError.recordNotFound(databaseTableName: table, key: key) {
|
|
/// print("Key \(key) was not found in table \(table).")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// You can create `RecordError` instances with the
|
|
/// ``TableRecord/recordNotFound(_:id:)`` method and its variants.
|
|
public enum RecordError: Error {
|
|
/// A record does not exist in the database.
|
|
///
|
|
/// - parameters:
|
|
/// - databaseTableName: The table of the missing record.
|
|
/// - key: The key of the missing record (column and values).
|
|
case recordNotFound(databaseTableName: String, key: [String: DatabaseValue])
|
|
}
|
|
|
|
extension RecordError: CustomStringConvertible {
|
|
public var description: String {
|
|
switch self {
|
|
case let .recordNotFound(databaseTableName: databaseTableName, key: key):
|
|
let row = Row(key) // For nice output
|
|
return "Key not found in table \(databaseTableName): \(row.description)"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TableRecord {
|
|
/// Returns an error for a record that does not exist in the database.
|
|
///
|
|
/// - returns: ``RecordError/recordNotFound(databaseTableName:key:)``, or
|
|
/// any error that prevented the `RecordError` from being constructed.
|
|
public static func recordNotFound(_ db: Database, key: some DatabaseValueConvertible) -> any Error {
|
|
do {
|
|
let primaryKey = try db.primaryKey(databaseTableName)
|
|
GRDBPrecondition(
|
|
primaryKey.columns.count == 1,
|
|
"Requesting by key requires a single-column primary key in the table \(databaseTableName)")
|
|
return RecordError.recordNotFound(
|
|
databaseTableName: databaseTableName,
|
|
key: [primaryKey.columns[0]: key.databaseValue])
|
|
} catch {
|
|
return error
|
|
}
|
|
}
|
|
|
|
/// Returns an error for a record that does not exist in the database.
|
|
public static func recordNotFound(key: [String: (any DatabaseValueConvertible)?]) -> RecordError {
|
|
RecordError.recordNotFound(
|
|
databaseTableName: databaseTableName,
|
|
key: key.mapValues { $0?.databaseValue ?? .null })
|
|
}
|
|
}
|
|
|
|
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
|
extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible {
|
|
/// Returns an error for a record that does not exist in the database.
|
|
///
|
|
/// - returns: ``RecordError/recordNotFound(databaseTableName:key:)``, or
|
|
/// any error that prevented the `RecordError` from being constructed.
|
|
public static func recordNotFound(_ db: Database, id: Self.ID) -> any Error {
|
|
recordNotFound(db, key: id)
|
|
}
|
|
}
|
|
|
|
@available(*, deprecated, renamed: "RecordError")
|
|
public typealias PersistenceError = RecordError
|
|
|
|
/// Calculating `defaultDatabaseTableName` is somewhat expensive due to the regular expression evaluation
|
|
///
|
|
/// This cache mitigates the cost of the calculation by storing the name for later retrieval
|
|
private let defaultDatabaseTableNameCache = NSCache<NSString, NSString>()
|