SQLite¶
(kaappi sqlite) — SQLite database client with cursors and transactions.
Requires: libsqlite3 (macOS: included, Linux: apt install libsqlite3-dev)
For a task-oriented walkthrough, see the cookbook recipe Store Data in SQLite.
Quick start¶
(import (kaappi sqlite))
(call-with-sqlite ":memory:"
(lambda (db)
(sqlite-exec db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
(sqlite-exec db "INSERT INTO users (name) VALUES (?)" "Alice")
(sqlite-query db "SELECT * FROM users")))
;=> (#(1 "Alice"))
Connection¶
(sqlite-open path) ;; ":memory:" for in-memory database
(sqlite-close conn)
(sqlite-open? conn)
(sqlite-error-message conn) ;; last error message string
Auto-closing connection¶
(call-with-sqlite "app.db"
(lambda (db)
(sqlite-query db "SELECT count(*) FROM users")))
;; connection is closed automatically when the lambda returns
call-with-sqlite also accepts an existing open connection — it passes it
through without closing.
Queries¶
Convenience functions¶
For most use cases, sqlite-query and sqlite-exec are all you need:
;; SELECT — returns list of row vectors
(sqlite-query db "SELECT * FROM users WHERE age > ?" 25)
;=> (#(1 "Alice" 30) #(2 "Bob" 28))
;; INSERT/UPDATE/DELETE — returns number of affected rows
(sqlite-exec db "INSERT INTO users (name, age) VALUES (?, ?)" "Carol" 22)
;=> 1
(sqlite-exec db "DELETE FROM users WHERE age < ?" 25)
;=> 1
;; Last inserted row ID
(sqlite-last-insert-id db)
;=> 3
Parameterized queries¶
Use ? placeholders. Types are preserved:
| Scheme value | SQLite type |
|---|---|
| exact integer | INTEGER |
| inexact number | REAL |
| string | TEXT |
#f |
NULL |
Reading results¶
Rows are returned as vectors. Column values are converted automatically:
| SQLite type | Scheme value |
|---|---|
| INTEGER | exact integer |
| REAL | inexact number |
| TEXT | string |
| NULL | #f |
| BLOB | string |
(define rows (sqlite-query db "SELECT id, name, score FROM scores"))
;; rows = (#(1 "Alice" 95.5) #(2 "Bob" 87.0))
(vector-ref (car rows) 0) ;=> 1 (integer)
(vector-ref (car rows) 1) ;=> "Alice" (string)
(vector-ref (car rows) 2) ;=> 95.5 (inexact)
Cursors¶
For streaming results or fine-grained control:
(define cur (sqlite-cursor db))
(sqlite-execute cur "SELECT * FROM large_table")
;; One row at a time
(sqlite-fetchone cur) ;=> #(1 "Alice") or #f at end
;; All remaining rows
(sqlite-fetchall cur) ;=> list of vectors
;; Column names
(sqlite-description cur) ;=> (("id") ("name") ("age"))
;; Rows affected by last execute
(sqlite-rowcount cur) ;=> number
;; Free resources
(sqlite-cursor-close cur)
Streaming large results¶
Process rows one at a time without loading everything into memory:
(define cur (sqlite-cursor db))
(sqlite-execute cur "SELECT * FROM events ORDER BY timestamp")
(let loop ((row (sqlite-fetchone cur)))
(when row
(process-event row)
(loop (sqlite-fetchone cur))))
(sqlite-cursor-close cur)
Transactions¶
Automatic transactions¶
call-with-sqlite-transaction commits on success, rolls back on error:
(call-with-sqlite-transaction db
(lambda ()
(sqlite-exec db "UPDATE accounts SET balance = balance - 100 WHERE id = ?" 1)
(sqlite-exec db "UPDATE accounts SET balance = balance + 100 WHERE id = ?" 2)))
;; Both updates succeed or both are rolled back
If the lambda raises an error, the transaction is rolled back and the error is re-raised.
Manual transactions¶
(sqlite-exec db "BEGIN")
(sqlite-exec db "INSERT INTO log (msg) VALUES (?)" "step 1")
(sqlite-exec db "INSERT INTO log (msg) VALUES (?)" "step 2")
(sqlite-exec db "COMMIT")
;; or (sqlite-exec db "ROLLBACK") to cancel
Common patterns¶
Create table if not exists¶
(sqlite-exec db "CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at TEXT DEFAULT (datetime('now'))
)")
Upsert¶
(sqlite-exec db "INSERT INTO config (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value"
"theme" "dark")
Count rows¶
Check if a row exists¶
Batch inserts in a transaction¶
(call-with-sqlite-transaction db
(lambda ()
(for-each
(lambda (user)
(sqlite-exec db "INSERT INTO users (name) VALUES (?)" user))
'("Alice" "Bob" "Carol" "Dave"))))
Wrapping batch inserts in a transaction is much faster than individual commits — SQLite commits once instead of once per row.
API reference¶
Connection¶
| Procedure | Description |
|---|---|
(sqlite-open path) |
Open database |
(sqlite-close conn) |
Close connection |
(sqlite-open? conn) |
Check if open |
(sqlite-error-message conn) |
Last error message |
(call-with-sqlite path proc) |
Auto-closing connection |
Convenience¶
| Procedure | Description |
|---|---|
(sqlite-exec conn sql param ...) |
Execute, return affected row count |
(sqlite-query conn sql param ...) |
Execute, return all rows as vectors |
(sqlite-last-insert-id conn) |
Last inserted ROWID |
Cursor¶
| Procedure | Description |
|---|---|
(sqlite-cursor conn) |
Create cursor |
(sqlite-execute cur sql param ...) |
Execute with parameters |
(sqlite-fetchone cur) |
Next row as vector, #f at end |
(sqlite-fetchall cur) |
All remaining rows |
(sqlite-description cur) |
Column names |
(sqlite-rowcount cur) |
Rows affected |
(sqlite-cursor-close cur) |
Free resources |
Transactions¶
| Procedure | Description |
|---|---|
(call-with-sqlite-transaction conn proc) |
Auto-commit/rollback |