Refactor entire codebase and rewrite documentation
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user