Richer Sqlite API

* Adds a strongly typed API for creating custom SQL queries and
  executing them

* Uses destructors to simplify the error handling in the init logic
This commit is contained in:
Zahary Karadjov 2020-10-13 21:44:42 +03:00
parent c103721391
commit cc0d15ccac
No known key found for this signature in database
GPG Key ID: C8936F8A3073D609
2 changed files with 247 additions and 23 deletions

View File

@ -10,14 +10,42 @@ import
export kvstore export kvstore
type type
Sqlite3Ptr* = ptr sqlite3 RawStmtPtr = ptr sqlite3_stmt
AutoDisposed[T: ptr|ref] = object
val: T
# TODO: These should become AutoDisposed
# This is currently considered risky due to the destructor
# problem found in FastStreams (triggered when objects in
# the GC heap have destructors)
Sqlite* = ptr sqlite3
SqliteStmt*[Params; Result] = distinct RawStmtPtr
NoParams* = tuple # this is the empty tuple
ResultHandler*[T] = proc(val: T) {.gcsafe, raises: [Defect].}
KeySpaceStatements = object KeySpaceStatements = object
getStmt, putStmt, delStmt, containsStmt: ptr sqlite3_stmt getStmt, putStmt, delStmt, containsStmt: RawStmtPtr
SqStoreRef* = ref object of RootObj SqStoreRef* = ref object of RootObj
env: Sqlite3Ptr env: Sqlite
keyspaces: seq[KeySpaceStatements] keyspaces: seq[KeySpaceStatements]
extraStmts: seq[RawStmtPtr]
template dispose(db: Sqlite) =
discard sqlite3_close(db)
template dispose(db: RawStmtPtr) =
discard sqlite3_finalize(db)
proc release[T](x: var AutoDisposed[T]): T =
result = x.val
x.val = nil
proc `=destroy`*[T](x: var AutoDisposed[T]) =
mixin dispose
if x.val != nil:
dispose(x.release)
template checkErr(op, cleanup: untyped) = template checkErr(op, cleanup: untyped) =
if (let v = (op); v != SQLITE_OK): if (let v = (op); v != SQLITE_OK):
@ -27,15 +55,129 @@ template checkErr(op, cleanup: untyped) =
template checkErr(op) = template checkErr(op) =
checkErr(op): discard checkErr(op): discard
proc bindBlob(s: ptr sqlite3_stmt, n: int, blob: openarray[byte]): cint = proc prepareStmt*(db: SqStoreRef,
sqlite3_bind_blob(s, n.cint, unsafeAddr blob[0], blob.len.cint, nil) stmt: string,
Params: type,
Res: type): KvResult[SqliteStmt[Params, Res]] =
var s: RawStmtPtr
checkErr sqlite3_prepare_v2(db.env, stmt, stmt.len.cint, addr s, nil)
ok SqliteStmt[Params, Res](s)
proc bindParam(s: RawStmtPtr, n: int, val: auto): cint =
when val is openarray[byte]|seq[byte]:
if val.len > 0:
sqlite3_bind_blob(s, n.cint, unsafeAddr val[0], val.len.cint, nil)
else:
sqlite3_bind_blob(s, n.cint, nil, 0.cint, nil)
elif val is int32:
sqlite3_bind_int(s, n.cint, val)
elif val is int64:
sqlite3_bind_int64(s, n.cint, val)
else:
{.fatal: "Please add support for the '" & $(T) & "' type".}
template bindParams(s: RawStmtPtr, params: auto) =
when params is tuple:
var i = 1
for param in fields(params):
checkErr bindParam(s, i, param)
inc i
else:
checkErr bindParam(s, 1, params)
proc exec*[P](s: SqliteStmt[P, void], params: P): KvResult[void] =
let s = RawStmtPtr s
bindParams(s, params)
let res =
if (let v = sqlite3_step(s); v != SQLITE_DONE):
err($sqlite3_errstr(v))
else:
ok()
# release implict transaction
discard sqlite3_reset(s) # same return information as step
discard sqlite3_clear_bindings(s) # no errors possible
res
template readResult(s: RawStmtPtr, column: cint, T: type): auto =
when T is int32:
sqlite3_column_int(s, column)
elif T is int64:
sqlite3_column_int64(s, column)
elif T is int:
{.fatal: "Please use specify either int32 or int64 precisely".}
elif T is openarray[byte]:
let
p = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, column))
l = sqlite3_column_bytes(s, column)
toOpenArray(p, 0, l-1)
elif T is seq[byte]:
var res: seq[byte]
let len = sqlite3_column_bytes(s, column)
if len > 0:
res.setLen(len)
copyMem(addr res[0], sqlite3_column_blob(s, column), len)
res
else:
{.fatal: "Please add support for the '" & $(T) & "' type".}
template readResult(s: RawStmtPtr, T: type): auto =
when T is tuple:
var res: T
var i = cint 0
for field in fields(res):
field = readResult(s, i, typeof(field))
inc i
res
else:
readResult(s, 0.cint, T)
proc exec*[Params, Res](s: SqliteStmt[Params, Res],
params: Params,
onData: ResultHandler[Res]): KvResult[bool] =
let s = RawStmtPtr s
bindParams(s, params)
try:
var gotResults = false
while true:
let v = sqlite3_step(s)
case v
of SQLITE_ROW:
onData(readResult(s, Res))
gotResults = true
of SQLITE_DONE:
break
else:
return err($sqlite3_errstr(v))
return ok gotResults
finally:
# release implicit transaction
discard sqlite3_reset(s) # same return information as step
discard sqlite3_clear_bindings(s) # no errors possible
template exec*(s: SqliteStmt[NoParams, void]): KvResult[void] =
exec(s, ())
template exec*[Res](s: SqliteStmt[NoParams, Res],
onData: ResultHandler[Res]): KvResult[bool] =
exec(s, (), onData)
proc exec*(db: SqStoreRef, stmt: string): KvResult[void] =
let stmt = ? db.prepareStmt(stmt, NoParams, void)
result = exec(stmt)
let finalizeStatus = sqlite3_finalize(RawStmtPtr stmt)
if finalizeStatus != SQLITE_OK and result.isOk:
return err($sqlite3_errstr(finalizeStatus))
proc getImpl(db: SqStoreRef, proc getImpl(db: SqStoreRef,
keyspace: int, keyspace: int,
key: openarray[byte], key: openarray[byte],
onData: DataProc): KvResult[bool] = onData: DataProc): KvResult[bool] =
let getStmt = db.keyspaces[keyspace].getStmt let getStmt = db.keyspaces[keyspace].getStmt
checkErr bindBlob(getStmt, 1, key) checkErr bindParam(getStmt, 1, key)
let let
v = sqlite3_step(getStmt) v = sqlite3_step(getStmt)
@ -66,8 +208,8 @@ template get*(db: SqStoreRef, keyspace: int, key: openarray[byte], onData: DataP
proc putImpl(db: SqStoreRef, keyspace: int, key, value: openarray[byte]): KvResult[void] = proc putImpl(db: SqStoreRef, keyspace: int, key, value: openarray[byte]): KvResult[void] =
let putStmt = db.keyspaces[keyspace].putStmt let putStmt = db.keyspaces[keyspace].putStmt
checkErr bindBlob(putStmt, 1, key) checkErr bindParam(putStmt, 1, key)
checkErr bindBlob(putStmt, 2, value) checkErr bindParam(putStmt, 2, value)
let res = let res =
if (let v = sqlite3_step(putStmt); v != SQLITE_DONE): if (let v = sqlite3_step(putStmt); v != SQLITE_DONE):
@ -89,7 +231,7 @@ template put*(db: SqStoreRef, keyspace: int, key, value: openarray[byte]): KvRes
proc containsImpl(db: SqStoreRef, keyspace: int, key: openarray[byte]): KvResult[bool] = proc containsImpl(db: SqStoreRef, keyspace: int, key: openarray[byte]): KvResult[bool] =
let containsStmt = db.keyspaces[keyspace].containsStmt let containsStmt = db.keyspaces[keyspace].containsStmt
checkErr bindBlob(containsStmt, 1, key) checkErr bindParam(containsStmt, 1, key)
let let
v = sqlite3_step(containsStmt) v = sqlite3_step(containsStmt)
@ -112,7 +254,7 @@ template contains*(db: SqStoreRef, keyspace: int, key: openarray[byte]): KvResul
proc delImpl(db: SqStoreRef, keyspace: int, key: openarray[byte]): KvResult[void] = proc delImpl(db: SqStoreRef, keyspace: int, key: openarray[byte]): KvResult[void] =
let delStmt = db.keyspaces[keyspace].delStmt let delStmt = db.keyspaces[keyspace].delStmt
checkErr bindBlob(delStmt, 1, key) checkErr bindParam(delStmt, 1, key)
let res = let res =
if (let v = sqlite3_step(delStmt); v != SQLITE_DONE): if (let v = sqlite3_step(delStmt); v != SQLITE_DONE):
@ -139,10 +281,16 @@ proc close*(db: SqStoreRef) =
discard sqlite3_finalize(keyspace.delStmt) discard sqlite3_finalize(keyspace.delStmt)
discard sqlite3_finalize(keyspace.containsStmt) discard sqlite3_finalize(keyspace.containsStmt)
for stmt in db.extraStmts:
discard sqlite3_finalize(stmt)
discard sqlite3_close(db.env) discard sqlite3_close(db.env)
db[] = SqStoreRef()[] db[] = SqStoreRef()[]
proc isClosed*(db: SqStoreRef): bool =
db.env != nil
proc init*( proc init*(
T: type SqStoreRef, T: type SqStoreRef,
basePath: string, basePath: string,
@ -150,8 +298,7 @@ proc init*(
readOnly = false, readOnly = false,
inMemory = false, inMemory = false,
keyspaces: openarray[string] = ["kvstore"]): KvResult[T] = keyspaces: openarray[string] = ["kvstore"]): KvResult[T] =
var var env: AutoDisposed[ptr sqlite3]
env: ptr sqlite3
let let
name = name =
@ -167,23 +314,20 @@ proc init*(
except OSError, IOError: except OSError, IOError:
return err("`sqlite: cannot create database directory") return err("`sqlite: cannot create database directory")
checkErr sqlite3_open_v2(name, addr env, flags.cint, nil) checkErr sqlite3_open_v2(name, addr env.val, flags.cint, nil)
template prepare(q: string, cleanup: untyped): ptr sqlite3_stmt = template prepare(q: string, cleanup: untyped): ptr sqlite3_stmt =
var s: ptr sqlite3_stmt var s: ptr sqlite3_stmt
checkErr sqlite3_prepare_v2(env, q, q.len.cint, addr s, nil): checkErr sqlite3_prepare_v2(env.val, q, q.len.cint, addr s, nil):
cleanup cleanup
discard sqlite3_close(env)
s s
template checkExec(s: ptr sqlite3_stmt) = template checkExec(s: ptr sqlite3_stmt) =
if (let x = sqlite3_step(s); x != SQLITE_DONE): if (let x = sqlite3_step(s); x != SQLITE_DONE):
discard sqlite3_finalize(s) discard sqlite3_finalize(s)
discard sqlite3_close(env)
return err($sqlite3_errstr(x)) return err($sqlite3_errstr(x))
if (let x = sqlite3_finalize(s); x != SQLITE_OK): if (let x = sqlite3_finalize(s); x != SQLITE_OK):
discard sqlite3_close(env)
return err($sqlite3_errstr(x)) return err($sqlite3_errstr(x))
template checkExec(q: string) = template checkExec(q: string) =
@ -193,18 +337,15 @@ proc init*(
template checkWalPragmaResult(journalModePragma: ptr sqlite3_stmt) = template checkWalPragmaResult(journalModePragma: ptr sqlite3_stmt) =
if (let x = sqlite3_step(journalModePragma); x != SQLITE_ROW): if (let x = sqlite3_step(journalModePragma); x != SQLITE_ROW):
discard sqlite3_finalize(journalModePragma) discard sqlite3_finalize(journalModePragma)
discard sqlite3_close(env)
return err($sqlite3_errstr(x)) return err($sqlite3_errstr(x))
if (let x = sqlite3_column_type(journalModePragma, 0); x != SQLITE3_TEXT): if (let x = sqlite3_column_type(journalModePragma, 0); x != SQLITE3_TEXT):
discard sqlite3_finalize(journalModePragma) discard sqlite3_finalize(journalModePragma)
discard sqlite3_close(env)
return err($sqlite3_errstr(x)) return err($sqlite3_errstr(x))
if (let x = sqlite3_column_text(journalModePragma, 0); if (let x = sqlite3_column_text(journalModePragma, 0);
x != "memory" and x != "wal"): x != "memory" and x != "wal"):
discard sqlite3_finalize(journalModePragma) discard sqlite3_finalize(journalModePragma)
discard sqlite3_close(env)
return err("Invalid pragma result: " & $x) return err("Invalid pragma result: " & $x)
# TODO: check current version and implement schema versioning # TODO: check current version and implement schema versioning
@ -243,7 +384,7 @@ proc init*(
containsStmt: containsStmt) containsStmt: containsStmt)
ok(SqStoreRef( ok(SqStoreRef(
env: env, env: env.release,
keyspaces: keyspaceStatements keyspaces: keyspaceStatements
)) ))

View File

@ -2,13 +2,96 @@
import import
os, os,
unittest, testutils/unittests,
../../eth/db/[kvstore, kvstore_sqlite3], ../../eth/db/[kvstore, kvstore_sqlite3],
./test_kvstore ./test_kvstore
suite "SqStoreRef": procSuite "SqStoreRef":
test "KvStore interface": test "KvStore interface":
let db = SqStoreRef.init("", "test", inMemory = true)[] let db = SqStoreRef.init("", "test", inMemory = true)[]
defer: db.close() defer: db.close()
testKvStore(kvStore db) testKvStore(kvStore db)
test "Prepare and execute statements":
let db = SqStoreRef.init("", "test", inMemory = true)[]
defer: db.close()
let createTableRes = db.exec """
CREATE TABLE IF NOT EXISTS records(
key INTEGER PRIMARY KEY,
value BLOB
);
"""
check createTableRes.isOk
let insertStmt = db.prepareStmt(
"INSERT INTO records(value) VALUES (?);",
openarray[byte], void).get
let insert1Res = insertStmt.exec [byte 1, 2, 3, 4]
let insert2Res = insertStmt.exec @[]
let insert3Res = insertStmt.exec @[byte 5]
check:
insert1Res.isOk
insert2Res.isOk
insert3Res.isOk
let countStmt = db.prepareStmt(
"SELECT COUNT(*) FROM records;",
NoParams, int64).get
var totalRecords = 0
echo "About to call total records"
let countRes = countStmt.exec do (res: int64):
totalRecords = int res
check:
countRes.isOk and countRes.get == true
totalRecords == 3
let selectRangeStmt = db.prepareStmt(
"SELECT value FROM records WHERE key >= ? and key < ?;",
(int64, int64), openarray[byte]).get
block:
var allBytes = newSeq[byte]()
let selectRangeRes = selectRangeStmt.exec((0'i64, 5'i64)) do (bytes: openarray[byte]) {.gcsafe.}:
allBytes.add byte(bytes.len)
allBytes.add bytes
if selectRangeRes.isErr:
echo selectRangeRes.error
check:
selectRangeRes.isOk and selectRangeRes.get == true
allBytes == [byte 4, 1, 2, 3, 4,
0,
1, 5]
block:
let selectRangeRes = selectRangeStmt.exec((10'i64, 20'i64)) do (bytes: openarray[byte]):
echo "Got unexpected bytes: ", bytes
check:
selectRangeRes.isOk and selectRangeRes.get == false
let selectAllStmt = db.prepareStmt(
"SELECT * FROM records;",
NoParams, (int64, seq[byte])).get
var indices = newSeq[int64]()
var values = newSeq[seq[byte]]()
discard selectAllStmt.exec do (res: (int64, seq[byte])):
indices.add res[0]
values.add res[1]
check:
indices == [int64 1, 2, 3]
values == [
@[byte 1, 2, 3, 4],
@[],
@[byte 5]
]