Swift Driver
High-performance Swift driver for Stoolap. Calls the Rust engine directly through the official C ABI with zero intermediate wrappers. Provides sync, async, and streaming cursor APIs. Targets macOS 12+ and iOS 15+.
Installation
Swift Package Manager
dependencies: [
.package(url: "https://github.com/stoolap/stoolap-swift.git", from: "0.4.0")
]
The package links against libstoolap_c, a thin cdylib shim that re-exports the official stoolap::ffi cursor API. Prebuilt binaries are available on the releases page.
Quick Start
import Stoolap
let db = try Database.open(":memory:")
try db.exec("""
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT
)
""")
try db.execute(
"INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
[.integer(1), .text("Alice"), .text("alice@example.com")]
)
let users = try db.query("SELECT * FROM users ORDER BY id")
for user in users {
print(user["name"]?.stringValue ?? "")
}
let one = try db.queryOne("SELECT * FROM users WHERE id = $1", [.integer(1)])
print(one?["email"]?.stringValue ?? "")
Opening a Database
// In-memory
let db = try Database.open(":memory:")
let db = try Database.open("memory://")
// File-based (data persists across restarts)
let db = try Database.open("./mydata")
let db = try Database.open("file:///absolute/path/to/db")
// With configuration
let db = try Database.open("./mydata?sync=full&compression=on")
Methods
| Method | Returns | Description |
|---|---|---|
execute(sql, params?) |
Int64 |
Execute DML statement, return rows affected |
exec(sql) |
Void |
Execute one or more semicolon-separated statements |
query(sql, params?) |
[Row] |
Query rows with named column access |
queryOne(sql, params?) |
Row? |
Query single row |
queryRaw(sql, params?) |
ColumnarResult |
Query in flat columnar format |
queryCursor(sql, params?) |
RowCursor |
Streaming cursor over result set |
prepare(sql) |
PreparedStatement |
Create a prepared statement |
begin() |
Transaction |
Begin a transaction |
withTransaction(_:) |
Generic | Auto-commit/rollback closure |
Row Access
Row is a zero-allocation struct backed by an ArraySlice<Value> into a shared result-wide cell array.
let rows = try db.query("SELECT id, name, email FROM users ORDER BY id")
for row in rows {
// By name (linear scan, fast for typical column counts)
let name = row["name"]?.stringValue
// By index (zero-based)
let id = row[0].int64Value
// Iterate all columns
row.forEach { column, value in
print("\(column): \(value)")
}
}
Columnar Results
queryRaw() returns a ColumnarResult with all cells in a single flat array. Zero per-row heap allocations.
let raw = try db.queryRaw("SELECT id, name FROM users ORDER BY id")
// Row access (ArraySlice view)
let firstRow = raw[row: 0]
// Cell access
let name = raw[row: 0, column: 1]
// Column-wise access (strided RandomAccessCollection, zero allocation)
let ids = raw.column(at: 0) // ColumnarColumn
let names = raw.column(named: "name") // ColumnarColumn?
for id in ids {
print(id.int64Value ?? 0)
}
Streaming Cursor
For large result sets where you want bounded memory instead of bulk materialization:
let cursor = try db.queryCursor("SELECT * FROM users ORDER BY id")
while try cursor.next() {
// Read individual cells without materializing the whole row
let id = try cursor.value(at: 0)
let name = try cursor.value(named: "name")
// Or materialize the current row
let row = try cursor.row()
print(row["email"]?.stringValue ?? "")
}
// Or drain with a closure
let cursor = try db.queryCursor("SELECT * FROM large_table")
try cursor.forEachRemaining { row in
process(row)
}
Prepared Statements
Prepared statements parse SQL once and reuse the cached execution plan. Column names are cached after the first execution, eliminating per-call string allocations.
let insert = try db.prepare("INSERT INTO users VALUES ($1, $2, $3)")
try insert.execute([.integer(1), .text("Alice"), .text("alice@example.com")])
try insert.execute([.integer(2), .text("Bob"), .text("bob@example.com")])
let lookup = try db.prepare("SELECT * FROM users WHERE id = $1")
let user = try lookup.queryOne([.integer(1)])
Prepared Statement Methods
| Method | Returns | Description |
|---|---|---|
execute(params?) |
Int64 |
Execute DML statement |
query(params?) |
[Row] |
Query rows |
queryOne(params?) |
Row? |
Query single row |
queryRaw(params?) |
ColumnarResult |
Query in columnar format |
queryCursor(params?) |
RowCursor |
Streaming cursor |
executeBatch(paramsList) |
Int64 |
Batch execute in single FFI call |
Batch Execution
Execute multiple parameter sets in a single FFI call. Automatically wraps in a transaction on the Rust side.
let insert = try db.prepare("INSERT INTO users VALUES ($1, $2, $3)")
let changes = try insert.executeBatch([
[.integer(1), .text("Alice"), .text("alice@example.com")],
[.integer(2), .text("Bob"), .text("bob@example.com")],
[.integer(3), .text("Charlie"), .text("charlie@example.com")],
])
// changes == 3
Transactions
Auto-commit/rollback
try db.withTransaction { tx in
try tx.execute("INSERT INTO users VALUES ($1, $2, $3)",
[.integer(1), .text("Alice"), .text("alice@example.com")])
try tx.execute("INSERT INTO users VALUES ($1, $2, $3)",
[.integer(2), .text("Bob"), .text("bob@example.com")])
}
Manual control
let tx = try db.begin()
do {
try tx.execute("INSERT INTO users VALUES ($1, $2, $3)",
[.integer(1), .text("Alice"), .text("alice@example.com")])
try tx.commit()
} catch {
try? tx.rollback()
throw error
}
Transaction Methods
| Method | Returns | Description |
|---|---|---|
execute(sql, params?) |
Int64 |
Execute DML statement |
query(sql, params?) |
[Row] |
Query rows |
queryOne(sql, params?) |
Row? |
Query single row |
queryRaw(sql, params?) |
ColumnarResult |
Query in columnar format |
commit() |
Void |
Commit the transaction |
rollback() |
Void |
Rollback the transaction |
Async API
AsyncDatabase wraps Database with Swift concurrency. All calls dispatch to a detached task so they do not block the cooperative thread pool.
let db = try await AsyncDatabase.open(":memory:")
try await db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
try await db.execute("INSERT INTO users VALUES ($1, $2)",
[.integer(1), .text("Alice")])
let users = try await db.query("SELECT * FROM users")
let one = try await db.queryOne("SELECT * FROM users WHERE id = $1", [.integer(1)])
let raw = try await db.queryRaw("SELECT * FROM users")
Error Handling
All methods throw StoolapError on failure:
do {
try db.execute("INSERT INTO users VALUES ($1, $2)", [.integer(1), .null])
} catch let error as StoolapError {
print("Database error: \(error.message)")
}
Type Mapping
| Swift Value | Stoolap SQL | Notes |
|---|---|---|
.integer(Int64) |
INTEGER |
64-bit signed |
.float(Double) |
FLOAT |
64-bit double |
.text(String) |
TEXT |
UTF-8 encoded |
.boolean(Bool) |
BOOLEAN |
|
.null |
NULL |
Any type |
.timestamp(Date) |
TIMESTAMP |
Nanosecond precision |
.json(String) |
JSON |
Pre-serialized JSON string |
.blob(Data) |
BLOB |
Raw bytes |
.vector([Float]) |
VECTOR |
Packed f32 for similarity search |
Persistence
File-based databases persist data using WAL and immutable cold volumes.
let db = try Database.open("./mydata?sync=full")
try db.exec("CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT)")
try db.execute("INSERT INTO kv VALUES ($1, $2)", [.text("hello"), .text("world")])
// Reopen: data is still there
let db2 = try Database.open("./mydata")
let row = try db2.queryOne("SELECT * FROM kv WHERE key = $1", [.text("hello")])
Thread Safety
Database is @unchecked Sendable and safe to share across threads (the Rust engine uses interior locking). Transaction instances must be used by one thread at a time. PreparedStatement is @unchecked Sendable with an internal lock protecting the column name cache.
Building from Source
Requires Rust (stable) and Swift 5.9+.
git clone https://github.com/stoolap/stoolap-swift.git
cd stoolap-swift
# Build the Rust shared library
cd crates/stoolap-c && cargo build --release && cd ../..
# Build and test
swift build
swift test