- C 71.6%
- Odin 28%
- Shell 0.4%
| examples | feat(eventsource): add streaming SSE client | |
| pkg | feat(eventsource): add streaming SSE client | |
| scripts | refactor(sqlite3): vendor libsqlite3.so alongside the binding (drop system:sqlite3) | |
| vendor/http | feat(eventsource): add streaming SSE client | |
| .gitignore | refactor: move database and fff under pkg/ for pkg: collection imports | |
| AGENTS.md | docs(agents): clarify validation, compatibility, and dependency rules | |
| odinfmt.json | chore: add ols and odinfmt config | |
| ols.json | chore(ols): fix checker_args array to string | |
| README.md | refactor: move SQL packages from db/ to pkg/database/ | |
Odin SQL runtime
Experimental Odin-native SQL runtime inspired by Go database/sql public concepts. It is not a line-by-line port.
Package layout
pkg/database/sqlite3
Raw SQLite C API binding.
pkg/database/sql
Generic SQL runtime abstraction.
pkg/database/drivers
SQLite driver adapter from pkg/database/sqlite3 to pkg/database/sql.
Design notes
pkg/database/sqlis database-agnostic.pkg/database/sqldoes not know about SQLite or any other concrete database.- Concrete drivers live in their own packages and expose
sql.Driverdescriptors. - Open databases with explicit driver descriptors, not string registration.
DBis a pool, not a single connection.- Pool v1 returns
Busywhen no connection is available instead of blocking. close(db)returnsBusyif there are still active connections pinned by openRows,Stmt, orTxhandles.- Core runtime intentionally avoids reflection and struct scanning magic.
- Use explicit getters:
get_i64,get_string,get_bool,get_f64,get_bytes,is_null. Rows,Stmt, andTxare transient value handles returned by value.Rows,Stmt, andTxmust not be copied after initialization.rows_destroy,stmt_destroy, andtx_destroywere removed from the public API.get_stringandget_bytescopy values into the caller allocator.- SQLite is the first supported driver.
- Code generation/sqlc-like tooling is out of scope for this implementation.
Canonical usage
importsql"pkg/database/sql"importsqlite"pkg/database/drivers"db,err:=sql.open(sqlite.Driver,"file:nabla.db")iferr.kind!=.None{// handle error
}defersql.close(db)Opening and pool defaults
sql.open now takes a sql.Driver descriptor:
open::proc(driver:Driver,dsn:string,allocator:=context.allocator)->(^DB,Error)The Driver descriptor carries:
namedataopenclosedefault_max_open_connsdefault_max_idle_conns
Each concrete driver can provide its own conservative defaults. SQLite currently uses:
max_open_conns = 1max_idle_conns = 1
Pool settings can still be overridden after open:
sql.set_max_open_conns(db,1)sql.set_max_idle_conns(db,1)st:=sql.stats(db)Pool notes:
set_max_open_conns(db, 0)means no open-connection limit.set_max_idle_conns(db, 0)means released connections are not kept idle in the pool.
Parameters
SQL parameters use explicit sql.Value constructors:
sql.null()sql.bool(true)sql.int(123)sql.float(7.25)sql.text("hello")sql.bytes([]u8{1,2,3})Value.text and Value.bytes are borrowed input values. Drivers should copy/bind transiently unless documented otherwise. The SQLite driver uses transient/copy binding semantics for text/blob values.
Querying rows
query returns a transient Rows value handle:
rows,err:=sql.query(db,"select id, name from users order by id",nil)iferr.kind!=.None{// handle error
}defersql.rows_close(&rows)forsql.rows_next(&rows){id,_:=sql.get_i64(&rows,0)name,_:=sql.get_string(&rows,1,context.allocator)_=id_=name}err=sql.rows_err(&rows)iferr.kind!=.None{// handle error
}Available row helpers:
columnsrows_nextrows_closerows_erris_nullget_i64get_f64get_boolget_stringget_bytes
columns(&rows, allocator) returns column names owned by the caller allocator.
Prepared statements
prepare returns a transient Stmt value handle:
stmt,err:=sql.prepare(db,"select id from users where name = ?")iferr.kind!=.None{// handle error
}defersql.stmt_close(&stmt)rows,err:=sql.stmt_query(&stmt,[]sql.Value{sql.text("Matheus")})iferr.kind!=.None{// handle error
}defersql.rows_close(&rows)Statement lifecycle rules:
Stmtpins a connection untilstmt_close(&stmt).stmt_query(&stmt, ...)marks the statement busy.- Rows from
stmt_querymust be closed before the statement can be reused or closed. stmt_execandstmt_queryreject busy statements withBusy.
Transactions
begin returns a transient Tx value handle:
tx,err:=sql.begin(db)iferr.kind!=.None{// handle error
}defersql.rollback_if_open(&tx)_,err=sql.tx_exec(&tx,"insert into users(name) values(?)",[]sql.Value{sql.text("Commit")})iferr.kind!=.None{// handle error
}err=sql.commit(&tx)iferr.kind!=.None{// handle error
}Transaction lifecycle rules:
Txpins a connection untilcommit(&tx)orrollback(&tx).- Rows opened via
tx_query(&tx, ...)must be closed before commit/rollback. commitandrollbackreturnBusyif transaction rows are still open.
Connection pinning rules
- Rows from
querypin a connection untilrows_close(&rows). Stmtpins a connection untilstmt_close(&stmt).- Rows from
stmt_querymust be closed before statement reuse. Txpins a connection untilcommit(&tx)orrollback(&tx).- Rows inside a
Txmust be closed before commit/rollback.
SQLite driver behavior
pkg/database/drivers exposes:
Driver::sql.DriverNormal SQLite usage does not require global registration.
Current SQLite behavior:
- opens read-write/create/URI
- sets
busy_timeout = 5000ms - enables
PRAGMA foreign_keys = ON - uses conservative pool defaults:
1 / 1 - preserves
Rows/Stmt/Txvalue-handle semantics
Testing
pkg/database/sqlhas fake driver tests to validate the generic runtime independently from SQLite.pkg/database/driverskeeps SQLite-specific lifecycle tests.
Non-goals for this implementation
Not included:
- sqlc/codegen
- migrations
- PostgreSQL/MySQL drivers
- reflection scanning
- high-level
pkg/database/sqlitepackage - blocking pool waits
- statement cache
- named parameters
- async cancellation
API is experimental and expected to change.