Skip to content

Guide 43 of 47

Database

SQLite, SQL transpilation, query, execute and connection management.

On this page

std::database exports Database, DbError, DbErrorKind, SQL and Blob. Opening, executing and querying return Result. Use ? in fallible functions, or unwrap() in small examples.

Bound queries

use std::database::Database

let db = Database::open("sqlite://:memory:").unwrap()
defer db.close()
db.execute("CREATE TABLE stock (id BIGINT PRIMARY KEY, available BIGINT NOT NULL)", []).unwrap()
db.execute("INSERT INTO stock VALUES (?, ?)", [1, 20]).unwrap()

let changed = db.execute(
    "UPDATE stock SET available = available - ? WHERE id = ? AND available >= ?",
    [3, 1, 3],
).unwrap()
assert(changed == 1)
let rows = db.query("SELECT available FROM stock WHERE id = ?", [1]).unwrap()
assert(rows[0]["available"] == 17)

Values stay bound to the driver; never concatenate user input into SQL. Portable ? placeholders and quoted identifiers are compiled for the connected driver. PostgreSQL binds parameters and returns actual affected-row counts, including commands with RETURNING. Use query when the returned rows matter. Rows are materialized; this API is synchronous and does not expose a streaming cursor.

PostgreSQL connections and pooling

use std::database::Database

let db = Database::open(
    "postgres://user:password@localhost:5432/erp?pool_size=8&pool_timeout=5000",
).unwrap()
defer db.close()
assert(db.driver() == "postgres")
db.ping().unwrap()

pool_size limits PostgreSQL sessions. pool_timeout is the connection acquisition timeout in milliseconds. A transaction retains its leased connection until completion. Other requests can acquire other available leases. A connection opened without a pool owns a single session.

Repeated opens of the same URL share a process resource. Closing one shared handle closes that resource for its other ordinary aliases; long-running applications should own the pool at application scope.

The PostgreSQL driver currently connects with NoTls. TLS/SCRAM deployment configuration must not be inferred from support for parameter binding or pooling. The new server execution, pool and migration paths have source regressions; runtime/database validation remains pending in this development pass.

Callback transactions

use std::database::{Database, DbError}

fn reserve(db: Database, id: int, quantity: int) -> Result<int, DbError> {
    let result = db.transaction(|tx| -> Result<int, DbError> {
        assert(tx.is_transaction())
        return tx.execute(
            "UPDATE stock SET available = available - ? WHERE id = ? AND available >= ?",
            [quantity, id, quantity],
        )
    })?
    return result as Result<int, DbError>
}

Use the callback handle for every operation belonging to the transaction. It is a distinct handle to the pinned session. A panic or returned Result::Err rolls back; a successful return commits. The outer Result describes transaction setup/finalization and preserves the callback's exact value, so a fallible callback produces a nested result.

A dedicated parent connection rejects commands while its callback is active. A pool may serve independent operations through other leases; those operations do not join the callback's transaction. Nested transactions are unsupported. Raw BEGIN, COMMIT, ROLLBACK and savepoint control inside the callback are rejected because the callback owns completion.

A callback handle expires after return. Returning or storing it does not extend the transaction: later queries return TransactionFailed. Closing the callback handle aborts it without closing its owner.

Database.driver() returns the driver name even after close. Database.is_transaction() is true only for an active callback handle.

Structured errors

use std::database::{DbError, DbErrorKind}

fn transient(error: DbError) -> bool {
    return error.is_retryable()
}

fn duplicate(error: DbError) -> bool {
    return error.is(DbErrorKind::UniqueViolation)
}

Constraint failures retain their categories: UniqueViolation, ForeignKeyViolation, NotNullViolation, CheckViolation and ConstraintViolation. PostgreSQL and MySQL also classify transient SerializationFailure, Deadlock and LockTimeout errors.

is_retryable() identifies those transient categories. Retrying requires re-running the whole transaction with idempotent application effects. A generic TransactionFailed, SQL syntax error or uniqueness violation is not automatically retryable. Messages and driver identity are available through message() and driver().

Exact decimals and binary data

ORM models now accept native decimal/bigdecimal fields without a manual codec: numeric storage defaults to DECIMAL(28,2) and DECIMAL(38,2), respectively. Generated bindings use canonical decimal text and validate the destination precision/scale. Query operands retain their own precision. Explicit Text storage preserves the native value on SQLite but does not provide numeric SQL semantics. For raw Database parameter arrays, encode decimal values explicitly with to_plain_string(); raw result rows remain driver scalars and require decimal::parse/bigdecimal::parse when no model decoder is involved.

PostgreSQL NUMERIC(p,s) accepts canonical decimal strings as bound values and returns decimal strings without a floating-point conversion. The driver rejects a floating-point parameter targeting NUMERIC. An ORM codec can provide a nominal domain over this exact physical value.

SQLite NUMERIC affinity can convert values to floating point. Exact numeric ORM schemas and migration snapshots therefore require PostgreSQL; text-backed decimal codecs remain appropriate for exact storage without SQL arithmetic.

Blob::from_bytes([0, 255, 65]) builds opaque binary data. Bind blob.unwrap(); empty bytes remain distinct from NULL and text.

Schema migrations

Configure the schema entry in zolo.toml:

[database]
schema = "src/schema.zolo"
dialect = "postgres"
migrations = "migrations"
url_env = "DATABASE_URL"
zolo db generate initial
zolo db status
zolo db migrate
zolo db check
zolo db rollback 1

Generation reads compiler/ORM schema metadata and writes reviewable portable SQL plus deterministic snapshots. It does not run the application or connect to the database. Review up.sql and down.sql before applying.

PostgreSQL execution uses a dedicated session, ignores application pool options, and obtains a transaction-scoped advisory lock for the current schema. Another runner receives an explicit conflict. DDL, schema verification and checksum history commit together; failures roll back. check coordinates with the same lock. baseline adopts an existing matching schema with no prior history.

Plans support tables, columns, defaults/nullability, reviewed type changes, primary/unique/foreign keys and supported indexes. Foreign keys are created after their referenced keys/indexes, including cyclic relationships. Generated integer keys carry an explicit marker that compiles to BIGINT GENERATED BY DEFAULT AS IDENTITY; an ordinary integer primary key is not inferred to be generated. Numeric snapshots retain precision and scale. Exact numeric defaults are emitted as text literals that PostgreSQL casts, avoiding float-based SQL reformatting.

--allow-destructive is required for drops and type changes. Adding a required column to an existing table needs a SQL default or a staged nullable/backfill migration. Dropped required columns must be restorable on rollback. --allow-rebuild remains SQLite-only.

The PostgreSQL snapshot contract covers the current schema with ASCII identifiers of at most 63 bytes. It fails explicitly on unsupported features such as custom triggers, views, partitions, inheritance, row policies, generated expressions, custom sequence options, deferrable constraints, custom collations or unsupported index contracts. Constraint names must match the reviewed generated names; baseline reports expected and actual names for an unmanaged schema. MySQL migration generation/execution is not implemented.

Standalone SQL rendering

use std::database::SQL

let postgres = SQL::compile(
    "SELECT id FROM stock WHERE available >= ? ORDER BY id LIMIT 10",
    "postgres",
).unwrap()
print(postgres)

Rendering a dialect does not imply that its driver, migrations or every SQL feature is implemented. The API declaration in database.zolo is the complete public surface.

DOCS / FEEDBACK

Did this page leave a question?

Tell us where the explanation lost you. Documentation is part of the language experience.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

en