# SQLite
Deft ships SQLite built in — no driver to install, no shared library
to load. The `sqlite/*` stdlib functions work identically in the
scripting binary, the ANSI binary, and the full TUI. SQLite is
always available; you don't need a build flag.
## Opening and Closing
def db [sqlite/open ":memory:"] # in-memory database # ... or ... def db [sqlite/open "data.db"] # file-backed sqlite/close $db
The handle is a `native_resource`. Closing it releases the
underlying SQLite handle; using a closed handle is an error.
## Exec — DDL and DML
`sqlite/exec` runs SQL that doesn't return rows. Returns the number
of rows changed.
sqlite/exec $db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)" sqlite/exec $db "INSERT INTO users (name, age) VALUES ('Alice', 30)"
### Parameterised exec
Pass parameters as a list after the SQL string. Placeholders are
`?`:
sqlite/exec $db "INSERT INTO users (name, age) VALUES (?, ?)" @{ "Alice" 30 } sqlite/exec $db "INSERT INTO users (name, age) VALUES (?, ?)" @{ "Bob" 25 }
Numbers, floats, strings, and nil all bind correctly. Invalid SQL
returns a `%Err` — check with `err?`:
set r [sqlite/exec $db "INSERT INTO bogus_table VALUES (1)"] if [err? $r] { echo "failed: $r~message" }
## Query — SELECT
`sqlite/query` returns a list of maps keyed by column name:
def rows [sqlite/query $db "SELECT * FROM users"] for row $rows { echo "$row~name (age $row~age)" } # Alice (age 30) # Bob (age 25)
With parameters:
def adults [sqlite/query $db "SELECT * FROM users WHERE age >= ?" @{ 18 }]
NULL cells come through as `nil`.
### `query-one` — single row
`sqlite/query-one` returns the first matching row, or `nil`:
def alice [sqlite/query-one $db "SELECT * FROM users WHERE name = ?" @{ "Alice" }] if $alice { echo "found: $alice~name" }
### `scalar` — single value
`sqlite/scalar` returns the first column of the first row, or `nil`:
def count [sqlite/scalar $db "SELECT COUNT(*) FROM users"] echo "user count: $count"
## Introspection
sqlite/tables $db # => list of table names sqlite/columns $db "users" # => column metadata list
`sqlite/columns` returns rows from `PRAGMA table_info(...)`. Each
row has fields like `:name`, `:type`, `:notnull`, `:pk`.
## Transactions
sqlite/begin $db try { sqlite/exec $db "INSERT INTO users (name, age) VALUES (?, ?)" @{ "Carol" 28 } sqlite/exec $db "UPDATE counters SET value = value + 1 WHERE name = 'users'" sqlite/commit $db } catch e { sqlite/rollback $db echo "rolled back: $e~message" }
Rollback discards everything since the matching `begin`:
sqlite/begin $db sqlite/exec $db "INSERT INTO users (name, age) VALUES ('Temp', 0)" sqlite/rollback $db # The 'Temp' row is gone.
## Pipelines and Transducers
Query results are ordinary lists, so all collection operations work
without ceremony:
# Filter and map results def adults $rows |> filter { ($it~age >= 18) } |> map { $it~name } |> collect # Aggregate set total-age $rows |> map { $it~age } |> reduce 0 { $acc + $it } # Index by name def by-name [from-entries [map $rows {|r| @{ $r~name $r }}]] echo $by-name~Alice~age # => 30
For reusable transforms, factor them into a transducer:
def xf [comp [filter { ($it~age >= 18) }] [map { $r~name }]] [into @{} xf $rows]
See [Collections](../01-Language/collections) and
[Transducers](../01-Language/transducers).
## `runtime/on-shutdown` for Cleanup
Long-running scripts and TUI apps should register a shutdown hook to
close database handles:
def db [sqlite/open "data.db"] runtime/on-shutdown { try { sqlite/close $db } catch _e { nil } }
This pattern is used in `packages/chat/src/chat.dft` and
`packages/stdext/src/semantic.dft` for production DB persistence.
## A Complete Example
#!/usr/bin/env deft def db [sqlite/open ":memory:"] sqlite/exec $db """ CREATE TABLE posts ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, body TEXT ) """ sqlite/exec $db "INSERT INTO posts (title, body) VALUES (?, ?)" @{ "First Post" "This is my first post." } sqlite/exec $db "INSERT INTO posts (title, body) VALUES (?, ?)" @{ "Second Post" "More content here." } def count [sqlite/scalar $db "SELECT COUNT(*) FROM posts"] echo "Total posts: $count" def posts [sqlite/query $db "SELECT * FROM posts ORDER BY id"] # Render via pipeline $posts |> map { " - $it~title" } |> each { echo $it } sqlite/close $db
## Concurrency Notes
SQLite handles are not safe to share across OS threads. Within a
single runtime, this isn't a problem — only one thread touches the
handle. For cross-runtime access, either:
- Open a separate handle per runtime against the same file (SQLite
handles concurrent file access via WAL mode), or
- Use `runtime/eval` to dispatch DB work to a single owning runtime.
## `sqlite/*` Quick Reference
| Function | Returns | Purpose |
|---|---|---|
| `[sqlite/open ":memory:" \| path]` | handle | Open |
| `[sqlite/close $db]` | nil | Close |
| `[sqlite/exec $db sql ?params]` | number | DDL/DML; rows changed |
| `[sqlite/query $db sql ?params]` | list of maps | SELECT |
| `[sqlite/query-one $db sql ?params]` | map or nil | First row |
| `[sqlite/scalar $db sql ?params]` | value or nil | First column of first row |
| `[sqlite/tables $db]` | list | All table names |
| `[sqlite/columns $db "table"]` | list | Column metadata |
| `[sqlite/begin $db]` | nil | Start transaction |
| `[sqlite/commit $db]` | nil | Commit |
| `[sqlite/rollback $db]` | nil | Rollback |