Refactor entire codebase and rewrite documentation
This commit is contained in:
144
Sources/DataLiteCore/Docs.docc/Articles/CustomFunctions.md
Normal file
144
Sources/DataLiteCore/Docs.docc/Articles/CustomFunctions.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Extending SQLite with Custom Functions
|
||||
|
||||
Build custom scalar and aggregate SQL functions that run inside SQLite.
|
||||
|
||||
DataLiteCore lets you register custom SQL functions that participate in expressions, queries, and
|
||||
aggregations just like built-in ones. Each function is registered on a specific connection and
|
||||
becomes available to all statements executed through that connection.
|
||||
|
||||
## Registering Functions
|
||||
|
||||
Use ``ConnectionProtocol/add(function:)`` to register a function type on a connection. Pass the
|
||||
function’s type, not an instance. DataLiteCore automatically manages function creation and
|
||||
lifecycle — scalar functions are executed via their type, while aggregate functions are instantiated
|
||||
per SQL invocation.
|
||||
|
||||
```swift
|
||||
try connection.add(function: Function.Regexp.self) // Built-in helper
|
||||
try connection.add(function: Slugify.self) // Custom scalar function
|
||||
```
|
||||
|
||||
To remove a registered function, call ``ConnectionProtocol/remove(function:)``. This is useful for
|
||||
dynamic plug-ins or test environments that require a clean registration state.
|
||||
|
||||
```swift
|
||||
try connection.remove(function: Slugify.self)
|
||||
```
|
||||
|
||||
## Implementing Scalar Functions
|
||||
|
||||
Subclass ``Function/Scalar`` to define a function that returns a single value for each call.
|
||||
Override the static metadata properties — ``Function/name``, ``Function/argc``, and
|
||||
``Function/options`` — to declare the function’s signature, and implement its logic in
|
||||
``Function/Scalar/invoke(args:)``. Return any type conforming to ``SQLiteRepresentable``.
|
||||
|
||||
```swift
|
||||
final class Slugify: Function.Scalar {
|
||||
override class var name: String { "slugify" }
|
||||
override class var argc: Int32 { 1 }
|
||||
override class var options: Function.Options { [.deterministic, .innocuous] }
|
||||
|
||||
override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
|
||||
guard let value = args[0] as String?, !value.isEmpty else { return nil }
|
||||
return value.lowercased()
|
||||
.replacingOccurrences(of: "\\W+", with: "-", options: .regularExpression)
|
||||
.trimmingCharacters(in: .init(charactersIn: "-"))
|
||||
}
|
||||
}
|
||||
|
||||
try connection.add(function: Slugify.self)
|
||||
let rows = try connection.prepare(sql: "SELECT slugify(title) FROM articles")
|
||||
```
|
||||
|
||||
## Implementing Aggregate Functions
|
||||
|
||||
Aggregate functions maintain internal state across multiple rows. Subclass ``Function/Aggregate``
|
||||
and override ``Function/Aggregate/step(args:)`` to process each row and
|
||||
``Function/Aggregate/finalize()`` to produce the final result.
|
||||
|
||||
```swift
|
||||
final class Median: Function.Aggregate {
|
||||
private var values: [Double] = []
|
||||
|
||||
override class var name: String { "median" }
|
||||
override class var argc: Int32 { 1 }
|
||||
override class var options: Function.Options { [.deterministic] }
|
||||
|
||||
override func step(args: any ArgumentsProtocol) throws {
|
||||
if let value = args[0] as Double? {
|
||||
values.append(value)
|
||||
}
|
||||
}
|
||||
|
||||
override func finalize() throws -> SQLiteRepresentable? {
|
||||
guard !values.isEmpty else { return nil }
|
||||
let sorted = values.sorted()
|
||||
let mid = sorted.count / 2
|
||||
return sorted.count.isMultiple(of: 2)
|
||||
? (sorted[mid - 1] + sorted[mid]) / 2
|
||||
: sorted[mid]
|
||||
}
|
||||
}
|
||||
|
||||
try connection.add(function: Median.self)
|
||||
```
|
||||
|
||||
SQLite creates a new instance of an aggregate function for each aggregate expression in a query and
|
||||
reuses it for all rows contributing to that result. It’s safe to store mutable state in instance
|
||||
properties.
|
||||
|
||||
## Handling Arguments and Results
|
||||
|
||||
Custom functions receive input through an ``ArgumentsProtocol`` instance. Use subscripts to access
|
||||
arguments by index and automatically convert them to Swift types.
|
||||
|
||||
Two access forms are available:
|
||||
|
||||
- `subscript(index: Index) -> SQLiteValue` — returns the raw SQLite value without conversion.
|
||||
- `subscript<T: SQLiteRepresentable>(index: Index) -> T?`— converts the value to a Swift type
|
||||
conforming to ``SQLiteRepresentable``. Returns `nil` if the argument is `NULL` or cannot be
|
||||
converted.
|
||||
|
||||
Use ``Function/Arguments/count`` to verify argument count before accessing elements. For
|
||||
fine-grained decoding control, prefer the raw ``SQLiteValue`` form and handle conversion manually.
|
||||
|
||||
```swift
|
||||
override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
|
||||
guard args.count == 2 else {
|
||||
throw SQLiteError(code: SQLITE_MISUSE, message: "expected two arguments")
|
||||
}
|
||||
guard let lhs = args[0] as Double?, let rhs = args[1] as Double? else {
|
||||
return nil // returns SQL NULL if either argument is NULL
|
||||
}
|
||||
return lhs * rhs
|
||||
}
|
||||
```
|
||||
|
||||
Any type conforming to ``SQLiteRepresentable`` can be used both to read arguments and to return
|
||||
results. Returning `nil` produces an SQL `NULL`.
|
||||
|
||||
## Choosing Function Options
|
||||
|
||||
Customize function characteristics via the ``Function/Options`` bitset:
|
||||
|
||||
- ``Function/Options/deterministic`` — identical arguments always yield the same result, enabling
|
||||
SQLite to cache calls and optimize query plans.
|
||||
- ``Function/Options/directonly`` — restricts usage to trusted contexts (for example, disallows
|
||||
calls from triggers or CHECK constraints).
|
||||
- ``Function/Options/innocuous`` — marks the function as side-effect-free and safe for untrusted
|
||||
SQL.
|
||||
|
||||
Each scalar or aggregate subclass may return a different option set, depending on its behavior.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Throwing from ``Function/Scalar/invoke(args:)``, ``Function/Aggregate/step(args:)``, or
|
||||
``Function/Aggregate/finalize()`` propagates an error back to SQLite. DataLiteCore converts the
|
||||
thrown error into a generic `SQLITE_ERROR` result code and uses its `localizedDescription` as the
|
||||
message text.
|
||||
|
||||
You can use this mechanism to signal both validation failures and runtime exceptions during function
|
||||
execution. Throwing an error stops evaluation immediately and returns control to SQLite.
|
||||
|
||||
- SeeAlso: ``Function``
|
||||
- SeeAlso: [Application-Defined SQL Functions](https://sqlite.org/appfunc.html)
|
||||
@@ -0,0 +1,61 @@
|
||||
# Database Encryption
|
||||
|
||||
Secure SQLite databases with SQLCipher encryption using DataLiteCore.
|
||||
|
||||
DataLiteCore provides a clean API for applying and rotating SQLCipher encryption keys through the
|
||||
connection interface. You can use it to unlock existing encrypted databases or to initialize new
|
||||
ones securely before executing SQL statements.
|
||||
|
||||
## Applying an Encryption Key
|
||||
|
||||
Use ``ConnectionProtocol/apply(_:name:)`` to unlock an encrypted database file or to initialize
|
||||
encryption on a new one. Supported key formats include:
|
||||
|
||||
- ``Connection/Key/passphrase(_:)`` — a textual passphrase processed by SQLCipher’s key derivation.
|
||||
- ``Connection/Key/rawKey(_:)`` — a 256-bit (`32`-byte) key supplied as `Data`.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
location: .file(path: "/path/to/sqlite.db"),
|
||||
options: [.readwrite, .create, .fullmutex]
|
||||
)
|
||||
try connection.apply(.passphrase("vault-password"), name: nil)
|
||||
```
|
||||
|
||||
The first call on a new database establishes encryption. If the database already exists and is
|
||||
encrypted, the same call unlocks it for the current session. Plaintext files cannot be encrypted in
|
||||
place. Always call ``ConnectionProtocol/apply(_:name:)`` immediately after opening the connection
|
||||
and before executing any statements to avoid `SQLITE_NOTADB` errors.
|
||||
|
||||
## Rotating Keys
|
||||
|
||||
Use ``ConnectionProtocol/rekey(_:name:)`` to rewrite the database with a new key. The connection
|
||||
must already be unlocked with the current key via ``ConnectionProtocol/apply(_:name:)``.
|
||||
|
||||
```swift
|
||||
let newKey = Data((0..<32).map { _ in UInt8.random(in: 0...UInt8.max) })
|
||||
try connection.rekey(.rawKey(newKey), name: nil)
|
||||
```
|
||||
|
||||
Rekeying touches every page in the database and can take noticeable time on large files. Schedule
|
||||
it during maintenance windows and be prepared for `SQLITE_BUSY` if other connections keep the file
|
||||
locked. Adjust ``ConnectionProtocol/busyTimeout`` or coordinate access with application-level
|
||||
locking.
|
||||
|
||||
## Attached Databases
|
||||
|
||||
When attaching additional databases, pass the attachment alias through the `name` parameter.
|
||||
Use `nil` or `"main"` for the primary database, `"temp"` for the temporary one, and the alias for
|
||||
others.
|
||||
|
||||
```swift
|
||||
try connection.execute(raw: "ATTACH DATABASE 'analytics.db' AS analytics")
|
||||
try connection.apply(.passphrase("aux-password"), name: "analytics")
|
||||
```
|
||||
|
||||
All databases attached to the same connection must follow a consistent encryption policy. If an
|
||||
attached database must remain unencrypted, attach it using a separate connection instead.
|
||||
|
||||
- SeeAlso: ``ConnectionProtocol/apply(_:name:)``
|
||||
- SeeAlso: ``ConnectionProtocol/rekey(_:name:)``
|
||||
- SeeAlso: [SQLCipher Documentation](https://www.zetetic.net/sqlcipher/documentation/)
|
||||
66
Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md
Normal file
66
Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Handling SQLite Errors
|
||||
|
||||
Handle SQLite errors predictably with DataLiteCore.
|
||||
|
||||
DataLiteCore converts all SQLite failures into an ``SQLiteError`` structure that contains both the
|
||||
extended result code and a descriptive message. This unified error model lets you accurately
|
||||
distinguish between constraint violations, locking issues, and other failure categories — while
|
||||
preserving full diagnostic information for recovery and logging.
|
||||
|
||||
## SQLiteError Breakdown
|
||||
|
||||
``SQLiteError`` exposes fields that help with diagnostics and recovery:
|
||||
|
||||
- ``SQLiteError/code`` — the extended SQLite result code (for example,
|
||||
`SQLITE_CONSTRAINT_FOREIGNKEY` or `SQLITE_BUSY_TIMEOUT`). Use it for programmatic
|
||||
branching — e.g., retry logic, rollbacks, or user-facing messages.
|
||||
- ``SQLiteError/message`` — a textual description of the underlying SQLite failure.
|
||||
|
||||
Since ``SQLiteError`` conforms to `CustomStringConvertible`, you can log it directly. For
|
||||
user-facing alerts, derive your own localized messages from the error code instead of exposing
|
||||
SQLite messages verbatim.
|
||||
|
||||
## Typed Throws
|
||||
|
||||
Most DataLiteCore APIs are annotated as `throws(SQLiteError)`, meaning they only throw SQLiteError
|
||||
instances.
|
||||
|
||||
Only APIs that touch the file system or execute arbitrary user code may throw other error
|
||||
types — for example, ``Connection/init(location:options:)`` when creating directories for on-disk
|
||||
databases.
|
||||
|
||||
```swift
|
||||
do {
|
||||
try connection.execute(raw: """
|
||||
INSERT INTO users(email) VALUES ('ada@example.com')
|
||||
""")
|
||||
} catch {
|
||||
switch error.code {
|
||||
case SQLITE_CONSTRAINT:
|
||||
showAlert("A user with this email already exists.")
|
||||
case SQLITE_BUSY, SQLITE_LOCKED:
|
||||
retryLater()
|
||||
default:
|
||||
print("Unexpected error: \(error.message)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Statement Scenarios
|
||||
|
||||
- ``ConnectionProtocol/execute(sql:)`` and ``ConnectionProtocol/execute(raw:)`` stop at the first
|
||||
failing statement and propagate its ``SQLiteError``.
|
||||
- ``StatementProtocol/execute(_:)`` reuses prepared statements; inside `catch` blocks, remember to
|
||||
call ``StatementProtocol/reset()`` and (if needed) ``StatementProtocol/clearBindings()`` before
|
||||
retrying.
|
||||
- When executing multiple statements, add your own logging if you need to know which one
|
||||
failed — the propagated ``SQLiteError`` reflects SQLite’s diagnostics only.
|
||||
|
||||
## Custom Functions
|
||||
|
||||
Errors thrown from ``Function/Scalar`` or ``Function/Aggregate`` implementations are reported back
|
||||
to SQLite as `SQLITE_ERROR`, with the error’s `localizedDescription` as the message text.
|
||||
Define clear, domain-specific error types to make SQL traces and logs more meaningful.
|
||||
|
||||
- SeeAlso: ``SQLiteError``
|
||||
- SeeAlso: [SQLite Result Codes](https://sqlite.org/rescode.html)
|
||||
94
Sources/DataLiteCore/Docs.docc/Articles/Multithreading.md
Normal file
94
Sources/DataLiteCore/Docs.docc/Articles/Multithreading.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Multithreading Strategies
|
||||
|
||||
Coordinate SQLite safely across queues, actors, and Swift concurrency using DataLiteCore.
|
||||
|
||||
SQLite remains fundamentally serialized, so deliberate connection ownership and scheduling are
|
||||
essential for correctness and performance. DataLiteCore does not include a built-in connection
|
||||
pool, but its deterministic behavior and configuration options allow you to design synchronization
|
||||
strategies that match your workload.
|
||||
|
||||
## Core Guidelines
|
||||
|
||||
- **One connection per queue or actor**. Keep each ``Connection`` confined to a dedicated serial
|
||||
`DispatchQueue` or an `actor` to ensure ordered execution and predictable statement lifecycles.
|
||||
- **Do not share statements across threads**. ``Statement`` instances are bound to their parent
|
||||
``Connection`` and are not thread-safe.
|
||||
- **Scale with multiple connections**. For concurrent workloads, use a dedicated writer connection
|
||||
alongside a pool of readers so long-running transactions don’t block unrelated operations.
|
||||
|
||||
```swift
|
||||
actor Database {
|
||||
private let connection: Connection
|
||||
|
||||
init(path: String) throws {
|
||||
connection = try Connection(
|
||||
path: path,
|
||||
options: [.readwrite, .create, .fullmutex]
|
||||
)
|
||||
connection.busyTimeout = 5_000 // wait up to 5 seconds for locks
|
||||
}
|
||||
|
||||
func insertUser(name: String) throws {
|
||||
let statement = try connection.prepare(
|
||||
sql: "INSERT INTO users(name) VALUES (?)"
|
||||
)
|
||||
try statement.bind(name, at: 1)
|
||||
try statement.step()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Encapsulating database work in an `actor` or serial queue aligns naturally with Swift Concurrency
|
||||
while maintaining safe access to SQLite’s synchronous API.
|
||||
|
||||
## Synchronization Options
|
||||
|
||||
- ``Connection/Options/nomutex`` — disables SQLite’s internal mutexes (multi-thread mode). Each
|
||||
connection must be accessed by only one thread at a time.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
location: .file(path: "/path/to/sqlite.db"),
|
||||
options: [.readwrite, .nomutex]
|
||||
)
|
||||
```
|
||||
|
||||
- ``Connection/Options/fullmutex`` — enables serialized mode with full internal locking. A single
|
||||
``Connection`` may be shared across threads, but global locks reduce throughput.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
location: .file(path: "/path/to/sqlite.db"),
|
||||
options: [.readwrite, .fullmutex]
|
||||
)
|
||||
```
|
||||
|
||||
SQLite defaults to serialized mode, but concurrent writers still contend for locks. Plan long
|
||||
transactions carefully and adjust ``ConnectionProtocol/busyTimeout`` to handle `SQLITE_BUSY`
|
||||
conditions gracefully.
|
||||
|
||||
- SeeAlso: [Using SQLite In Multi-Threaded Applications](https://sqlite.org/threadsafe.html)
|
||||
|
||||
## Delegates and Side Effects
|
||||
|
||||
``ConnectionDelegate`` and ``ConnectionTraceDelegate`` callbacks execute synchronously on SQLite’s
|
||||
internal thread. Keep them lightweight and non-blocking. Offload work to another queue when
|
||||
necessary to prevent deadlocks or extended lock holds.
|
||||
|
||||
```swift
|
||||
final class Logger: ConnectionTraceDelegate {
|
||||
private let queue = DispatchQueue(label: "logging")
|
||||
|
||||
func connection(
|
||||
_ connection: ConnectionProtocol,
|
||||
trace sql: ConnectionTraceDelegate.Trace
|
||||
) {
|
||||
queue.async {
|
||||
print("SQL:", sql.expandedSQL)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This pattern keeps tracing responsive and prevents SQLite’s internal thread from being blocked by
|
||||
slow I/O or external operations.
|
||||
192
Sources/DataLiteCore/Docs.docc/Articles/PreparedStatements.md
Normal file
192
Sources/DataLiteCore/Docs.docc/Articles/PreparedStatements.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Mastering Prepared Statements
|
||||
|
||||
Execute SQL efficiently and safely with reusable prepared statements in DataLiteCore.
|
||||
|
||||
Prepared statements allow you to compile SQL once, bind parameters efficiently, and execute it
|
||||
multiple times without re-parsing or re-planning. The ``Statement`` type in DataLiteCore is a thin,
|
||||
type-safe wrapper around SQLite’s C API that manages the entire lifecycle of a compiled SQL
|
||||
statement — from preparation and parameter binding to execution and result retrieval. Statements are
|
||||
automatically finalized when no longer referenced, ensuring predictable resource cleanup.
|
||||
|
||||
## Preparing Statements
|
||||
|
||||
Use ``ConnectionProtocol/prepare(sql:)`` or ``ConnectionProtocol/prepare(sql:options:)`` to create
|
||||
a compiled ``StatementProtocol`` instance ready for parameter binding and execution.
|
||||
|
||||
The optional ``Statement/Options`` control how the database engine optimizes the compilation and
|
||||
reuse of a prepared statement. For example, ``Statement/Options/persistent`` marks the statement as
|
||||
suitable for long-term reuse, allowing the engine to optimize memory allocation for statements
|
||||
expected to remain active across multiple executions. ``Statement/Options/noVtab`` restricts the use
|
||||
of virtual tables during preparation, preventing them from being referenced at compile time.
|
||||
|
||||
```swift
|
||||
let statement = try connection.prepare(
|
||||
sql: """
|
||||
SELECT id, email
|
||||
FROM users
|
||||
WHERE status = :status
|
||||
AND updated_at >= ?
|
||||
""",
|
||||
options: [.persistent]
|
||||
)
|
||||
```
|
||||
|
||||
Implementations of ``StatementProtocol`` are responsible for managing the lifetime of their
|
||||
underlying database resources. The default ``Statement`` type provided by DataLiteCore automatically
|
||||
finalizes statements when their last strong reference is released, ensuring deterministic cleanup
|
||||
through Swift’s memory management.
|
||||
|
||||
## Managing the Lifecycle
|
||||
|
||||
Use ``StatementProtocol/reset()`` to return a statement to its initial state so that it can be
|
||||
executed again. Call ``StatementProtocol/clearBindings()`` to remove all previously bound parameter
|
||||
values, allowing the same prepared statement to be reused with completely new input data. Always
|
||||
use these methods before reusing a prepared statement.
|
||||
|
||||
```swift
|
||||
try statement.reset()
|
||||
try statement.clearBindings()
|
||||
```
|
||||
|
||||
## Binding Parameters
|
||||
|
||||
You can bind either raw ``SQLiteValue`` values or any Swift type that conforms to ``SQLiteBindable``
|
||||
or ``SQLiteRepresentable``. Parameter placeholders in the SQL statement are assigned numeric indexes
|
||||
in the order they appear, starting from `1`.
|
||||
|
||||
To inspect or debug parameter mappings, use ``StatementProtocol/parameterCount()`` to check the
|
||||
total number of parameters, or ``StatementProtocol/parameterNameBy(_:)`` to retrieve the name of a
|
||||
specific placeholder by its index.
|
||||
|
||||
### Binding by Index
|
||||
|
||||
Use positional placeholders to bind parameters by numeric index. A simple `?` placeholder is
|
||||
automatically assigned the next available index in the order it appears, starting from `1`.
|
||||
A numbered placeholder (`?NNN`) explicitly defines its own index within the statement, letting you
|
||||
bind parameters out of order if needed.
|
||||
|
||||
```swift
|
||||
let insertLog = try connection.prepare(sql: """
|
||||
INSERT INTO logs(level, message, created_at)
|
||||
VALUES (?, ?, ?)
|
||||
""")
|
||||
|
||||
try insertLog.bind("info", at: 1)
|
||||
try insertLog.bind("Cache warmed", at: 2)
|
||||
try insertLog.bind(Date(), at: 3)
|
||||
try insertLog.step() // executes the INSERT
|
||||
try insertLog.reset()
|
||||
```
|
||||
|
||||
### Binding by Name
|
||||
|
||||
Named placeholders (`:name`, `@name`, `$name`) improve readability and allow the same parameter to
|
||||
appear multiple times within a statement. When binding, pass the full placeholder token — including
|
||||
its prefix — to the ``StatementProtocol/bind(_:by:)-(SQLiteValue,_)`` method.
|
||||
|
||||
```swift
|
||||
let usersByStatus = try connection.prepare(sql: """
|
||||
SELECT id, email
|
||||
FROM users
|
||||
WHERE status = :status
|
||||
AND email LIKE :pattern
|
||||
""")
|
||||
|
||||
try usersByStatus.bind("active", by: ":status")
|
||||
try usersByStatus.bind("%@example.com", by: ":pattern")
|
||||
```
|
||||
|
||||
If you need to inspect the numeric index associated with a named parameter, use
|
||||
``StatementProtocol/parameterIndexBy(_:)``. This can be useful for diagnostics, logging, or
|
||||
integrating with utility layers that operate by index.
|
||||
|
||||
### Reusing Parameters
|
||||
|
||||
When the same named placeholder appears multiple times in a statement, SQLite internally assigns all
|
||||
of them to a single binding slot. This means you only need to set the value once, and it will be
|
||||
applied everywhere that placeholder occurs.
|
||||
|
||||
```swift
|
||||
let sales = try connection.prepare(sql: """
|
||||
SELECT id
|
||||
FROM orders
|
||||
WHERE customer_id = :client
|
||||
OR created_by = :client
|
||||
LIMIT :limit
|
||||
""")
|
||||
|
||||
try sales.bind(42, by: ":client") // used for both conditions
|
||||
try sales.bind(50, by: ":limit")
|
||||
```
|
||||
|
||||
### Mixing Placeholders
|
||||
|
||||
You can freely combine named and positional placeholders within the same statement. SQLite assigns
|
||||
numeric indexes to all placeholders in the order they appear, regardless of whether they are named
|
||||
or positional. To keep bindings predictable, it’s best to follow a consistent style within each
|
||||
statement.
|
||||
|
||||
```swift
|
||||
let search = try connection.prepare(sql: """
|
||||
SELECT id, title
|
||||
FROM articles
|
||||
WHERE category_id IN (?, ?, ?)
|
||||
AND published_at >= :since
|
||||
""")
|
||||
|
||||
try search.bind(3, at: 1)
|
||||
try search.bind(5, at: 2)
|
||||
try search.bind(8, at: 3)
|
||||
try search.bind(Date(timeIntervalSinceNow: -7 * 24 * 60 * 60), by: ":since")
|
||||
```
|
||||
|
||||
## Executing Statements
|
||||
|
||||
Advance execution with ``StatementProtocol/step()``. This method returns `true` while rows are
|
||||
available, and `false` when the statement is fully consumed — or immediately, for statements that
|
||||
do not produce results.
|
||||
|
||||
Always reset a statement before re-executing it; otherwise, the database engine will report a misuse
|
||||
error.
|
||||
|
||||
```swift
|
||||
var rows: [SQLiteRow] = []
|
||||
while try usersByStatus.step() {
|
||||
if let row = usersByStatus.currentRow() {
|
||||
rows.append(row)
|
||||
}
|
||||
}
|
||||
|
||||
try usersByStatus.reset()
|
||||
try usersByStatus.clearBindings()
|
||||
```
|
||||
|
||||
For bulk operations, use ``StatementProtocol/execute(_:)``. It accepts an array of ``SQLiteRow``
|
||||
values and automatically performs binding, stepping, clearing, and resetting in a loop — making it
|
||||
convenient for batch inserts or updates.
|
||||
|
||||
## Fetching Result Data
|
||||
|
||||
Use ``StatementProtocol/columnCount()`` and ``StatementProtocol/columnName(at:)`` to inspect the
|
||||
structure of the result set. Retrieve individual column values with
|
||||
``StatementProtocol/columnValue(at:)->SQLiteValue`` — either as a raw ``SQLiteValue`` or as a typed
|
||||
value conforming to ``SQLiteRepresentable``. Alternatively, use ``StatementProtocol/currentRow()``
|
||||
to obtain the full set of column values for the current result row.
|
||||
|
||||
```swift
|
||||
while try statement.step() {
|
||||
guard let identifier: Int64 = statement.columnValue(at: 0),
|
||||
let email: String = statement.columnValue(at: 1)
|
||||
else { continue }
|
||||
print("User \(identifier): \(email)")
|
||||
}
|
||||
try statement.reset()
|
||||
```
|
||||
|
||||
Each row returned by `currentRow()` is an independent copy of the current result data. You can
|
||||
safely store it, transform it into a domain model, or reuse its values as parameters in subsequent
|
||||
statements through ``StatementProtocol/bind(_:)``.
|
||||
|
||||
- SeeAlso: ``StatementProtocol``
|
||||
- SeeAlso: ``Statement``
|
||||
- SeeAlso: [SQLite Prepared Statements](https://sqlite.org/c3ref/stmt.html)
|
||||
98
Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md
Normal file
98
Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Running SQL Scripts
|
||||
|
||||
Execute and automate SQL migrations, seed data, and test fixtures with DataLiteCore.
|
||||
|
||||
``SQLScript`` and ``ConnectionProtocol/execute(sql:)`` let you run sequences of prepared statements
|
||||
as a single script. The loader splits content by semicolons, removes comments and whitespace, and
|
||||
compiles each statement individually. Scripts run in autocommit mode by default — execution stops at
|
||||
the first failure and throws the corresponding ``SQLiteError``.
|
||||
|
||||
## Building Scripts
|
||||
|
||||
Create a script inline or load it from a bundled resource. ``SQLScript`` automatically strips
|
||||
comments and normalizes whitespace.
|
||||
|
||||
```swift
|
||||
let script = SQLScript(string: """
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE
|
||||
);
|
||||
INSERT INTO users (username) VALUES ('ada'), ('grace');
|
||||
""")
|
||||
```
|
||||
|
||||
Load a script from your module or app bundle using ``SQLScript/init(byResource:extension:in:)``:
|
||||
|
||||
```swift
|
||||
let bootstrap = try? SQLScript(
|
||||
byResource: "bootstrap",
|
||||
extension: "sql",
|
||||
in: .module
|
||||
)
|
||||
```
|
||||
|
||||
## Executing Scripts
|
||||
|
||||
Run the script through ``ConnectionProtocol/execute(sql:)``. Statements execute sequentially in the
|
||||
order they appear.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
location: .file(path: dbPath),
|
||||
options: [.readwrite, .create]
|
||||
)
|
||||
try connection.execute(sql: script)
|
||||
```
|
||||
|
||||
In autocommit mode, each statement commits as soon as it succeeds. If any statement fails, execution
|
||||
stops and previously executed statements remain committed. To ensure all-or-nothing execution, wrap
|
||||
the script in an explicit transaction:
|
||||
|
||||
```swift
|
||||
try connection.beginTransaction(.immediate)
|
||||
do {
|
||||
try connection.execute(sql: script)
|
||||
try connection.commitTransaction()
|
||||
} catch {
|
||||
try? connection.rollbackTransaction()
|
||||
throw error
|
||||
}
|
||||
```
|
||||
|
||||
- Important: SQLScript must not include BEGIN, COMMIT, or ROLLBACK. Always manage transactions at
|
||||
the connection level.
|
||||
|
||||
## Executing Raw SQL
|
||||
|
||||
Use ``ConnectionProtocol/execute(raw:)`` to run multi-statement SQL directly, without parsing or
|
||||
preprocessing. This method executes the script exactly as provided, allowing you to manage
|
||||
transactions explicitly within the SQL itself.
|
||||
|
||||
```swift
|
||||
let migrations = """
|
||||
BEGIN;
|
||||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
INSERT INTO categories (name) VALUES ('Swift'), ('SQLite');
|
||||
COMMIT;
|
||||
"""
|
||||
|
||||
try connection.execute(raw: migrations)
|
||||
```
|
||||
|
||||
Each statement runs in sequence until completion or until the first error occurs. If a statement
|
||||
fails, execution stops and remaining statements are skipped. Open transactions are not rolled back
|
||||
automatically — they must be handled explicitly inside the script or by the caller.
|
||||
|
||||
## Handling Errors
|
||||
|
||||
Inspect the thrown ``SQLiteError`` to identify the failing statement’s result code and message. For
|
||||
longer scripts, wrap execution in logging to trace progress and isolate the exact statement that
|
||||
triggered the exception.
|
||||
|
||||
- SeeAlso: ``SQLScript``
|
||||
- SeeAlso: ``ConnectionProtocol/execute(sql:)``
|
||||
- SeeAlso: ``ConnectionProtocol/execute(raw:)``
|
||||
142
Sources/DataLiteCore/Docs.docc/Articles/SQLiteRows.md
Normal file
142
Sources/DataLiteCore/Docs.docc/Articles/SQLiteRows.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Working with SQLiteRow
|
||||
|
||||
Represent SQL rows and parameters with SQLiteRow.
|
||||
|
||||
``SQLiteRow`` is an ordered container for column/value pairs. It preserves insertion order—matching
|
||||
the schema when representing result sets—and provides helpers for column names, named parameters,
|
||||
and literal rendering.
|
||||
|
||||
## Creating Rows
|
||||
|
||||
Initialize a row with a dictionary literal or assign values incrementally through subscripting.
|
||||
Values can be ``SQLiteValue`` instances or any type convertible via ``SQLiteRepresentable``.
|
||||
|
||||
```swift
|
||||
var payload: SQLiteRow = [
|
||||
"username": .text("ada"),
|
||||
"email": "ada@example.com".sqliteValue,
|
||||
"is_admin": false.sqliteValue
|
||||
]
|
||||
|
||||
payload["last_login_at"] = Int64(Date().timeIntervalSince1970).sqliteValue
|
||||
```
|
||||
|
||||
``SQLiteRow/columns`` returns the ordered column names, and ``SQLiteRow/namedParameters`` provides
|
||||
matching tokens (prefixed with `:`) suitable for parameterized SQL.
|
||||
|
||||
```swift
|
||||
print(payload.columns) // ["username", "email", "is_admin", "last_login_at"]
|
||||
print(payload.namedParameters) // [":username", ":email", ":is_admin", ":last_login_at"]
|
||||
```
|
||||
|
||||
## Generating SQL Fragments
|
||||
|
||||
Use row metadata to build SQL snippets without manual string concatenation:
|
||||
|
||||
```swift
|
||||
let columns = payload.columns.joined(separator: ", ")
|
||||
let placeholders = payload.namedParameters.joined(separator: ", ")
|
||||
let assignments = zip(payload.columns, payload.namedParameters)
|
||||
.map { "\($0) = \($1)" }
|
||||
.joined(separator: ", ")
|
||||
|
||||
// columns -> "username, email, is_admin, last_login_at"
|
||||
// placeholders -> ":username, :email, :is_admin, :last_login_at"
|
||||
// assignments -> "username = :username, ..."
|
||||
```
|
||||
|
||||
When generating migrations or inserting literal values, ``SQLiteValue/sqliteLiteral`` renders safe
|
||||
SQL fragments for numeric and text values. Always escape identifiers manually if column names come
|
||||
from untrusted input.
|
||||
|
||||
## Inserting Rows
|
||||
|
||||
Bind an entire row to a statement using ``StatementProtocol/bind(_:)``. The method matches column
|
||||
names to identically named placeholders.
|
||||
|
||||
```swift
|
||||
var user: SQLiteRow = [
|
||||
"username": .text("ada"),
|
||||
"email": .text("ada@example.com"),
|
||||
"created_at": Int64(Date().timeIntervalSince1970).sqliteValue
|
||||
]
|
||||
|
||||
let insertSQL = """
|
||||
INSERT INTO users (\(user.columns.joined(separator: ", ")))
|
||||
VALUES (\(user.namedParameters.joined(separator: ", ")))
|
||||
"""
|
||||
|
||||
let insert = try connection.prepare(sql: insertSQL)
|
||||
try insert.bind(user)
|
||||
try insert.step()
|
||||
try insert.reset()
|
||||
```
|
||||
|
||||
To insert multiple rows, prepare an array of ``SQLiteRow`` values and call
|
||||
``StatementProtocol/execute(_:)``. The helper performs binding, stepping, and clearing for each row:
|
||||
|
||||
```swift
|
||||
let batch: [SQLiteRow] = [
|
||||
["username": .text("ada"), "email": .text("ada@example.com")],
|
||||
["username": .text("grace"), "email": .text("grace@example.com")]
|
||||
]
|
||||
|
||||
try insert.execute(batch)
|
||||
```
|
||||
|
||||
## Updating Rows
|
||||
|
||||
Because ``SQLiteRow`` is a value type, you can duplicate and extend it for related operations such
|
||||
as building `SET` clauses or constructing `WHERE` conditions.
|
||||
|
||||
```swift
|
||||
var changes: SQLiteRow = [
|
||||
"email": .text("ada@new.example"),
|
||||
"last_login_at": Int64(Date().timeIntervalSince1970).sqliteValue
|
||||
]
|
||||
|
||||
let setClause = zip(changes.columns, changes.namedParameters)
|
||||
.map { "\($0) = \($1)" }
|
||||
.joined(separator: ", ")
|
||||
|
||||
var parameters = changes
|
||||
parameters["id"] = .int(1)
|
||||
|
||||
let update = try connection.prepare(sql: """
|
||||
UPDATE users
|
||||
SET \(setClause)
|
||||
WHERE id = :id
|
||||
""")
|
||||
|
||||
try update.bind(parameters)
|
||||
try update.step()
|
||||
```
|
||||
|
||||
## Reading Rows
|
||||
|
||||
``StatementProtocol/currentRow()`` returns an ``SQLiteRow`` snapshot of the current result. Use it
|
||||
to pass data through mapping layers or transform results lazily without immediate conversion:
|
||||
|
||||
```swift
|
||||
let statement = try connection.prepare(sql: "SELECT id, email FROM users LIMIT 10")
|
||||
|
||||
var rows: [SQLiteRow] = []
|
||||
while try statement.step() {
|
||||
if let row = statement.currentRow() {
|
||||
rows.append(row)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can iterate over a row’s columns via `columns`, and subscript by name to retrieve stored values.
|
||||
For typed access, cast through ``SQLiteValue`` or adopt ``SQLiteRepresentable`` in your custom
|
||||
types.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Use ``SQLiteRow/description`` to log payloads during development. For security-sensitive logs,
|
||||
redact or whitelist keys before printing. Because rows preserve order, logs mirror the schema
|
||||
defined in your SQL, making comparisons straightforward.
|
||||
|
||||
- SeeAlso: ``SQLiteRow``
|
||||
- SeeAlso: ``StatementProtocol``
|
||||
@@ -0,0 +1,188 @@
|
||||
# Working with Connections
|
||||
|
||||
Open, configure, monitor, and transact with SQLite connections using DataLiteCore.
|
||||
|
||||
Establishing and configuring a ``Connection`` is the first step before executing SQL statements
|
||||
with **DataLiteCore**. A connection wraps the underlying SQLite handle, exposes ergonomic Swift
|
||||
APIs, and provides hooks for observing database activity.
|
||||
|
||||
## Opening a Connection
|
||||
|
||||
Create a connection with ``Connection/init(location:options:)``. The initializer opens (or creates)
|
||||
the target database file and registers lifecycle hooks that enable tracing, update notifications,
|
||||
and transaction callbacks.
|
||||
|
||||
Call ``ConnectionProtocol/initialize()`` once during application start-up when you need to ensure
|
||||
the SQLite core has been initialized manually—for example, when linking SQLite dynamically or when
|
||||
the surrounding framework does not do it on your behalf. Pair it with
|
||||
``ConnectionProtocol/shutdown()`` during application tear-down if you require full control of
|
||||
SQLite's global state.
|
||||
|
||||
```swift
|
||||
import DataLiteCore
|
||||
|
||||
do {
|
||||
try Connection.initialize()
|
||||
let connection = try Connection(
|
||||
location: .file(path: "/path/to/sqlite.db"),
|
||||
options: [.readwrite, .create]
|
||||
)
|
||||
// Execute SQL or configure PRAGMAs
|
||||
} catch {
|
||||
print("Failed to open database: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
### Choosing a Location
|
||||
|
||||
Pick the database storage strategy from the ``Connection/Location`` enumeration:
|
||||
|
||||
- ``Connection/Location/file(path:)`` — a persistent on-disk file or URI backed database.
|
||||
- ``Connection/Location/inMemory`` — a pure in-memory database that disappears once the connection
|
||||
closes.
|
||||
- ``Connection/Location/temporary`` — a transient on-disk file that SQLite removes when the session
|
||||
ends.
|
||||
|
||||
### Selecting Options
|
||||
|
||||
Control how the connection is opened with ``Connection/Options``. Combine flags to describe the
|
||||
required access mode, locking policy, and URI behavior.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.readwrite, .nomutex]
|
||||
)
|
||||
```
|
||||
|
||||
Common combinations include:
|
||||
|
||||
- ``Connection/Options/readwrite`` + ``Connection/Options/create`` — read/write access that creates
|
||||
the file if missing.
|
||||
- ``Connection/Options/readonly`` — read-only access preventing accidental writes.
|
||||
- ``Connection/Options/fullmutex`` — enables serialized mode for multi-threaded access.
|
||||
- ``Connection/Options/uri`` — allows SQLite URI parameters, such as query string pragmas.
|
||||
|
||||
- SeeAlso: [Opening A New Database Connection](https://sqlite.org/c3ref/open.html)
|
||||
- SeeAlso: [In-Memory Databases](https://sqlite.org/inmemorydb.html)
|
||||
|
||||
## Closing a Connection
|
||||
|
||||
``Connection`` automatically closes the underlying SQLite handle when the instance is deallocated.
|
||||
This ensures resources are released even when the object leaves scope unexpectedly. For long-lived
|
||||
applications, prefer explicit lifecycle management—store the connection in a dedicated component
|
||||
and release it deterministically when you are done to avoid keeping file locks or WAL checkpoints
|
||||
around unnecessarily.
|
||||
|
||||
If your application called ``ConnectionProtocol/shutdown()`` to clean up global state, make sure all
|
||||
connections have been released before invoking it.
|
||||
|
||||
## Managing Transactions
|
||||
|
||||
Manage transactional work with ``ConnectionProtocol/beginTransaction(_:)``,
|
||||
``ConnectionProtocol/commitTransaction()``, and ``ConnectionProtocol/rollbackTransaction()``. When
|
||||
you do not start a transaction explicitly, SQLite runs in autocommit mode and executes each
|
||||
statement in its own transaction.
|
||||
|
||||
```swift
|
||||
do {
|
||||
try connection.beginTransaction(.immediate)
|
||||
try connection.execute(raw: "INSERT INTO users (name) VALUES ('Ada')")
|
||||
try connection.execute(raw: "INSERT INTO users (name) VALUES ('Grace')")
|
||||
try connection.commitTransaction()
|
||||
} catch {
|
||||
try? connection.rollbackTransaction()
|
||||
throw error
|
||||
}
|
||||
```
|
||||
|
||||
``TransactionType`` controls when SQLite acquires locks:
|
||||
|
||||
- ``TransactionType/deferred`` — defers locking until the first read or write; this is the default.
|
||||
- ``TransactionType/immediate`` — immediately takes a RESERVED lock to prevent other writers.
|
||||
- ``TransactionType/exclusive`` — escalates to an EXCLUSIVE lock and, in `DELETE` journal mode,
|
||||
blocks readers.
|
||||
|
||||
``ConnectionProtocol/beginTransaction(_:)`` uses `.deferred` by default. When
|
||||
``ConnectionProtocol/isAutocommit`` returns `false`, a transaction is already active. Calling
|
||||
`beginTransaction` again raises an error, so guard composite operations accordingly.
|
||||
|
||||
- SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html)
|
||||
|
||||
## PRAGMA Parameters
|
||||
|
||||
Most frequently used PRAGMA directives are modeled as direct properties on ``ConnectionProtocol``:
|
||||
``ConnectionProtocol/busyTimeout``, ``ConnectionProtocol/applicationID``,
|
||||
``ConnectionProtocol/foreignKeys``, ``ConnectionProtocol/journalMode``,
|
||||
``ConnectionProtocol/synchronous``, and ``ConnectionProtocol/userVersion``. Update them directly on
|
||||
an active connection:
|
||||
|
||||
```swift
|
||||
connection.userVersion = 2024
|
||||
connection.foreignKeys = true
|
||||
connection.journalMode = .wal
|
||||
```
|
||||
|
||||
### Custom PRAGMAs
|
||||
|
||||
Use ``ConnectionProtocol/get(pragma:)`` and ``ConnectionProtocol/set(pragma:value:)`` for PRAGMAs
|
||||
that do not have a dedicated API. They accept ``Pragma`` values (string literal expressible) and
|
||||
any type that conforms to ``SQLiteRepresentable``. `set` composes a `PRAGMA <name> = <value>`
|
||||
statement, while `get` issues `PRAGMA <name>`.
|
||||
|
||||
```swift
|
||||
// Read the current cache_size value
|
||||
let cacheSize: Int32? = try connection.get(pragma: "cache_size")
|
||||
|
||||
// Enable WAL journaling and adjust the sync mode
|
||||
try connection.set(pragma: .journalMode, value: JournalMode.wal)
|
||||
try connection.set(pragma: .synchronous, value: Synchronous.normal)
|
||||
```
|
||||
|
||||
The `value` parameter automatically converts to ``SQLiteValue`` through ``SQLiteRepresentable``,
|
||||
so you can pass `Bool`, `Int`, `String`, `Synchronous`, `JournalMode`, or a custom type that
|
||||
supports the protocol.
|
||||
|
||||
- SeeAlso: [PRAGMA Statements](https://sqlite.org/pragma.html)
|
||||
|
||||
## Observing Connection Events
|
||||
|
||||
``ConnectionDelegate`` lets you observe connection-level events such as row updates, commits, and
|
||||
rollbacks. Register a delegate with ``ConnectionProtocol/add(delegate:)``. Delegates are stored
|
||||
weakly, so you are responsible for managing their lifetime. Remove a delegate with
|
||||
``ConnectionProtocol/remove(delegate:)`` when it is no longer required.
|
||||
|
||||
Use ``ConnectionTraceDelegate`` to receive SQL statement traces and register it with
|
||||
``ConnectionProtocol/add(trace:)``. Trace delegates are also held weakly.
|
||||
|
||||
```swift
|
||||
final class QueryLogger: ConnectionDelegate, ConnectionTraceDelegate {
|
||||
func connection(_ connection: ConnectionProtocol, trace sql: ConnectionTraceDelegate.Trace) {
|
||||
print("SQL:", sql.expandedSQL)
|
||||
}
|
||||
|
||||
func connection(_ connection: ConnectionProtocol, didUpdate action: SQLiteAction) {
|
||||
print("Change:", action)
|
||||
}
|
||||
|
||||
func connectionWillCommit(_ connection: ConnectionProtocol) throws {
|
||||
try validatePendingOperations()
|
||||
}
|
||||
|
||||
func connectionDidRollback(_ connection: ConnectionProtocol) {
|
||||
resetInMemoryCache()
|
||||
}
|
||||
}
|
||||
|
||||
let logger = QueryLogger()
|
||||
connection.add(delegate: logger)
|
||||
connection.add(trace: logger)
|
||||
|
||||
// ...
|
||||
|
||||
connection.remove(trace: logger)
|
||||
connection.remove(delegate: logger)
|
||||
```
|
||||
|
||||
All callbacks execute synchronously on SQLite's internal thread. Keep delegate logic lightweight,
|
||||
avoid blocking I/O, and hand heavy work off to other queues when necessary to preserve responsiveness.
|
||||
@@ -1,30 +0,0 @@
|
||||
# ``DataLiteCore/Connection/init(location:options:)``
|
||||
|
||||
Initializes a new connection to an SQLite database.
|
||||
|
||||
This initializer opens a connection to the SQLite database at the specified `location`
|
||||
with the provided `options`. If the location is a file path, it ensures the necessary
|
||||
directory exists, creating intermediate directories if needed.
|
||||
|
||||
```swift
|
||||
do {
|
||||
let connection = try Connection(
|
||||
location: .file(path: "~/example.db"),
|
||||
options: .readwrite
|
||||
)
|
||||
// Use the connection to execute queries
|
||||
} catch {
|
||||
print("Error establishing connection: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
- Parameters:
|
||||
- location: Specifies where the database is located.
|
||||
Can be a file path, an in-memory database, or a temporary database.
|
||||
- options: Configures connection behavior,
|
||||
such as read-only or read-write access and cache mode.
|
||||
|
||||
- Throws: ``Connection/Error`` if the connection fails to open due to SQLite errors,
|
||||
invalid path, permission issues, or other underlying failures.
|
||||
|
||||
- Throws: An error if directory creation fails for file-based database locations.
|
||||
@@ -1,31 +0,0 @@
|
||||
# ``DataLiteCore/Connection/init(path:options:)``
|
||||
|
||||
Initializes a new connection to an SQLite database using a file path.
|
||||
|
||||
This convenience initializer sets up a connection to the SQLite database located at the
|
||||
specified `path` with the provided `options`. It internally calls the main initializer
|
||||
to manage the connection setup.
|
||||
|
||||
### Usage Example
|
||||
|
||||
```swift
|
||||
do {
|
||||
let connection = try Connection(
|
||||
path: "~/example.db",
|
||||
options: .readwrite
|
||||
)
|
||||
// Use the connection to execute queries
|
||||
} catch {
|
||||
print("Error establishing connection: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
- Parameters:
|
||||
- path: A string representing the file path to the SQLite database.
|
||||
- options: Configures the connection behavior,
|
||||
such as read-only or read-write access and cache mode.
|
||||
|
||||
- Throws: ``Connection/Error`` if the connection fails to open due to SQLite errors,
|
||||
invalid path, permission issues, or other underlying failures.
|
||||
|
||||
- Throws: An error if subdirectories for the database file cannot be created.
|
||||
@@ -1,298 +0,0 @@
|
||||
# ``DataLiteCore/Connection``
|
||||
|
||||
A class representing a connection to an SQLite database.
|
||||
|
||||
## Overview
|
||||
|
||||
The `Connection` class manages the connection to an SQLite database. It provides an interface
|
||||
for preparing SQL queries, managing transactions, and handling errors. This class serves as the
|
||||
main object for interacting with the database.
|
||||
|
||||
## Opening a New Connection
|
||||
|
||||
Use the ``init(location:options:)`` initializer to open a database connection. Specify the
|
||||
database's location using the ``Location`` parameter and configure connection settings with the
|
||||
``Options`` parameter.
|
||||
|
||||
```swift
|
||||
do {
|
||||
let connection = try Connection(
|
||||
location: .file(path: "~/example.db"),
|
||||
options: [.readwrite, .create]
|
||||
)
|
||||
print("Connection established")
|
||||
} catch {
|
||||
print("Failed to connect: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## Closing the Connection
|
||||
|
||||
The `Connection` class automatically closes the database connection when the object is
|
||||
deallocated (`deinit`). This ensures proper cleanup even if the object goes out of scope.
|
||||
|
||||
## Delegate
|
||||
|
||||
The `Connection` class can optionally use a delegate to handle specific events during the
|
||||
connection lifecycle, such as tracing SQL statements or responding to transaction actions.
|
||||
The delegate must conform to the ``ConnectionDelegate`` protocol, which provides methods for
|
||||
handling these events.
|
||||
|
||||
## Custom SQL Functions
|
||||
|
||||
The `Connection` class allows you to add custom SQL functions using subclasses of ``Function``.
|
||||
You can create either **scalar** functions (which return a single value) or **aggregate**
|
||||
functions (which perform operations across multiple rows). Both types can be used directly in
|
||||
SQL queries.
|
||||
|
||||
To add or remove custom functions, use the ``add(function:)`` and ``remove(function:)`` methods
|
||||
of the `Connection` class.
|
||||
|
||||
## Preparing SQL Statements
|
||||
|
||||
The `Connection` class provides functionality for preparing SQL statements that can be
|
||||
executed multiple times with different parameter values. The ``prepare(sql:options:)`` method
|
||||
takes a SQL query as a string and an optional ``Statement/Options`` parameter to configure
|
||||
the behavior of the statement. It returns a ``Statement`` object that can be executed.
|
||||
|
||||
```swift
|
||||
do {
|
||||
let statement = try connection.prepare(
|
||||
sql: "SELECT * FROM users WHERE age > ?",
|
||||
options: [.persistent]
|
||||
)
|
||||
// Bind parameters and execute the statement
|
||||
} catch {
|
||||
print("Error preparing statement: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## Executing SQL Scripts
|
||||
|
||||
The `Connection` class allows you to execute a series of SQL statements using the ``SQLScript``
|
||||
structure. The ``SQLScript`` structure is designed to load and process multiple SQL queries
|
||||
from a file, URL, or string.
|
||||
|
||||
You can create an instance of ``SQLScript`` with the SQL script content and then pass it to the
|
||||
``execute(sql:)`` method of the `Connection` class to execute the script.
|
||||
|
||||
```swift
|
||||
let script: SQLScript = """
|
||||
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
|
||||
INSERT INTO users (name) VALUES ('Alice');
|
||||
INSERT INTO users (name) VALUES ('Bob');
|
||||
"""
|
||||
|
||||
do {
|
||||
try connection.execute(sql: script)
|
||||
print("Script executed successfully")
|
||||
} catch {
|
||||
print("Error executing script: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## Transaction Handling
|
||||
|
||||
By default, the `Connection` class operates in **autocommit mode**, where each SQL statement is
|
||||
automatically committed after execution. In this mode, each statement is treated as a separate
|
||||
transaction, eliminating the need for explicit transaction management. To determine whether the
|
||||
connection is in autocommit mode, use the ``isAutocommit`` property.
|
||||
|
||||
For manual transaction management, use ``beginTransaction(_:)`` to start a transaction, and
|
||||
``commitTransaction()`` or ``rollbackTransaction()`` to either commit or roll back the
|
||||
transaction.
|
||||
|
||||
```swift
|
||||
do {
|
||||
try connection.beginTransaction()
|
||||
try connection.execute(sql: "INSERT INTO users (name) VALUES ('Alice')")
|
||||
try connection.execute(sql: "INSERT INTO users (name) VALUES ('Bob')")
|
||||
try connection.commitTransaction()
|
||||
print("Transaction committed successfully")
|
||||
} catch {
|
||||
try? connection.rollbackTransaction()
|
||||
print("Error during transaction: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
Learn more in the [SQLite Transaction Documentation](https://www.sqlite.org/lang_transaction.html).
|
||||
|
||||
## Error Handling
|
||||
|
||||
The `Connection` class uses Swift's throwing mechanism to handle errors. Errors in database
|
||||
operations are propagated using `throws`, allowing you to catch and handle specific issues in
|
||||
your application.
|
||||
|
||||
SQLite-related errors, such as invalid SQL queries, connection failures, or issues with
|
||||
transaction management, throw an ``Connection/Error`` struct. These errors conform to the
|
||||
`Error` protocol, and you can handle them using Swift's `do-catch` syntax to manage exceptions
|
||||
in your code.
|
||||
|
||||
```swift
|
||||
do {
|
||||
let statement = try connection.prepare(
|
||||
sql: "SELECT * FROM users WHERE age > ?",
|
||||
options: []
|
||||
)
|
||||
} catch let error as Error {
|
||||
print("SQLite error: \(error.mesg), Code: \(error.code)")
|
||||
} catch {
|
||||
print("Unexpected error: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## Multithreading
|
||||
|
||||
The `Connection` class supports multithreading, but its behavior depends on the selected
|
||||
thread-safety mode. You can configure the desired mode using the ``Options`` parameter in the
|
||||
``init(location:options:)`` method.
|
||||
|
||||
**Multi-thread** (``Options/nomutex``): This mode allows SQLite to be used across multiple
|
||||
threads. However, it requires that no `Connection` instance or its derived objects (e.g.,
|
||||
prepared statements) are accessed simultaneously by multiple threads.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
location: .file(path: "~/example.db"),
|
||||
options: [.readwrite, .nomutex]
|
||||
)
|
||||
```
|
||||
|
||||
**Serialized** (``Options/fullmutex``): In this mode, SQLite uses internal mutexes to ensure
|
||||
thread safety. This allows multiple threads to safely share `Connection` instances and their
|
||||
derived objects.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
location: .file(path: "~/example.db"),
|
||||
options: [.readwrite, .fullmutex]
|
||||
)
|
||||
```
|
||||
|
||||
- Important: The `Connection` class does not include built-in synchronization for shared
|
||||
resources. Developers must implement custom synchronization mechanisms, such as using
|
||||
`DispatchQueue`, when sharing resources across threads.
|
||||
|
||||
For more details, see the [Using SQLite in Multi-Threaded Applications](https://www.sqlite.org/threadsafe.html).
|
||||
|
||||
## Encryption
|
||||
|
||||
The `Connection` class supports transparent encryption and re-encryption of databases using the
|
||||
``apply(_:name:)`` and ``rekey(_:name:)`` methods. This allows sensitive data to be securely
|
||||
stored on disk.
|
||||
|
||||
### Applying an Encryption Key
|
||||
|
||||
To open an encrypted database or encrypt a new one, call ``apply(_:name:)`` immediately after
|
||||
initializing the connection, and before executing any SQL statements.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
path: "~/secure.db",
|
||||
options: [.readwrite, .create]
|
||||
)
|
||||
try connection.apply(Key.passphrase("secret-password"))
|
||||
```
|
||||
|
||||
- If the database is already encrypted, the key must match the one previously used.
|
||||
- If the database is unencrypted, applying a key will encrypt it on first write.
|
||||
|
||||
You can use either a **passphrase**, which is internally transformed into a key,
|
||||
or a **raw key**:
|
||||
|
||||
```swift
|
||||
try connection.apply(Key.raw(data: rawKeyData))
|
||||
```
|
||||
|
||||
- Important: The encryption key must be applied *before* any SQL queries are executed.
|
||||
Otherwise, the database may remain unencrypted or unreadable.
|
||||
|
||||
### Rekeying the Database
|
||||
|
||||
To change the encryption key of an existing database, you must first apply the current key
|
||||
using ``apply(_:name:)``, then call ``rekey(_:name:)`` with the new key.
|
||||
|
||||
```swift
|
||||
let connection = try Connection(
|
||||
path: "~/secure.db",
|
||||
options: [.readwrite]
|
||||
)
|
||||
try connection.apply(Key.passphrase("old-password"))
|
||||
try connection.rekey(Key.passphrase("new-password"))
|
||||
```
|
||||
|
||||
- Important: ``rekey(_:name:)`` requires that the correct current key has already been applied
|
||||
via ``apply(_:name:)``. If the wrong key is used, the operation will fail with an error.
|
||||
|
||||
### Attached Databases
|
||||
|
||||
Both ``apply(_:name:)`` and ``rekey(_:name:)`` accept an optional `name` parameter to operate
|
||||
on an attached database. If omitted, they apply to the main database.
|
||||
|
||||
## Topics
|
||||
|
||||
### Errors
|
||||
|
||||
- ``Error``
|
||||
|
||||
### Initializers
|
||||
|
||||
- ``Location``
|
||||
- ``Options``
|
||||
- ``init(location:options:)``
|
||||
- ``init(path:options:)``
|
||||
|
||||
### Connection State
|
||||
|
||||
- ``isAutocommit``
|
||||
- ``isReadonly``
|
||||
- ``busyTimeout``
|
||||
|
||||
### PRAGMA Accessors
|
||||
|
||||
- ``applicationID``
|
||||
- ``foreignKeys``
|
||||
- ``journalMode``
|
||||
- ``synchronous``
|
||||
- ``userVersion``
|
||||
|
||||
### Delegation
|
||||
|
||||
- ``addDelegate(_:)``
|
||||
- ``removeDelegate(_:)``
|
||||
|
||||
### SQLite Lifecycle
|
||||
|
||||
- ``initialize()``
|
||||
- ``shutdown()``
|
||||
|
||||
### Custom SQL Functions
|
||||
|
||||
- ``add(function:)``
|
||||
- ``remove(function:)``
|
||||
|
||||
### Statement Preparation
|
||||
|
||||
- ``prepare(sql:options:)``
|
||||
|
||||
### Script Execution
|
||||
|
||||
- ``execute(sql:)``
|
||||
- ``execute(raw:)``
|
||||
|
||||
### PRAGMA Execution
|
||||
|
||||
- ``get(pragma:)``
|
||||
- ``set(pragma:value:)``
|
||||
|
||||
### Transactions
|
||||
|
||||
- ``beginTransaction(_:)``
|
||||
- ``commitTransaction()``
|
||||
- ``rollbackTransaction()``
|
||||
|
||||
### Encryption Keys
|
||||
|
||||
- ``Connection/Key``
|
||||
- ``apply(_:name:)``
|
||||
- ``rekey(_:name:)``
|
||||
@@ -2,6 +2,19 @@
|
||||
|
||||
**DataLiteCore** is an intuitive library for working with SQLite in Swift applications.
|
||||
|
||||
## Overview
|
||||
**DataLiteCore** provides an object-oriented API on top of the C interface, making it simple to
|
||||
integrate SQLite capabilities into your projects. The library combines powerful database
|
||||
management and SQL execution features with the ergonomics and flexibility of native Swift code.
|
||||
|
||||
**DataLiteCore** provides an object-oriented API over the C interface, allowing developers to easily integrate SQLite functionality into their projects. The library offers powerful capabilities for database management and executing SQL queries while maintaining the simplicity and flexibility of the native Swift interface.
|
||||
## Topics
|
||||
|
||||
### Articles
|
||||
|
||||
- <doc:WorkingWithConnections>
|
||||
- <doc:CustomFunctions>
|
||||
- <doc:PreparedStatements>
|
||||
- <doc:SQLiteRows>
|
||||
- <doc:SQLScripts>
|
||||
- <doc:ErrorHandling>
|
||||
- <doc:Multithreading>
|
||||
- <doc:DatabaseEncryption>
|
||||
|
||||
Reference in New Issue
Block a user