Database

Import db for SQL connections, queries, transactions, prepared statements, pool configuration, stats, and migrations.

Supported database drivers:

Driver Geblang name Notes
SQLite "sqlite" File-backed or in-memory SQLite databases
PostgreSQL "postgres" PostgreSQL server databases
MySQL / MariaDB "mysql" MySQL-compatible server databases

Use the class API (db.Connection) for application code. It is the portable DB wrapper: use ? positional placeholders or :name named placeholders across SQLite, PostgreSQL, MySQL, and MariaDB. Geblang rewrites the query for the selected driver and binds parameters in the correct order.

The low-level functional API (db.open, db.query, db.exec, and friends) remains available for adapter code that wants raw handles. It is intentionally closer to the underlying driver and should not be the default for application code.

Connecting

db.Connection(driver, connectionString) opens a connection pool using a driver name and connection string:

let sqlite = db.Connection("sqlite", "/tmp/app.sqlite");
let memory = db.Connection("sqlite", ":memory:");

let pgUrl = db.Connection(
    "postgres",
    "postgres://app:secret@localhost:5432/app?sslmode=disable"
);

let pgKv = db.Connection(
    "postgres",
    "host=localhost port=5432 user=app password=secret dbname=app sslmode=disable"
);

let mysql = db.Connection(
    "mysql",
    "app:secret@tcp(127.0.0.1:3306)/app?parseTime=true"
);

let mysqlSocket = db.Connection(
    "mysql",
    "app:secret@unix(/var/run/mysqld/mysqld.sock)/app?parseTime=true"
);

The options-dict form accepts pool tuning alongside the driver and DSN, applied at connect time: maxOpenConns, maxIdleConns, connMaxLifetimeMs, connMaxIdleTimeMs. Setting maxOpenConns without maxIdleConns keeps the idle pool the same size; with no pool options the idle pool defaults to 8 connections. Size maxOpenConns to your expected concurrency - a shared connection then serves parallel tasks and handlers at full speed:

let pool = db.Connection({
    "driver": "postgres",
    "dsn": pgUrl,
    "maxOpenConns": 16,
});

Parameterized queries use native prepared statements on all three drivers: PostgreSQL binds parameters over the extended protocol (pgx caches the server-side statements), MySQL uses the binary protocol's prepare/execute pair, and SQLite binds through its prepared-statement C API. Parameter values are never interpolated into SQL text.

SQLite connections default to a five-second busy_timeout on every pooled connection so concurrent access waits instead of failing with database is locked; pass your own busy_timeout or _pragma DSN parameter to override.

For a file-backed SQLite database, the options-dict form accepts tuning keys that map to per-connection pragmas applied at connect time:

Option Pragma Purpose
wal: true journal_mode(WAL) Write-ahead logging: concurrent readers with one writer.
synchronous: "NORMAL" synchronous(NORMAL) Sync level (OFF / NORMAL / FULL / EXTRA); NORMAL is the safe, fast pairing with WAL.
foreignKeys: true foreign_keys(ON) Enforce foreign-key constraints.
busyTimeoutMs: N busy_timeout(N) Override the default five-second busy timeout.
cacheSizeKb: N cache_size(-N) Page-cache size in KiB.
mmapSizeMb: N mmap_size(N MiB) Memory-mapped I/O size.
tempStoreMemory: true temp_store(MEMORY) Keep temp tables and indices in memory.

Each option is explicit; wal: true sets only journal_mode, so pair it with synchronous: "NORMAL" yourself. These keys are ignored for :memory: databases (the pragmas only matter for a file). Run connection.optimize() (PRAGMA optimize) periodically, or before closing a long-lived connection, to let SQLite refresh its query-planner statistics.

let db = db.Connection({
    "driver": "sqlite",
    "dsn": "/var/lib/app/app.sqlite",
    "wal": true,
    "synchronous": "NORMAL",
    "foreignKeys": true,
});
db.optimize();

A private in-memory database (:memory:) is distinct per connection, so its pool is pinned to a single connection; concurrent access shares one database rather than each connection seeing a separate empty one. Use a shared cache (file::memory:?cache=shared) or a file path if you need a larger pool.

The repository includes runnable examples:

  • examples/sqlite.gb uses a local SQLite file and runs without external services.
  • examples/database_objects.gb demonstrates the class API, option-dictionary connections, named placeholders, positional-list binding, transactions, prepared statements, streaming rows, and pool stats with SQLite.
  • examples/postgres_db.gb reads GEBLANG_POSTGRES_DSN and skips cleanly when it is not set. Example DSN: postgres://app:secret@localhost:5432/app?sslmode=disable.
  • examples/mysql_db.gb reads GEBLANG_MYSQL_DSN and skips cleanly when it is not set. Example DSN: app:secret@tcp(127.0.0.1:3306)/app?parseTime=true.

You can also pass an options dictionary and let Geblang build the connection string:

let sqlite = db.Connection({
    "driver": "sqlite",
    "path": "/tmp/app.sqlite"
});

let postgres = db.Connection({
    "driver": "postgres",
    "host": "localhost",
    "port": 5432,
    "database": "app",
    "user": "app",
    "password": "secret",
    "sslmode": "disable"
});

let mysql = db.Connection({
    "driver": "mysql",
    "host": "127.0.0.1",
    "port": 3306,
    "database": "app",
    "user": "app",
    "password": "secret",
    "parseTime": true
});

