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.
|
||||
Reference in New Issue
Block a user