nim-datastore/datastore/sql/sqliteutils.nim

270 lines
6.8 KiB
Nim
Raw Permalink Normal View History

{.push raises: [].}
2022-05-11 10:50:05 -05:00
import pkg/questionable
import pkg/questionable/results
import pkg/sqlite3_abi
2022-09-16 21:14:31 -06:00
export sqlite3_abi
2022-05-11 10:50:05 -05:00
# 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`
2023-03-09 10:24:44 +01:00
{.passc: "-DSQLITE_ENABLE_COLUMN_METADATA".}
2022-05-11 10:50:05 -05:00
type
AutoDisposed*[T: ptr|ref] = object
val*: T
DataProc* = proc(s: RawStmtPtr) {.closure, gcsafe, raises: [].}
2022-05-11 10:50:05 -05:00
NoParams* = tuple # empty tuple
NoParamsStmt* = SQLiteStmt[NoParams, void]
RawStmtPtr* = ptr sqlite3_stmt
SQLite* = ptr sqlite3
SQLiteStmt*[Params, Res] = distinct RawStmtPtr
2022-07-15 15:28:42 -05:00
# see https://github.com/arnetheduck/nim-sqlite3-abi/issues/4
sqlite3_destructor_type_gcsafe =
proc (a1: pointer) {.cdecl, gcsafe, raises: [].}
2022-07-15 15:28:42 -05:00
const
SQLITE_TRANSIENT_GCSAFE* = cast[sqlite3_destructor_type_gcsafe](-1)
2022-05-11 10:50:05 -05:00
proc bindParam(
s: RawStmtPtr,
n: int,
val: auto): cint =
2023-03-09 10:24:44 +01:00
when val is openArray[byte]|seq[byte]:
2022-05-11 10:50:05 -05:00
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)
2022-05-11 10:50:05 -05:00
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)
2022-05-11 10:50:05 -05:00
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:
2022-09-16 21:14:31 -06:00
var i = 1
2022-05-11 10:50:05 -05:00
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 dispose*(rawStmt: RawStmtPtr) =
doAssert SQLITE_OK == sqlite3_finalize(rawStmt)
rawStmt = nil
2022-05-11 10:50:05 -05:00
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 prepare*(
env: SQLite,
q: string,
prepFlags: cuint = 0): RawStmtPtr =
var
s: RawStmtPtr
checkErr sqlite3_prepare_v3(
env, q.cstring, q.len.cint, prepFlags, addr s, nil)
s
2022-05-11 10:50:05 -05:00
template checkExec*(env: SQLite, q: string) =
var
2022-05-11 10:50:05 -05:00
s = prepare(env, q)
checkExec(s)
template dispose*(db: SQLite) =
# TODO: the assert bellow fails because we're
# not releasing all the statements at the time of
# releasing the connection. I suspect these are the
# query iterators that aren't being released on close
# doAssert SQLITE_OK == sqlite3_close(db)
2022-05-11 10:50:05 -05:00
discard sqlite3_close(db)
template dispose*(sqliteStmt: SQLiteStmt) =
doAssert SQLITE_OK == sqlite3_finalize(RawStmtPtr(sqliteStmt))
# nil literals can no longer be directly assigned to variables or fields of distinct pointer types.
# They must be converted instead.
# See https://nim-lang.org/blog/2022/12/21/version-20-rc.html#:~:text=nil%20literals%20can%20no%20longer%20be%20directly%20assigned%20to%20variables%20or%20fields%20of%20distinct%20pointer%20types.%20They%20must%20be%20converted%20instead.
# SQLiteStmt(nil) is generating a SIGSEGV, so we need to cast it
sqliteStmt = cast[typeof sqliteStmt](nil)
2022-05-11 10:50:05 -05:00
proc release*[T](x: var AutoDisposed[T]): T =
result = x.val
x.val = nil
2022-05-11 10:50:05 -05:00
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 =
2022-05-11 10:50:05 -05:00
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 implicit transaction
2022-05-11 10:50:05 -05:00
discard sqlite3_reset(s) # same return information as step
discard sqlite3_clear_bindings(s) # no errors possible
res
proc sqlite3_column_text_not_null*(
s: RawStmtPtr,
index: cint): cstring =
let
text = sqlite3_column_text(s, index).cstring
if text.isNil:
# see the conversion table and final paragraph of:
# https://www.sqlite.org/c3ref/column_blob.html
# a null pointer here implies an out-of-memory error
let
v = sqlite3_errcode(sqlite3_db_handle(s))
raise (ref Defect)(msg: $sqlite3_errstr(v))
text
2022-05-11 10:50:05 -05:00
template journalModePragmaStmt*(env: SQLite): RawStmtPtr =
var
2022-05-11 10:50:05 -05:00
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_not_null(s, 0)
if not (x in ["memory", "wal"]):
2022-05-11 10:50:05 -05:00
s.dispose
return failure "Invalid pragma result: \"" & x & "\""
2022-05-11 10:50:05 -05:00
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,
prepFlags: cuint = 0): ?!T =
2022-05-11 10:50:05 -05:00
var
s: RawStmtPtr
checkErr sqlite3_prepare_v3(
env, stmt.cstring, stmt.len.cint, prepFlags, addr s, nil)
2022-05-11 10:50:05 -05:00
success T(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 =
var s = ? NoParamsStmt.prepare(env, query)
2022-05-11 10:50:05 -05:00
var res = s.query((), onData)
2022-05-11 10:50:05 -05:00
# NB: dispose of the prepared query statement and free associated memory
s.dispose
return res
2022-05-11 10:50:05 -05:00