Sqlite

Sqlite :: # (opaque)

Execute bounded SQLite statements with statically dispatched parameter encoders and row parsers.

Query parameters are ordinary flat records. Their field names map to SQLite parameters with a leading colon, so { status } binds :status. Query results are selected by the expected Roc type. Structural records derive their parser automatically when their fields use supported SQLite types.

Treat query as trusted, application-owned SQL. Never interpolate request data or other untrusted values into it. Pass data values through params, which uses SQLite parameter binding rather than manual escaping. Parameters cannot represent identifiers or SQL syntax; choose dynamic identifiers and clauses from an application-controlled allowlist before constructing a query.

Todo : { id : I64, task : Str }

todos : List(Todo)
db = Sqlite.open!(Sqlite.default_config(db_path))?
todos = Sqlite.query_many!({
    db,
    query: "SELECT id, task FROM todos WHERE status = :status",
    params: { status: "open" },
    limits: Sqlite.default_query_limits,
})?

SQLite INTEGER maps strictly to I64, REAL to F64, TEXT to Str, and BLOB to Sqlite.Blob. No text/number coercions are performed. TODO: Add derived nullable fields once the compiler can compose their parser errors through a platform-defined encoding.

A Db owns a bounded host connection pool. Statements are immutable logical query descriptors; each execution briefly leases a connection and a cached native prepared statement. Executions are safe to use concurrently. Their final ARC release resets the native statement and returns the connection on every success and error path.

default_config : Path -> Config

Conservative defaults for a conventional web application.

open! : Config => Try(Db, QueryError)

Open and validate a bounded database pool.

prepare! : { db : Db, query : Str } => Try(Stmt, QueryError)

Prepare a reusable logical statement and cache its result-column metadata.

execute! : { db : Db, query : Str, params : params } => Try({  }, QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
    ]

Execute a one-shot statement that must not return rows.

query! : { db : Db, query : Str, params : params, limits : QueryLimits } => Try(row, QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
        row.parser_for : RowEncoding -> RowState -> Try({ value : row, rest : RowState }, QueryError),
    ]

Execute a one-shot query returning exactly one inferred result value.

query_many! : { db : Db, query : Str, params : params, limits : QueryLimits } => Try(List(row), QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
        row.parser_for : RowEncoding -> RowState -> Try({ value : row, rest : RowState }, QueryError),
    ]

Execute a one-shot query returning a list of inferred result values.

begin! : Db, TransactionMode => Try(Transaction, QueryError)

Begin a transaction on one connection leased from the pool.

errcode_to_str : [
    Abort,
    AuthDenied,
    Busy,
    CanNotOpen,
    Constraint,
    Corrupt,
    Done,
    Empty,
    Error,
    Format,
    Full,
    IOErr,
    Internal,
    Interrupt,
    Locked,
    Mismatch,
    Misuse,
    NoLFS,
    NoMem,
    NotADatabase,
    NotFound,
    Notice,
    OutOfRange,
    Perm,
    Protocol,
    ReadOnly,
    Row,
    Schema,
    TooBig,
    Warning,
    Unknown(
        I64,
    ),
] -> Str

Convert an ErrCode to a display string.

Config : {
    path : Path,
    max_connections : U64,
    acquire_timeout_ms : U64,
    busy_timeout_ms : U64,
    max_cached_statements_per_connection : U64,
    journal_mode : JournalMode,
    synchronous : Synchronous,
}

Configuration for one bounded host-owned connection pool. max_connections is 1-64, both timeouts are at most ten minutes, and each connection caches at most 256 native statements.

JournalMode : [Delete, Wal]

SQLite rollback-journal policy applied and verified on every connection.

Synchronous : [Full, Normal]

Full is the durable default. Normal trades power-loss durability for substantially faster commits while preserving database consistency.

Db

Sqlite.Db :: # (opaque)

A host-owned SQLite connection pool, safe to retain in immutable context.

Value : [
    Null,
    Real(F64),
    Integer(I64),
    String(Str),
    Bytes(List(U8)),
]

A raw SQLite value. Most applications use derived record codecs instead.

Binding : {
    name : Str,
    value : Value,
}

A raw named binding used internally by the derived parameter encoder.

QueryLimits : {
    max_bytes : U64,
    max_rows : U64,
    timeout_ms : U64,
}

Bounds on a materialized query. max_bytes covers the host-side SQLite value storage handed to Roc; max_rows also bounds per-row record overhead after decoding. timeout_ms interrupts SQLite virtual-machine execution; pool acquisition has its own database-level timeout.

ValueType : [Blob, Integer, Null, Real, Text]

SQLite's five runtime storage classes, used in decode diagnostics.

QueryError : [
    DuplicateColumn(Str),
    ExpectedSingleColumn({ actual : U64 }),
    InvalidValue({ column : Str }),
    MalformedRow,
    MissingRequiredField(Str),
    MultipleValuesForParameter,
    NestedParameterRecord,
    NoRowsReturned,
    ParameterValueMissing(Str),
    ParameterValueOutsideRecord,
    PoolSaturated,
    QueryTimedOut,
    ResourceSaturated,
    ResultTooLarge({ max_bytes : U64 }),
    RowsReturnedUseQueryInstead,
    SqliteErr(ErrCode, Str),
    TooManyRows({ max_rows : U64 }),
    TooManyRowsReturned,
    ConcurrentTransactionUse,
    TransactionFinished,
    UnconsumedColumns,
    UnexpectedType({ actual : ValueType, column : Str, expected : ValueType }),
]

