Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions Sources/SQLite/AggregateFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public extension Connection {
deterministic: Bool = false,
initialState: @escaping () -> State,
step: @escaping (inout State, borrowing [Binding]) -> Void,
final: @escaping (State) -> Binding
final: @escaping (State) throws -> Binding
) throws(SQLiteError) {
try handle.createAggregateFunction(
name,
Expand Down Expand Up @@ -66,12 +66,12 @@ internal final class AggregateFunctionBox {

let step: (AnyObject, borrowing [Binding]) -> Void

let final: (AnyObject) -> Binding
let final: (AnyObject) throws -> Binding

init<State>(
initialState: @escaping () -> State,
step: @escaping (inout State, borrowing [Binding]) -> Void,
final: @escaping (State) -> Binding
final: @escaping (State) throws -> Binding
) {
self.makeState = { AggregateStateBox(initialState()) }
self.step = { boxed, arguments in
Expand All @@ -80,7 +80,7 @@ internal final class AggregateFunctionBox {
}
self.final = { boxed in
let box = boxed as! AggregateStateBox<State>
return final(box.state)
return try final(box.state)
}
}
}
Expand All @@ -93,7 +93,7 @@ internal extension Connection.Handle {
deterministic: Bool,
initialState: @escaping () -> State,
step: @escaping (inout State, borrowing [Binding]) -> Void,
final: @escaping (State) -> Binding
final: @escaping (State) throws -> Binding
) -> Result<Void, SQLiteError> {
let box = AggregateFunctionBox(initialState: initialState, step: step, final: final)
let context = Unmanaged.passRetained(box).toOpaque()
Expand Down Expand Up @@ -125,7 +125,11 @@ internal extension Connection.Handle {
}
let functionBox = Unmanaged<AggregateFunctionBox>.fromOpaque(boxPointer).takeUnretainedValue()
let stateObject = sqliteContext.finalizeAggregateState() ?? functionBox.makeState()
sqliteContext.setResult(functionBox.final(stateObject))
do {
sqliteContext.setResult(try functionBox.final(stateObject))
} catch {
sqliteContext.setError(error)
}
},
{ boxPointer in
guard let boxPointer else { return }
Expand Down
125 changes: 125 additions & 0 deletions Sources/SQLite/Backup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// Backup.swift
// SQLite
//
// Created by Alsey Coleman Miller on 7/17/26.
//

#if SQLITE_SWIFT_STANDALONE
import sqlite3
#elseif SQLITE_SWIFT_SQLCIPHER
import SQLCipher
#elseif os(Linux)
import SwiftToolchainCSQLite
#else
import SQLite3
#endif

/// Copies the contents of one database connection to another, optionally in incremental steps.
///
/// See: <https://www.sqlite.org/backup.html>
public struct Backup: ~Copyable {

let handle: Handle

/// Initializes a backup of `source` into `destination`.
///
/// - Parameters:
/// - source: Connection to copy from.
/// - sourceDatabase: Name of the attached database to copy from (`"main"` by default).
/// - destination: Connection to copy into.
/// - destinationDatabase: Name of the attached database to copy into (`"main"` by default).
public init(
source: borrowing Connection,
sourceDatabase: String = "main",
destination: borrowing Connection,
destinationDatabase: String = "main"
) throws(SQLiteError) {
self.handle = try Handle.open(
source: source.handle,
sourceDatabase: sourceDatabase,
destination: destination.handle,
destinationDatabase: destinationDatabase
).get()
}

deinit {
handle.finish()
}
}

public extension Backup {

/// The number of pages still to be copied as of the most recent call to `step(pageCount:)`.
var remainingPageCount: Int32 {
handle.remainingPageCount
}

/// The total number of pages in the source database as of the most recent call to `step(pageCount:)`.
var totalPageCount: Int32 {
handle.totalPageCount
}

/// Copies up to `pageCount` pages from the source to the destination database.
///
/// - Parameter pageCount: The number of pages to copy, or a negative value to copy every remaining page.
/// - Returns: `true` if there are more pages left to copy, `false` if the backup is complete.
mutating func step(pageCount: Int32 = -1) throws(SQLiteError) -> Bool {
try handle.step(pageCount: pageCount).get()
}
}

// MARK: - Supporting Types

internal extension Backup {

struct Handle {

let pointer: OpaquePointer

let destination: Connection.Handle
}
}

internal extension Backup.Handle {

static func open(
source: Connection.Handle,
sourceDatabase: String,
destination: Connection.Handle,
destinationDatabase: String
) -> Result<Backup.Handle, SQLiteError> {
guard let pointer = sqlite3_backup_init(
destination.pointer,
destinationDatabase,
source.pointer,
sourceDatabase
) else {
return .failure(destination.forceError(destination.errorCode ?? .init(SQLITE_ERROR)))
}
return .success(Backup.Handle(pointer: pointer, destination: destination))
}

consuming func finish() {
sqlite3_backup_finish(pointer)
}

var remainingPageCount: Int32 {
sqlite3_backup_remaining(pointer)
}

var totalPageCount: Int32 {
sqlite3_backup_pagecount(pointer)
}

func step(pageCount: Int32) -> Result<Bool, SQLiteError> {
let resultCode = sqlite3_backup_step(pointer, pageCount)
if resultCode == SQLITE_DONE {
return .success(false)
}
if resultCode == SQLITE_OK {
return .success(true)
}
return .failure(destination.forceError(SQLiteError.ErrorCode(resultCode)))
}
}
35 changes: 34 additions & 1 deletion Sources/SQLite/Connection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ public extension Connection {
var filename: String {
handle.filename
}

/// Whether the connection is currently outside of an explicit transaction (i.e. in autocommit mode).
var isAutocommit: Bool {
handle.isAutocommit
}

/// Sets a busy timeout, causing operations on locked tables to retry for up to `milliseconds`
/// before returning `SQLITE_BUSY`, instead of failing immediately.
func setBusyTimeout(_ milliseconds: Int32) {
handle.setBusyTimeout(milliseconds)
}

/// Interrupts a long-running query on this connection from another thread, causing it to fail
/// with `SQLITE_INTERRUPT` as soon as possible.
func interrupt() {
handle.interrupt()
}
}

// MARK: - Methods
Expand Down Expand Up @@ -118,7 +135,10 @@ internal extension Connection {
internal extension Connection.Handle {

consuming func close() {
sqlite3_close(pointer)
// sqlite3_close_v2 allows the connection to be deallocated even if statements or
// blob handles created against it haven't been finalized/closed yet, deferring
// actual deallocation until they are.
sqlite3_close_v2(pointer)
}

static func open(path: String, readonly: Bool = false) -> Result<Connection.Handle, SQLiteError> {
Expand Down Expand Up @@ -210,4 +230,17 @@ internal extension Connection.Handle {
var filename: String {
sqlite3_db_filename(pointer, nil).map { String(cString: $0) } ?? ""
}

/// Whether the connection is currently outside of an explicit transaction (i.e. in autocommit mode).
var isAutocommit: Bool {
sqlite3_get_autocommit(pointer) != 0
}

func setBusyTimeout(_ milliseconds: Int32) {
sqlite3_busy_timeout(pointer, milliseconds)
}

func interrupt() {
sqlite3_interrupt(pointer)
}
}
22 changes: 16 additions & 6 deletions Sources/SQLite/Function.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public extension Connection {
_ name: String,
argumentCount: Int32? = nil,
deterministic: Bool = false,
_ block: @escaping (borrowing [Binding]) -> Binding
_ block: @escaping (borrowing [Binding]) throws -> Binding
) throws(SQLiteError) {
try handle.createFunction(
name,
Expand All @@ -48,9 +48,9 @@ public extension Connection {

fileprivate final class FunctionBox {

let block: (borrowing [Binding]) -> Binding
let block: (borrowing [Binding]) throws -> Binding

init(_ block: @escaping (borrowing [Binding]) -> Binding) {
init(_ block: @escaping (borrowing [Binding]) throws -> Binding) {
self.block = block
}
}
Expand All @@ -61,7 +61,7 @@ internal extension Connection.Handle {
_ name: String,
argumentCount: Int32,
deterministic: Bool,
block: @escaping (borrowing [Binding]) -> Binding
block: @escaping (borrowing [Binding]) throws -> Binding
) -> Result<Void, SQLiteError> {
let box = FunctionBox(block)
let context = Unmanaged.passRetained(box).toOpaque()
Expand All @@ -84,8 +84,12 @@ internal extension Connection.Handle {
let value = argv?[index]
return Binding(sqliteValue: value)
}
let result = box.block(arguments)
sqliteContext.setResult(result)
do {
let result = try box.block(arguments)
sqliteContext.setResult(result)
} catch {
sqliteContext.setError(error)
}
},
nil,
nil,
Expand Down Expand Up @@ -161,4 +165,10 @@ internal extension OpaquePointer {
}
}
}

/// Reports `error` as the result of a SQL function invocation, given `self` as the `sqlite3_context`.
func setError(_ error: Error) {
let message = String(describing: error)
sqlite3_result_error(self, message, -1)
}
}
Loading
Loading