diff --git a/Sources/Lighter/Operations/SQLDatabaseAsyncChangeOperations.swift b/Sources/Lighter/Operations/SQLDatabaseAsyncChangeOperations.swift index 7afd764..24969a0 100644 --- a/Sources/Lighter/Operations/SQLDatabaseAsyncChangeOperations.swift +++ b/Sources/Lighter/Operations/SQLDatabaseAsyncChangeOperations.swift @@ -224,10 +224,10 @@ public extension SQLDatabaseAsyncChangeOperations { * - records: The records to update. */ @inlinable - func update(_ records: S) async throws - where S: Sequence & Sendable, - S.Element: SQLUpdatableRecord, - S.Element.Schema: SQLKeyedTableSchema + func update(_ records: C) async throws + where C: Collection & Sendable, + C.Element: SQLUpdatableRecord, + C.Element.Schema: SQLKeyedTableSchema { try await runOnDatabaseQueue { try update(records) } } @@ -256,8 +256,8 @@ public extension SQLDatabaseAsyncChangeOperations { */ @inlinable @discardableResult - func insert(_ records: S) async throws -> [ S.Element ] - where S: Sequence & Sendable, S.Element: SQLInsertableRecord + func insert(_ records: C) async throws -> [ C.Element ] + where C: Collection & Sendable, C.Element: SQLInsertableRecord { try await runOnDatabaseQueue { try insert(records) } } diff --git a/Sources/Lighter/Operations/SQLDatabaseChangeOperations.swift b/Sources/Lighter/Operations/SQLDatabaseChangeOperations.swift index 4a16156..4301911 100644 --- a/Sources/Lighter/Operations/SQLDatabaseChangeOperations.swift +++ b/Sources/Lighter/Operations/SQLDatabaseChangeOperations.swift @@ -218,22 +218,84 @@ extension SQLDatabaseChangeOperations { .deleteFailed(record: record), ok, sqlite3_errmsg(db)) } } +} +extension SQLDatabaseChangeOperations { // MARK: - Update + + /** + * Update a record in the given database. + * + * - Parameters: + * - record: A `SQLUpdatableRecord`. + * - db: A SQLite database handle. + */ @usableFromInline func update(_ record: T, in db: OpaquePointer) throws where T: SQLUpdatableRecord, T.Schema: SQLKeyedTableSchema { // UPDATE table SET values WHERE pkey + // ^^^ this really needs a primary key, i.e. doesn't work on views. + let ( mStatement, ok ) = prepareUpdate(T.self, in: db) + guard let statement = mStatement else { + throw LighterError( + .updateFailed(record: record), ok, sqlite3_errmsg(db)) + } + defer { sqlite3_finalize(statement) } + try bindUpdateAndExecute(record, using: statement, in: db) + } + + /** + * Update a set of uniform records in the given database. + * This reuses the same prepared statement for all records. + * + * - Parameters: + * - records: A collection of `SQLUpdatableRecord`s. + * - db: A SQLite database handle. + */ + @usableFromInline + func update(_ records: C, in db: OpaquePointer) throws + where C: Collection, + C.Element: SQLUpdatableRecord, + C.Element.Schema: SQLKeyedTableSchema + { + // UPDATE table SET values WHERE pkey + typealias T = C.Element + guard !records.isEmpty else { return } + let ( mStatement, ok ) = prepareUpdate(T.self, in: db) + guard let statement = mStatement else { + throw LighterError( // Hmmm + .updateFailed(record: records.first!), ok, sqlite3_errmsg(db)) + } + defer { sqlite3_finalize(statement) } + + for record in records { + try bindUpdateAndExecute(record, using: statement, in: db) + } + } + + private func prepareUpdate(_ recordType: T.Type, + in db: OpaquePointer) + -> ( OpaquePointer?, Int32 ) + where T: SQLUpdatableRecord, T.Schema: SQLKeyedTableSchema + { + // UPDATE table SET values WHERE pkey + // ^^^ this really needs a primary key, i.e. doesn't work on views. var statement : OpaquePointer? let ok = sqlite3_prepare_v2(db, T.Schema.update, -1, &statement, nil) - defer { sqlite3_finalize(statement) } guard ok == SQLITE_OK else { assert(ok == SQLITE_OK) - throw LighterError( - .updateFailed(record: record), ok, sqlite3_errmsg(db)) + sqlite3_finalize(statement) + return ( nil, ok ) } - + return ( statement, ok ) + } + + private func bindUpdateAndExecute(_ record: T, + using statement: OpaquePointer, + in db: OpaquePointer) throws + where T: SQLUpdatableRecord, T.Schema: SQLKeyedTableSchema + { let rok = record.bind(to: statement, indices: T.Schema.updateParameterIndices) { @@ -241,16 +303,94 @@ extension SQLDatabaseChangeOperations { } assert(rok == SQLITE_DONE) - // We allow 'row' results, not really and error, we just don't use them + // We allow 'row' results, not really an error, we just don't use them if rok != SQLITE_ROW && rok != SQLITE_DONE { throw LighterError( - .updateFailed(record: record), ok, sqlite3_errmsg(db)) + .updateFailed(record: record), SQLITE_OK, sqlite3_errmsg(db)) } } +} +extension SQLDatabaseChangeOperations { // MARK: - Insert + + /** + * Insert a record into the given database. + * + * - Parameters: + * - record: A `SQLInsertableRecord`. + * - db: A SQLite database handle. + * - Returns: The value of the records that got inserted. + */ @usableFromInline func insert(_ record: T, into db: OpaquePointer) throws -> T - where T: SQLInsertableRecord + where T: SQLInsertableRecord + { + // "INSERT INTO table ( names ) WHERE ( ?, ?, ? ) RETURNING *" + // RETURNING requires SQLite3 3.35.0+ (2021-03-12) + let ( mStatement, fetchStatement, ok ) = prepareInsert(T.self, in: db) + guard ok == SQLITE_OK, let statement = mStatement else { + assert(ok == SQLITE_OK) + throw LighterError( + .insertFailed(record: record), ok, sqlite3_errmsg(db)) + } + defer { + sqlite3_finalize(statement) + sqlite3_finalize(fetchStatement) + } + + return try bindInsertAndExecute( + record, + using: statement, fetch: fetchStatement, + in: db + ) + } + + /** + * Insert a set of uniform records into the given database. + * This reuses the same prepared statement for all records. + * + * - Parameters: + * - records: A collection of `SQLInsertableRecord`s. + * - db: A SQLite database handle. + * - Returns: The values of the records that got inserted. + */ + @usableFromInline + func insert(_ records: C, into db: OpaquePointer) throws -> [ C.Element ] + where C: Collection, C.Element: SQLInsertableRecord + { + // "INSERT INTO table ( names ) WHERE ( ?, ?, ? ) RETURNING *" + // RETURNING requires SQLite3 3.35.0+ (2021-03-12) + typealias T = C.Element + guard !records.isEmpty else { return [] } + + let ( mStatement, fetchStatement, ok ) = prepareInsert(T.self, in: db) + guard ok == SQLITE_OK, let statement = mStatement else { + assert(ok == SQLITE_OK) + throw LighterError( + .insertFailed(record: records.first!), ok, sqlite3_errmsg(db)) + } + defer { + sqlite3_finalize(statement) + sqlite3_finalize(fetchStatement) + } + + var results = [ C.Element ]() + results.reserveCapacity(records.count) + for record in records { + let result = try bindInsertAndExecute( + record, + using: statement, fetch: fetchStatement, + in: db + ) + results.append(result) + } + return results + } + + private func prepareInsert(_ recordType: T.Type, + in db: OpaquePointer) + -> ( OpaquePointer?, OpaquePointer?, Int32 ) + where T: SQLInsertableRecord { // "INSERT INTO table ( names ) WHERE ( ?, ?, ? ) RETURNING *" // RETURNING requires SQLite3 3.35.0+ (2021-03-12) @@ -259,14 +399,37 @@ extension SQLDatabaseChangeOperations { var statement : OpaquePointer? let ok = sqlite3_prepare_v2(db, sql, -1, &statement, nil) - defer { sqlite3_finalize(statement) } - + guard ok == SQLITE_OK else { assert(ok == SQLITE_OK) - throw LighterError( - .insertFailed(record: record), ok, sqlite3_errmsg(db)) + sqlite3_finalize(statement) + return ( nil, nil, ok ) + } + + var fetchStatement : OpaquePointer? + if !supportsReturning { + // Provide an own "RETURNING" implementation... + let sql = T.Schema.select + " WHERE ROWID = last_insert_rowid();" + let ok = sqlite3_prepare_v2(db, sql, -1, &fetchStatement, nil) + guard ok == SQLITE_OK else { + assert(ok == SQLITE_OK) + sqlite3_finalize(statement) + sqlite3_finalize(fetchStatement) + return ( nil, nil, ok ) + } } + return ( statement, fetchStatement, ok ) + } + + private func bindInsertAndExecute(_ record: T, + using statement: OpaquePointer, + fetch fetchStatement: OpaquePointer?, + in db: OpaquePointer) throws -> T + where T: SQLInsertableRecord + { + let supportsReturning = fetchStatement == nil + let rok = record.bind(to: statement, indices: T.Schema.insertParameterIndices) { @@ -281,18 +444,8 @@ extension SQLDatabaseChangeOperations { assertionFailure("Expected new record to be returned") return record } - - // Provide an own "RETURNING" implementation... - let sql = T.Schema.select + " WHERE ROWID = last_insert_rowid();" - var statement : OpaquePointer? - let ok = sqlite3_prepare_v2(db, sql, -1, &statement, nil) - defer { sqlite3_finalize(statement) } - guard ok == SQLITE_OK else { - assert(ok == SQLITE_OK) - throw LighterError( - .insertFailed(record: record), ok, sqlite3_errmsg(db)) - } - let rok = sqlite3_step(statement) + + let rok = sqlite3_step(fetchStatement) if rok == SQLITE_ROW { return T(statement, indices: T.Schema.selectColumnIndices) } @@ -306,7 +459,7 @@ extension SQLDatabaseChangeOperations { } else { throw LighterError( - .insertFailed(record: record), ok, sqlite3_errmsg(db)) + .insertFailed(record: record), rok, sqlite3_errmsg(db)) } } } @@ -355,13 +508,14 @@ public extension SQLDatabaseChangeOperations { * - records: The records to update. */ @inlinable - func update(_ records: S) throws - where S: Sequence, - S.Element: SQLUpdatableRecord, - S.Element.Schema: SQLKeyedTableSchema + func update(_ records: C) throws + where C: Collection, + C.Element: SQLUpdatableRecord, + C.Element.Schema: SQLKeyedTableSchema { + guard !records.isEmpty else { return } try connectionHandler.withConnection(readOnly: false) { db in - try records.forEach { try update($0, in: db) } + try update(records, in: db) } } @@ -389,13 +543,14 @@ public extension SQLDatabaseChangeOperations { */ @inlinable @discardableResult - func insert(_ records: S) throws -> [ S.Element ] - where S: Sequence, S.Element: SQLInsertableRecord + func insert(_ records: C) throws -> [ C.Element ] + where C: Collection, C.Element: SQLInsertableRecord { // There could be an `any T` variant, but that would make the return value // less convenient on the consuming side. + guard !records.isEmpty else { return [] } return try connectionHandler.withConnection(readOnly: false) { db in - return try records.map { try insert($0, into: db) } + return try insert(records, into: db) } } } @@ -439,12 +594,12 @@ public extension SQLDatabaseChangeOperations where Self: SQLDatabase { * - records: The records to update. */ @inlinable - func update(_ records: S) throws - where S: Sequence, - S.Element: SQLUpdatableRecord, - S.Element.Schema: SQLKeyedTableSchema + func update(_ records: C) throws + where C: Collection, + C.Element: SQLUpdatableRecord, + C.Element.Schema: SQLKeyedTableSchema { - try transaction(mode: .immediate) { try $0.update(records) } + try transaction(mode: .immediate) { tx in try tx.update(records) } } /** @@ -471,9 +626,9 @@ public extension SQLDatabaseChangeOperations where Self: SQLDatabase { */ @inlinable @discardableResult - func insert(_ records: S) throws -> [ S.Element ] - where S: Sequence, S.Element: SQLInsertableRecord + func insert(_ records: C) throws -> [ C.Element ] + where C: Collection, C.Element: SQLInsertableRecord { - try transaction(mode: .immediate) { try $0.insert(records) } + try transaction(mode: .immediate) { tx in try tx.insert(records) } } }