mirror of
https://github.com/logos-storage/nim-datastore.git
synced 2026-01-02 21:53:05 +00:00
The goal is to detect mismatches between caller-supplied indexes and original column names, and in that case crash the process by raising Defect. This should help avoid harder to debug errors for such mismatches. These helper procs are now higher-order, which allows column metadata checks to be run only once, i.e. when setting up derivative helpers to be used in an `onData` callback. Use compile-time constants for column names and default indexes. Adjust callback proc annotations to be more precise, and remove unnecessary annotations.
248 lines
5.8 KiB
Nim
248 lines
5.8 KiB
Nim
import pkg/questionable
|
|
import pkg/questionable/results
|
|
import pkg/sqlite3_abi
|
|
import pkg/upraises
|
|
|
|
# Adapted from:
|
|
# https://github.com/status-im/nwaku/blob/master/waku/v2/node/storage/sqlite.nim
|
|
|
|
# see https://www.sqlite.org/c3ref/column_database_name.html
|
|
# can pass `--forceBuild:on` to the Nim compiler if a SQLite build without
|
|
# `-DSQLITE_ENABLE_COLUMN_METADATA` option is stuck in the build cache,
|
|
# e.g. `nimble test --forceBuild:on`
|
|
{.passC: "-DSQLITE_ENABLE_COLUMN_METADATA".}
|
|
|
|
push: {.upraises: [].}
|
|
|
|
type
|
|
AutoDisposed*[T: ptr|ref] = object
|
|
val*: T
|
|
|
|
DataProc* = proc(s: RawStmtPtr) {.closure, gcsafe.}
|
|
|
|
NoParams* = tuple # empty tuple
|
|
|
|
NoParamsStmt* = SQLiteStmt[NoParams, void]
|
|
|
|
RawStmtPtr* = ptr sqlite3_stmt
|
|
|
|
SQLite* = ptr sqlite3
|
|
|
|
SQLiteStmt*[Params, Res] = distinct RawStmtPtr
|
|
|
|
proc bindParam(
|
|
s: RawStmtPtr,
|
|
n: int,
|
|
val: auto): cint =
|
|
|
|
when val is openarray[byte]|seq[byte]:
|
|
if val.len > 0:
|
|
# `SQLITE_TRANSIENT` "indicate[s] that the object is to be copied prior
|
|
# to the return from sqlite3_bind_*(). The object and pointer to it
|
|
# must remain valid until then. SQLite will then manage the lifetime of
|
|
# its private copy."
|
|
sqlite3_bind_blob(s, n.cint, unsafeAddr val[0], val.len.cint,
|
|
SQLITE_TRANSIENT)
|
|
else:
|
|
sqlite3_bind_null(s, n.cint)
|
|
elif val is int32:
|
|
sqlite3_bind_int(s, n.cint, val)
|
|
elif val is uint32 | int64:
|
|
sqlite3_bind_int64(s, n.cint, val.int64)
|
|
elif val is float32 | float64:
|
|
sqlite3_bind_double(s, n.cint, val.float64)
|
|
elif val is string:
|
|
# `-1` implies string length is num bytes up to first null-terminator;
|
|
# `SQLITE_TRANSIENT` "indicate[s] that the object is to be copied prior
|
|
# to the return from sqlite3_bind_*(). The object and pointer to it must
|
|
# remain valid until then. SQLite will then manage the lifetime of its
|
|
# private copy."
|
|
sqlite3_bind_text(s, n.cint, val.cstring, -1.cint, SQLITE_TRANSIENT)
|
|
else:
|
|
{.fatal: "Please add support for the '" & $typeof(val) & "' type".}
|
|
|
|
template bindParams(
|
|
s: RawStmtPtr,
|
|
params: auto) =
|
|
|
|
when params is tuple:
|
|
when params isnot NoParams:
|
|
var
|
|
i = 1
|
|
|
|
for param in fields(params):
|
|
checkErr bindParam(s, i, param)
|
|
inc i
|
|
|
|
else:
|
|
checkErr bindParam(s, 1, params)
|
|
|
|
template checkErr*(op: untyped) =
|
|
if (let v = (op); v != SQLITE_OK):
|
|
return failure $sqlite3_errstr(v)
|
|
|
|
template checkExec*(s: RawStmtPtr) =
|
|
if (let x = sqlite3_step(s); x != SQLITE_DONE):
|
|
s.dispose
|
|
return failure $sqlite3_errstr(x)
|
|
|
|
if (let x = sqlite3_finalize(s); x != SQLITE_OK):
|
|
return failure $sqlite3_errstr(x)
|
|
|
|
template checkExec*(env: SQLite, q: string) =
|
|
let
|
|
s = prepare(env, q)
|
|
|
|
checkExec(s)
|
|
|
|
template dispose*(db: SQLite) =
|
|
discard sqlite3_close(db)
|
|
|
|
template dispose*(rawStmt: RawStmtPtr) =
|
|
discard sqlite3_finalize(rawStmt)
|
|
|
|
template dispose*(sqliteStmt: SQLiteStmt) =
|
|
discard sqlite3_finalize(RawStmtPtr(sqliteStmt))
|
|
|
|
proc disposeIfUnreleased*[T](x: var AutoDisposed[T]) =
|
|
mixin dispose
|
|
if x.val != nil: dispose(x.release)
|
|
|
|
proc exec*[P](
|
|
s: SQLiteStmt[P, void],
|
|
params: P): ?!void =
|
|
|
|
let
|
|
s = RawStmtPtr(s)
|
|
|
|
bindParams(s, params)
|
|
|
|
let
|
|
res =
|
|
if (let v = sqlite3_step(s); v != SQLITE_DONE):
|
|
failure $sqlite3_errstr(v)
|
|
else:
|
|
success()
|
|
|
|
# release implict transaction
|
|
discard sqlite3_reset(s) # same return information as step
|
|
discard sqlite3_clear_bindings(s) # no errors possible
|
|
|
|
res
|
|
|
|
template journalModePragmaStmt*(env: SQLite): RawStmtPtr =
|
|
let
|
|
s = prepare(env, "PRAGMA journal_mode = WAL;")
|
|
|
|
if (let x = sqlite3_step(s); x != SQLITE_ROW):
|
|
s.dispose
|
|
return failure $sqlite3_errstr(x)
|
|
|
|
if (let x = sqlite3_column_type(s, 0); x != SQLITE3_TEXT):
|
|
s.dispose
|
|
return failure $sqlite3_errstr(x)
|
|
|
|
let
|
|
x = sqlite3_column_text(s, 0).cstring
|
|
|
|
# detect out-of-memory error
|
|
# see the conversion table and final paragraph of:
|
|
# https://www.sqlite.org/c3ref/column_blob.html
|
|
# see also https://www.sqlite.org/rescode.html
|
|
|
|
# in order to detect an out-of-memory error check that the result is a null
|
|
# pointer and that the result code is an error code
|
|
if x.isNil:
|
|
let
|
|
code = sqlite3_errcode(sqlite3_db_handle(s))
|
|
|
|
if not (code in [SQLITE_OK, SQLITE_ROW, SQLITE_DONE]):
|
|
raise (ref Defect)(msg: $sqlite3_errstr(code))
|
|
|
|
if not ($x in ["memory", "wal"]):
|
|
s.dispose
|
|
return failure "Invalid pragma result: \"" & $x & "\""
|
|
|
|
s
|
|
|
|
template open*(
|
|
dbPath: string,
|
|
env: var SQLite,
|
|
flags = 0) =
|
|
|
|
checkErr sqlite3_open_v2(dbPath.cstring, addr env, flags.cint, nil)
|
|
|
|
proc prepare*[Params, Res](
|
|
T: type SQLiteStmt[Params, Res],
|
|
env: SQLite,
|
|
stmt: string): ?!T =
|
|
|
|
var
|
|
s: RawStmtPtr
|
|
|
|
checkErr sqlite3_prepare_v2(env, stmt.cstring, stmt.len.cint, addr s, nil)
|
|
|
|
success T(s)
|
|
|
|
template prepare*(
|
|
env: SQLite,
|
|
q: string): RawStmtPtr =
|
|
|
|
var
|
|
s: RawStmtPtr
|
|
|
|
checkErr sqlite3_prepare_v2(env, q.cstring, q.len.cint, addr s, nil)
|
|
|
|
s
|
|
|
|
proc query*[P](
|
|
s: SQLiteStmt[P, void],
|
|
params: P,
|
|
onData: DataProc): ?!bool =
|
|
|
|
let
|
|
s = RawStmtPtr(s)
|
|
|
|
bindParams(s, params)
|
|
|
|
var
|
|
res = success false
|
|
|
|
while true:
|
|
let
|
|
v = sqlite3_step(s)
|
|
|
|
case v
|
|
of SQLITE_ROW:
|
|
onData(s)
|
|
res = success true
|
|
of SQLITE_DONE:
|
|
break
|
|
else:
|
|
res = failure $sqlite3_errstr(v)
|
|
break
|
|
|
|
# release implict transaction
|
|
discard sqlite3_reset(s) # same return information as step
|
|
discard sqlite3_clear_bindings(s) # no errors possible
|
|
|
|
res
|
|
|
|
proc query*(
|
|
env: SQLite,
|
|
query: string,
|
|
onData: DataProc): ?!bool =
|
|
|
|
let
|
|
s = ? NoParamsStmt.prepare(env, query)
|
|
res = s.query((), onData)
|
|
|
|
# NB: dispose of the prepared query statement and free associated memory
|
|
s.dispose
|
|
|
|
res
|
|
|
|
proc release*[T](x: var AutoDisposed[T]): T =
|
|
result = x.val
|
|
x.val = nil
|