Refactor entire codebase and rewrite documentation

This commit is contained in:
2025-10-10 18:06:34 +03:00
parent b4e9755c15
commit 8e471f2b9f
74 changed files with 3405 additions and 4149 deletions

View 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
functions 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 functions 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. Its 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)

View File

@@ -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 SQLCiphers 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/)

View 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 SQLites diagnostics only.
## Custom Functions
Errors thrown from ``Function/Scalar`` or ``Function/Aggregate`` implementations are reported back
to SQLite as `SQLITE_ERROR`, with the errors `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)

View 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 dont 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 SQLites synchronous API.
## Synchronization Options
- ``Connection/Options/nomutex`` — disables SQLites 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 SQLites
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 SQLites internal thread from being blocked by
slow I/O or external operations.

View 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 SQLites 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 Swifts 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, its 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)

View 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 statements 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:)``

View 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 rows 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``

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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:)``

View File

@@ -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>