Every failure produced by SQLite operations and their derived codecs.

Blob

Sqlite.Blob :: # (opaque)

A SQLite BLOB. The nominal wrapper distinguishes blobs from ordinary Roc lists for generic parsing and encoding.

TODO: Use Blob inside mixed result records once the compiler composes a custom nominal parser's errors with sibling derived fields.

parser_for : RowEncoding -> RowState -> Try(
    {
        rest : RowState,
        value : Blob,
    },
    [
        MalformedRow,
        UnconsumedColumns,
        ..[
            ConcurrentTransactionUse,
            DuplicateColumn(
                Str,
            ),
            ExpectedSingleColumn(
                {
                    actual : U64,
                },
            ),
            InvalidValue(
                {
                    column : Str,
                },
            ),
            MissingRequiredField(
                Str,
            ),
            MultipleValuesForParameter,
            NestedParameterRecord,
            NoRowsReturned,
            ParameterValueMissing(
                Str,
            ),
            ParameterValueOutsideRecord,
            PoolSaturated,
            QueryTimedOut,
            ResourceSaturated,
            ResultTooLarge(
                {
                    max_bytes : U64,
                },
            ),
            RowsReturnedUseQueryInstead,
            SqliteErr(
                ErrCode,
                Str,
            ),
            TooManyRows(
                {
                    max_rows : U64,
                },
            ),
            TooManyRowsReturned,
            TransactionFinished,
            UnexpectedType(
                {
                    actual : ValueType,
                    column : Str,
                    expected : ValueType,
                },
            ),
        ],
    ],
)
encoder_for : encoding -> Blob, state -> Try(state, err)
    where [
        encoding.encode_bytes : List(U8), state -> Try(state, err),
    ]
ParamsState : {
    bindings : List(Binding),
    field : [Field(Str), NoField],
    value : [Encoded(Value), NoValue],
}

State used by the derived parameter-record encoder.

ParamsEncoding

Sqlite.ParamsEncoding :: # (opaque)

SQLite named-parameter encoding.

encode_record : ParamsState, U64, (ParamsState, (ParamsState, Str, (ParamsState -> Try(ParamsState, QueryError)) -> Try(ParamsState, QueryError)) -> Try(ParamsState, QueryError)) -> Try(ParamsState, QueryError)
RowState : {
    columns : List(Str),
    current : [Current({ name : Str, value : Value }), NoCurrent],
    next : U64,
    values : List(Value),
}

Pure state used by a compiler-derived SQLite row parser.

RowEncoding

Sqlite.RowEncoding :: # (opaque)

SQLite row encoding consumed by parser_for.

parse_record_start : RowEncoding, RowState -> Try([Counted({ len : U64, rest : RowState }), Uncounted(RowState)], QueryError)
parse_str : RowEncoding, RowState -> Try({ value : Str, rest : RowState }, QueryError)
parse_i64 : RowEncoding, RowState -> Try({ value : I64, rest : RowState }, QueryError)
parse_f64 : RowEncoding, RowState -> Try({ value : F64, rest : RowState }, QueryError)
parse_record_field : RowEncoding, FieldNames(_shape), RowState -> Try(
    [
        Field({ field : FieldName(_shape), rest : RowState }),
        TryField({ name : Str, rest : RowState }),
        TryFieldCaseless({ name : Str, rest : RowState }),
        Continue(RowState),
        Done(RowState),
    ],
    QueryError,
)

Stmt

Sqlite.Stmt :: # (opaque)

Represents a prepared statement that can be executed many times.

execute! : Stmt, params => Try({  }, QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
    ]

Execute a prepared statement that must not return rows.

query! : Stmt, params, QueryLimits => Try(row, QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
        row.parser_for : RowEncoding -> RowState -> Try({ value : row, rest : RowState }, QueryError),
    ]

Decode exactly one row as the expected result type.

query_many! : Stmt, params, QueryLimits => Try(List(row), QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
        row.parser_for : RowEncoding -> RowState -> Try({ value : row, rest : RowState }, QueryError),
    ]

Decode all rows as the expected list item type.

Transaction

Sqlite.Transaction :: # (opaque)

A transaction pinned to one pooled connection. Dropping its final Roc reference before commit! or rollback! rolls it back automatically. Operations within one transaction are sequential; overlapping use returns ConcurrentTransactionUse.

execute! : Transaction, { query : Str, params : params } => Try({  }, QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
    ]
query! : Transaction, { query : Str, params : params, limits : QueryLimits } => Try(row, QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
        row.parser_for : RowEncoding -> RowState -> Try({ value : row, rest : RowState }, QueryError),
    ]
query_many! : Transaction, { query : Str, params : params, limits : QueryLimits } => Try(List(row), QueryError)
    where [
        params.encoder_for : ParamsEncoding -> params, ParamsState -> Try(ParamsState, QueryError),
        row.parser_for : RowEncoding -> RowState -> Try({ value : row, rest : RowState }, QueryError),
    ]
ErrCode : [
    Error,
    Internal,
    Perm,
    Abort,
    Busy,
    Locked,
    NoMem,
    ReadOnly,
    Interrupt,
    IOErr,
    Corrupt,
    NotFound,
    Full,
    CanNotOpen,
    Protocol,
    Empty,
    Schema,
    TooBig,
    Constraint,
    Mismatch,
    Misuse,
    NoLFS,
    AuthDenied,
    Format,
    OutOfRange,
    NotADatabase,
    Notice,
    Warning,
    Row,
    Done,
    Unknown(I64),
]

Represents SQLite result codes.