SQLite options support path, file, database, dbname, or "memory": true. PostgreSQL options support host, port, database/dbname, user, password, and sslmode. MySQL/MariaDB options support host, port, database/dbname, user, password, socket, parseTime, charset, and loc.

Query Parameters

The class API accepts varargs, a positional list, or a named dictionary:

conn.exec("insert into users (name, email) values (?, ?)", "Ada", "[email protected]");

conn.exec(
    "insert into users (name, email) values (?, ?)",
    ["Grace", "[email protected]"]
);

conn.exec(
    "insert into users (name, email) values (:name, :email)",
    {"name": "Linus", "email": "[email protected]"}
);

let rows = conn.query(
    "select id, email from users where name = :name",
    {"name": "Ada"}
);

Prepared statements use the same binding rules:

let stmt = conn.prepare("select id from users where email = :email");
let rows = stmt.query({"email": "[email protected]"});
defer rows.close();
stmt.close();

Named placeholders are written as :name, where name starts with a letter or underscore and then contains letters, digits, or underscores. Placeholders inside quoted strings or SQL comments are ignored by the binder.

db.Connection

db.Connection(driver, connectionString) or db.Connection(options) opens a SQL connection pool and returns a connection object.

Method Returns Description
exec(sql, args...) dict Execute a statement; returns rowsAffected and lastInsertId
query(sql, args...) db.Rows Run a query and return a streaming cursor
begin() db.Transaction Start a transaction
prepare(sql) db.Statement Prepare a reusable statement
configure(options) void Set pool options such as maxOpenConns, maxIdleConns, connMaxLifetimeMs, connMaxIdleTimeMs
stats() dict Return pool statistics
optimize() void Run PRAGMA optimize (SQLite maintenance)
migrate(migrations) dict Apply idempotent migrations
close() void Close the connection pool
import io;
import db;

let conn = db.Connection({
    "driver": "sqlite",
    "path": "/tmp/app.sqlite"
});
defer conn.close();

conn.exec("create table if not exists users (id text, name text)");
conn.exec(
    "insert into users (id, name) values (:id, :name)",
    {"id": "1", "name": "Ada"}
);

let rows = conn.query("select name from users where id = :id", {"id": "1"});
defer rows.close();

while (rows.next()) {
    let row = rows.row();
    io.println(row["name"]);
}

Transactions And Statements

db.Transaction has exec, query, commit, and rollback. Transaction queries use the same portable placeholder rules as Connection.

let tx = conn.begin();
try {
    tx.exec(
        "insert into users (id, name) values (:id, :name)",
        {"id": "2", "name": "Grace"}
    );
    tx.commit();
} catch (Error e) {
    tx.rollback();
    throw e;
}

db.Statement has exec, query, and close. A statement remembers the named placeholder order from prepare, so later calls can pass a dictionary.

let stmt = conn.prepare("select name from users where id = :id");
let result = stmt.query({"id": "2"});
defer result.close();
stmt.close();

Rows

Connection.query, Transaction.query, and Statement.query return db.Rows. Rows are streaming cursors: a next()/row() loop (or a for-in loop) holds one row at a time regardless of result-set size, so scanning millions of rows stays at constant memory on every driver (SQLite, PostgreSQL, MySQL). Exhausting a cursor closes the native SQL rows automatically, but defer rows.close() is still the normal pattern when a function may return early.

Cursors are iterable directly (1.19.0):

for (row in conn.query("select id, name from users")) {
    io.println(row["name"]);
}

The random-access methods (all(), first(), get(i), length(), isEmpty(), toList()) start caching rows from the moment they are first called. Mixing styles is allowed and has remaining-rows semantics: rows already consumed by next() before the first random-access call are not replayed - rows.next(); rows.all() returns everything after the first row.

The functional API (db.query, db.txQuery, and db.stmtQuery) keeps returning an eager list<dict> for code that expects materialized result sets. As of 1.19.0 the functional helpers also accept Connection/Transaction/Statement objects (not just raw handles) and normalize ? placeholders per driver, exactly like the class API.

Method Returns Description
next() bool Advance to the next row
row() `dict null`
columns() list<string> Column names for the result set
close() void Close the cursor
all() list<dict> Drain the cursor and return the cached and remaining rows
length() int Drain the cursor and return the number of rows
isEmpty() bool Read enough to determine whether the result set is empty
first() `dict null`
get(index) `dict null`
toList() list<dict> Alias for all()
let rows = conn.query("select id, name from users order by id");
defer rows.close();

io.println(rows.columns());

while (rows.next()) {
    let row = rows.row();
    io.println(row["id"] as string + ": " + row["name"]);
}

Low-Level Functions

Use these when writing adapters or working with raw handles:

Function Description
open(driver, connectionString) Open a raw database handle
exec(handle, sql, args...) Execute SQL using driver-native placeholders
query(handle, sql, args...) Return an eager list<dict>
begin(handle) Start a raw transaction handle
txExec(tx, sql, args...) Execute inside a transaction
txQuery(tx, sql, args...) Eager transaction query
commit(tx) / rollback(tx) Finish a transaction
prepare(handle, sql) Prepare a raw statement handle
stmtExec(stmt, args...) Execute a prepared statement
stmtQuery(stmt, args...) Eager prepared-statement query
stmtClose(stmt) Close a prepared statement
configure(handle, options) Configure pool limits
stats(handle) Return pool stats
migrate(handle, migrations) Apply idempotent migrations
close(handle) Close the database handle