6.1 Extended DB API
| (require toolbox/db/base) | package: toolbox-db-lib |
The toolbox/db/base module improves and extends db/base. In addition to the bindings documented in this section, it re-exports all bindings from db/base (except those that have the same name as one of the bindings documented in this section).
The interface provided by toolbox/db/base is mostly drop-in compatible with that of db/base, with two major exceptions:
Most functions have been changed to no longer require an explicit database connection argument. Instead, the value of the current-db parameter is used.
Nested uses of call-with-transaction do not create nested transactions by default. The #:nested 'allow option must be supplied if true nested transactions are desired.
parameter
(current-db) → (or/c connection? #f)
(current-db db) → void? db : (or/c connection? #f)
= #f
procedure
(get-db [who]) → connection?
who : symbol? = 'get-db
procedure
(in-transaction? [#:db db]) → boolean?
db : connection? = (current-db)
procedure
(call-with-transaction thunk [ #:db db #:isolation isolation-level #:option option #:nested nested-mode]) → any thunk : (-> any) db : connection? = (current-db)
isolation-level :
(or/c 'serializable 'repeatable-read 'read-committed 'read-uncommitted #f) = #f option : any/c = #f nested-mode : (or/c 'allow 'omit 'fail) = 'omit
If nested-mode is 'omit (the default), call-with-transaction has no effect when already inside a transaction: thunk is invoked directly, without starting a nested transaction.
If nested-mode is 'allow, call-with-transaction applies thunk within a nested transaction. (This is the behavior of call-with-transaction from db/base.)
If nested-mode is 'fail, call-with-transaction raises an exn:fail:contract exception if already inside a transaction.
The default value of 'omit makes call-with-transaction ensure that thunk is executed in the context of some transaction, but it does not allow the effects of thunk to be selectively rolled back. In practice, partial rollbacks are rarely useful, and creating savepoints to permit them can have significant performance overhead, so this is usually the right choice.
procedure
(call-with-transaction/retry thunk [ #:db db #:isolation isolation-level #:option option #:nested nested-mode #:max-retries max-retries #:retry-delay retry-delay-secs]) → any thunk : (-> any) db : connection? = (current-db)
isolation-level :
(or/c 'serializable 'repeatable-read 'read-committed 'read-uncommitted #f) = #f option : any/c = #f nested-mode : (or/c 'allow 'omit 'fail) = 'omit
max-retries : (or/c exact-nonnegative-integer? +inf.0) = (current-max-transaction-retries)
retry-delay-secs : (>=/c 0) = (current-transaction-retry-delay)
Assuming retrying is possible, thunk may be executed up to max-retries times before giving up. Before each retry attempt, call-with-transaction/retry sleeps for retry-delay-secs seconds.
When db is a SQLite connection and option is #f or 'deferred, retry attempts will automatically use 'immediate for option, instead. This is usually enough to ensure that thunk itself is only executed at most twice, though the retry limit may still be reached if call-with-transaction/retry is unable to successfully acquire a write transaction.
Note that the retry mechanism of call-with-transaction/retry is used in addition to the retry mechanism used for all SQLite operations, which is controlled separately via the #:busy-retry-limit and #:busy-retry-delay arguments to sqlite3-connect. For multi-statement transactions, the retry mechanism of call-with-transaction/retry is often substantially more useful, as a SQLITE_BUSY failure may indicate that the entire transaction must be restarted, in which case retrying the last statement will never succeed and serves no purpose. However, the intrinsic retry mechanism can be more useful in other situations, especially when the #:use-place argument to sqlite3-connect is 'os-thread, as it can use SQLite’s built-in busy handler.
parameter
→ (or/c exact-nonnegative-integer? +inf.0) (current-max-transaction-retries max-retries) → void? max-retries : (or/c exact-nonnegative-integer? +inf.0)
= 10
parameter
(current-transaction-retry-delay) → (>=/c 0)
(current-transaction-retry-delay retry-delay-secs) → void? retry-delay-secs : (>=/c 0)
= 0.1
procedure
(query stmt arg ... [ #:db db #:log? log? #:explain? explain? #:analyze? analyze?]) → (or/c simple-result? rows-result?) stmt : (or/c string? virtual-statement? prepared-statement?) arg : any/c db : connection? = (current-db) log? : any/c = (current-log-db-queries?) explain? : any/c = (current-explain-db-queries?) analyze? : any/c = (current-analyze-db-queries?)
If log? is not #f, the SQL text of stmt is logged before the query is executed, and the query’s (wall clock) execution time is logged after the execution completes.
If explain? is not #f, a textual representation of the database system’s query plan is logged before the query is executed. Currently, this option is only supported with SQLite; an exn:fail:unsupported exception will be raised with other database systems.
If analyze? is not #f, the query plan is logged in the same way as for explain?, but the plan is logged after executing the query, and it is annotated with performance information collected during the query’s execution. Like explain?, this option is currently only supported with SQLite, but analyze? additionally requires that SQLite was compiled with the SQLITE_ENABLE_STMT_SCANSTATUS compile-time option. The sqlite3-stmt-scanstatus-enabled? function can be used to check whether this is the case.
> (current-db (sqlite3-connect #:database 'memory)) > (define can-analyze? (sqlite3-stmt-scanstatus-enabled?))
> (query #:log? #t #:explain? (not can-analyze?) #:analyze? can-analyze? (string-join '("WITH RECURSIVE" " fib(i,a,b) AS" " (SELECT 1, 0, 1" " UNION ALL" " SELECT i+1, b, a+b FROM fib" " WHERE i <= 10)" "SELECT b FROM fib ORDER BY i") "\n"))
[2025-09-14 16:21:52] [toolbox:db:query/INFO]
WITH RECURSIVE
fib(i,a,b) AS
(SELECT 1, 0, 1
UNION ALL
SELECT i+1, b, a+b FROM fib
WHERE i <= 10)
SELECT b FROM fib ORDER BY i
[2025-09-14 16:21:52] [toolbox:db:query/INFO] 0.2 ms
[2025-09-14 16:21:52] [toolbox:db:query/INFO] QUERY PLAN [cycles=255,578]
├╴CO-ROUTINE fib
│ ├╴SETUP
│ │ ╰╴SCAN CONSTANT ROW
│ ╰╴RECURSIVE STEP
│ ╰╴SCAN fib 3.6% [est=983,040.0 actual=1.0 loops=11 rows=11 cycles=9,270]
├╴SCAN fib [est=122,880.0 actual=11.0 loops=1 rows=11]
╰╴USE TEMP B-TREE FOR ORDER BY 5.7% [est=1.0 actual=11.0 loops=1 rows=11 cycles=14,549]
(rows-result
'(((name . "b") (decltype . #f)))
'(#(1) #(1) #(2) #(3) #(5) #(8) #(13) #(21) #(34) #(55) #(89)))
procedure
(query-exec stmt arg ... [ #:db db #:log? log? #:explain? explain? #:analyze? analyze?]) → void? stmt : (or/c string? virtual-statement? prepared-statement?) arg : any/c db : connection? = (current-db) log? : any/c = (current-log-db-queries?) explain? : any/c = (current-explain-db-queries?) analyze? : any/c = (current-analyze-db-queries?)
procedure
(query-rows stmt arg ... [ #:db db #:group groupings #:group-mode group-mode #:log? log? #:explain? explain? #:analyze? analyze?]) → (listof vector?) stmt : (or/c string? virtual-statement? prepared-statement?) arg : any/c db : connection? = (current-db)
groupings :
(let* ([field/c (or/c string? exact-nonnegative-integer?)] [grouping/c (or/c field/c (vectorof field/c))]) (or/c grouping/c (listof grouping/c))) = '() group-mode : (listof (or/c 'preserve-null 'list)) = '() log? : any/c = (current-log-db-queries?) explain? : any/c = (current-explain-db-queries?) analyze? : any/c = (current-analyze-db-queries?)
procedure
(query-list stmt arg ... [ #:db db #:log? log? #:explain? explain? #:analyze? analyze?]) → list? stmt : (or/c string? virtual-statement? prepared-statement?) arg : any/c db : connection? = (current-db) log? : any/c = (current-log-db-queries?) explain? : any/c = (current-explain-db-queries?) analyze? : any/c = (current-analyze-db-queries?)
procedure
(query-row stmt arg ... [ #:db db #:log? log? #:explain? explain? #:analyze? analyze?]) → vector? stmt : (or/c string? virtual-statement? prepared-statement?) arg : any/c db : connection? = (current-db) log? : any/c = (current-log-db-queries?) explain? : any/c = (current-explain-db-queries?) analyze? : any/c = (current-analyze-db-queries?)
procedure
(query-maybe-row stmt arg ... [ #:db db #:log? log? #:explain? explain? #:analyze? analyze?]) → (or/c vector? #f) stmt : (or/c string? virtual-statement? prepared-statement?) arg : any/c db : connection? = (current-db) log? : any/c = (current-log-db-queries?) explain? : any/c = (current-explain-db-queries?) analyze? : any/c = (current-analyze-db-queries?)
procedure
(query-value stmt arg ... [ #:db db #:log? log? #:explain? explain? #:analyze? analyze?]) → any/c stmt : (or/c string? virtual-statement? prepared-statement?) arg : any/c db : connection? = (current-db) log? : any/c = (current-log-db-queries?) explain? : any/c = (current-explain-db-queries?) analyze? : any/c = (current-analyze-db-queries?)
procedure
(query-maybe-value stmt arg ... [ #:db db #:log? log? #:explain? explain? #:analyze? analyze?]) → any/c stmt : (or/c string? virtual-statement? prepared-statement?) arg : any/c db : connection? = (current-db) log? : any/c = (current-log-db-queries?) explain? : any/c = (current-explain-db-queries?) analyze? : any/c = (current-analyze-db-queries?)
parameter
(current-log-db-queries? log?) → void? log? : any/c
= #f
parameter
(current-explain-db-queries? explain?) → void? explain? : any/c
= #f
parameter
(current-analyze-db-queries? analyze?) → void? analyze? : any/c
= #f
procedure
(query-changes [#:db db]) → exact-nonnegative-integer?
db : connection? = (current-db)
> (map-sql-nullable add1 1) 2
> (map-sql-nullable add1 sql-null) #<sql-null>
Also see ~stmt, which combines lifted-statement and ~sql.
value
procedure
(exn:fail:sql:busy? v) → boolean?
v : any/c
procedure
v : any/c