mirror of https://github.com/status-im/nim-eth.git
sqlite: support read-only kvstores (#563)
in a read-only database, we cannot create the table but we can still reason about elements in it - they simply don't exist
This commit is contained in:
parent
6499ee2bc5
commit
8f4ef19fc9
|
@ -3,11 +3,11 @@
|
||||||
{.push raises: [Defect].}
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[os, options, strformat],
|
std/[os, options, strformat, typetraits],
|
||||||
sqlite3_abi,
|
sqlite3_abi,
|
||||||
./kvstore
|
./kvstore
|
||||||
|
|
||||||
export kvstore
|
export kvstore, typetraits
|
||||||
|
|
||||||
type
|
type
|
||||||
RawStmtPtr = ptr sqlite3_stmt
|
RawStmtPtr = ptr sqlite3_stmt
|
||||||
|
@ -37,6 +37,7 @@ type
|
||||||
SqKeyspace* = object of RootObj
|
SqKeyspace* = object of RootObj
|
||||||
# A Keyspace is a single key-value table - it is generally efficient to
|
# A Keyspace is a single key-value table - it is generally efficient to
|
||||||
# create separate keyspaces for each type of data stored
|
# create separate keyspaces for each type of data stored
|
||||||
|
open: bool
|
||||||
getStmt, putStmt, delStmt, containsStmt,
|
getStmt, putStmt, delStmt, containsStmt,
|
||||||
findStmt0, findStmt1, findStmt2: RawStmtPtr
|
findStmt0, findStmt1, findStmt2: RawStmtPtr
|
||||||
|
|
||||||
|
@ -115,10 +116,11 @@ proc bindParam(s: RawStmtPtr, n: int, val: auto): cint =
|
||||||
|
|
||||||
template bindParams(s: RawStmtPtr, params: auto) =
|
template bindParams(s: RawStmtPtr, params: auto) =
|
||||||
when params is tuple:
|
when params is tuple:
|
||||||
var i = 1
|
when params.type.arity > 0:
|
||||||
for param in fields(params):
|
var i = 1
|
||||||
checkErr bindParam(s, i, param)
|
for param in fields(params):
|
||||||
inc i
|
checkErr bindParam(s, i, param)
|
||||||
|
inc i
|
||||||
else:
|
else:
|
||||||
checkErr bindParam(s, 1, params)
|
checkErr bindParam(s, 1, params)
|
||||||
|
|
||||||
|
@ -208,7 +210,8 @@ proc exec*[Params, Res](s: SqliteStmt[Params, Res],
|
||||||
let v = sqlite3_step(s)
|
let v = sqlite3_step(s)
|
||||||
case v
|
case v
|
||||||
of SQLITE_ROW:
|
of SQLITE_ROW:
|
||||||
onData(readResult(s, Res))
|
if onData != nil:
|
||||||
|
onData(readResult(s, Res))
|
||||||
gotResults = true
|
gotResults = true
|
||||||
of SQLITE_DONE:
|
of SQLITE_DONE:
|
||||||
break
|
break
|
||||||
|
@ -294,8 +297,9 @@ template exec*(db: SqStoreRef, stmt: string): KvResult[void] =
|
||||||
proc get*(db: SqKeyspaceRef,
|
proc get*(db: SqKeyspaceRef,
|
||||||
key: openArray[byte],
|
key: openArray[byte],
|
||||||
onData: DataProc): KvResult[bool] =
|
onData: DataProc): KvResult[bool] =
|
||||||
if db.getStmt == nil: return err("sqlite: database closed")
|
if not db.open: return err("sqlite: database closed")
|
||||||
let getStmt = db.getStmt
|
let getStmt = db.getStmt
|
||||||
|
if getStmt == nil: return ok(false) # no such table
|
||||||
checkErr bindParam(getStmt, 1, key)
|
checkErr bindParam(getStmt, 1, key)
|
||||||
|
|
||||||
let
|
let
|
||||||
|
@ -339,23 +343,26 @@ proc find*(
|
||||||
db: SqKeyspaceRef,
|
db: SqKeyspaceRef,
|
||||||
prefix: openArray[byte],
|
prefix: openArray[byte],
|
||||||
onFind: KeyValueProc): KvResult[int] =
|
onFind: KeyValueProc): KvResult[int] =
|
||||||
|
if not db.open: return err("sqlite: database closed")
|
||||||
var next: seq[byte] # extended lifetime of bound param
|
var next: seq[byte] # extended lifetime of bound param
|
||||||
let findStmt =
|
let findStmt =
|
||||||
if prefix.len == 0:
|
if prefix.len == 0:
|
||||||
|
if db.findStmt0 == nil: return ok(0) # no such table
|
||||||
db.findStmt0 # all rows
|
db.findStmt0 # all rows
|
||||||
else:
|
else:
|
||||||
if not nextPrefix(prefix, next):
|
if not nextPrefix(prefix, next):
|
||||||
# For example when looking for the prefix [byte 255], there are no
|
# For example when looking for the prefix [byte 255], there are no
|
||||||
# prefixes that lexicographically are greater, thus we use the
|
# prefixes that lexicographically are greater, thus we use the
|
||||||
# query that only does the >= comparison
|
# query that only does the >= comparison
|
||||||
|
if db.findStmt1 == nil: return ok(0) # no such table
|
||||||
checkErr bindParam(db.findStmt1, 1, prefix)
|
checkErr bindParam(db.findStmt1, 1, prefix)
|
||||||
db.findStmt1
|
db.findStmt1
|
||||||
else:
|
else:
|
||||||
|
if db.findStmt2 == nil: return ok(0) # no such table
|
||||||
checkErr bindParam(db.findStmt2, 1, prefix)
|
checkErr bindParam(db.findStmt2, 1, prefix)
|
||||||
checkErr bindParam(db.findStmt2, 2, next)
|
checkErr bindParam(db.findStmt2, 2, next)
|
||||||
db.findStmt2
|
db.findStmt2
|
||||||
|
|
||||||
if findStmt == nil: return err("sqlite: database closed")
|
|
||||||
|
|
||||||
var
|
var
|
||||||
total = 0
|
total = 0
|
||||||
|
@ -369,7 +376,8 @@ proc find*(
|
||||||
kl = sqlite3_column_bytes(findStmt, 0)
|
kl = sqlite3_column_bytes(findStmt, 0)
|
||||||
vp = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(findStmt, 1))
|
vp = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(findStmt, 1))
|
||||||
vl = sqlite3_column_bytes(findStmt, 1)
|
vl = sqlite3_column_bytes(findStmt, 1)
|
||||||
onFind(kp.toOpenArray(0, kl - 1), vp.toOpenArray(0, vl - 1))
|
if onFind != nil:
|
||||||
|
onFind(kp.toOpenArray(0, kl - 1), vp.toOpenArray(0, vl - 1))
|
||||||
total += 1
|
total += 1
|
||||||
of SQLITE_DONE:
|
of SQLITE_DONE:
|
||||||
break
|
break
|
||||||
|
@ -387,8 +395,9 @@ proc find*(
|
||||||
ok(total)
|
ok(total)
|
||||||
|
|
||||||
proc put*(db: SqKeyspaceRef, key, value: openArray[byte]): KvResult[void] =
|
proc put*(db: SqKeyspaceRef, key, value: openArray[byte]): KvResult[void] =
|
||||||
|
if not db.open: return err("sqlite: database closed")
|
||||||
let putStmt = db.putStmt
|
let putStmt = db.putStmt
|
||||||
if putStmt == nil: return err("sqlite: database closed")
|
if putStmt == nil: return err("sqlite: cannot write to read-only database")
|
||||||
checkErr bindParam(putStmt, 1, key)
|
checkErr bindParam(putStmt, 1, key)
|
||||||
checkErr bindParam(putStmt, 2, value)
|
checkErr bindParam(putStmt, 2, value)
|
||||||
|
|
||||||
|
@ -405,8 +414,10 @@ proc put*(db: SqKeyspaceRef, key, value: openArray[byte]): KvResult[void] =
|
||||||
res
|
res
|
||||||
|
|
||||||
proc contains*(db: SqKeyspaceRef, key: openArray[byte]): KvResult[bool] =
|
proc contains*(db: SqKeyspaceRef, key: openArray[byte]): KvResult[bool] =
|
||||||
|
if not db.open: return err("sqlite: database closed")
|
||||||
let containsStmt = db.containsStmt
|
let containsStmt = db.containsStmt
|
||||||
if containsStmt == nil: return err("sqlite: database closed")
|
if containsStmt == nil: return ok(false) # no such table
|
||||||
|
|
||||||
checkErr bindParam(containsStmt, 1, key)
|
checkErr bindParam(containsStmt, 1, key)
|
||||||
|
|
||||||
let
|
let
|
||||||
|
@ -423,8 +434,9 @@ proc contains*(db: SqKeyspaceRef, key: openArray[byte]): KvResult[bool] =
|
||||||
res
|
res
|
||||||
|
|
||||||
proc del*(db: SqKeyspaceRef, key: openArray[byte]): KvResult[void] =
|
proc del*(db: SqKeyspaceRef, key: openArray[byte]): KvResult[void] =
|
||||||
|
if not db.open: return err("sqlite: database closed")
|
||||||
let delStmt = db.delStmt
|
let delStmt = db.delStmt
|
||||||
if delStmt == nil: return err("sqlite: database closed")
|
if delStmt == nil: return ok() # no such table
|
||||||
checkErr bindParam(delStmt, 1, key)
|
checkErr bindParam(delStmt, 1, key)
|
||||||
|
|
||||||
let res =
|
let res =
|
||||||
|
@ -473,14 +485,14 @@ proc checkpoint*(db: SqStoreRef, kind = SqStoreCheckpointKind.passive) =
|
||||||
template prepare(env: ptr sqlite3, q: string): ptr sqlite3_stmt =
|
template prepare(env: ptr sqlite3, q: string): ptr sqlite3_stmt =
|
||||||
block:
|
block:
|
||||||
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, cstring(q), q.len.cint, addr s, nil):
|
||||||
discard
|
discard
|
||||||
s
|
s
|
||||||
|
|
||||||
template prepare(env: ptr sqlite3, q: string, cleanup: untyped): ptr sqlite3_stmt =
|
template prepare(env: ptr sqlite3, q: string, cleanup: untyped): ptr sqlite3_stmt =
|
||||||
block:
|
block:
|
||||||
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, cstring(q), q.len.cint, addr s, nil)
|
||||||
s
|
s
|
||||||
|
|
||||||
template checkExec(s: ptr sqlite3_stmt) =
|
template checkExec(s: ptr sqlite3_stmt) =
|
||||||
|
@ -564,7 +576,15 @@ proc init*(
|
||||||
readOnly: readOnly
|
readOnly: readOnly
|
||||||
))
|
))
|
||||||
|
|
||||||
proc openKvStore*(db: SqStoreRef, name = "kvstore", withoutRowid = false): KvResult[SqKeyspaceRef] =
|
proc hasTable*(db: SqStoreRef, name: string): KvResult[bool] =
|
||||||
|
let
|
||||||
|
sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='" &
|
||||||
|
name & "';"
|
||||||
|
db.exec(sql, (), proc(_: openArray[byte]) = discard)
|
||||||
|
|
||||||
|
proc openKvStore*(
|
||||||
|
db: SqStoreRef, name = "kvstore", withoutRowid = false,
|
||||||
|
readOnly = false): KvResult[SqKeyspaceRef] =
|
||||||
## Open a new Key-Value store in the SQLite database
|
## Open a new Key-Value store in the SQLite database
|
||||||
##
|
##
|
||||||
## withoutRowid: Create the table without rowid - this is more efficient when
|
## withoutRowid: Create the table without rowid - this is more efficient when
|
||||||
|
@ -572,30 +592,33 @@ proc openKvStore*(db: SqStoreRef, name = "kvstore", withoutRowid = false): KvRes
|
||||||
## rows (the row being the sum of key and value) - see
|
## rows (the row being the sum of key and value) - see
|
||||||
## https://www.sqlite.org/withoutrowid.html
|
## https://www.sqlite.org/withoutrowid.html
|
||||||
##
|
##
|
||||||
|
let hasTable = if db.readOnly or readOnly:
|
||||||
if not db.readOnly:
|
? db.hasTable(name)
|
||||||
|
else:
|
||||||
let createSql = """
|
let createSql = """
|
||||||
CREATE TABLE IF NOT EXISTS """ & name & """ (
|
CREATE TABLE IF NOT EXISTS '""" & name & """' (
|
||||||
key BLOB PRIMARY KEY,
|
key BLOB PRIMARY KEY,
|
||||||
value BLOB
|
value BLOB
|
||||||
)"""
|
)"""
|
||||||
checkExec db.env,
|
checkExec db.env,
|
||||||
if withoutRowid: createSql & " WITHOUT ROWID;" else: createSql & ";"
|
if withoutRowid: createSql & " WITHOUT ROWID;" else: createSql & ";"
|
||||||
|
true
|
||||||
var
|
var
|
||||||
tmp: SqKeyspace
|
tmp: SqKeyspace
|
||||||
defer:
|
defer:
|
||||||
# We'll "move" ownership to the return value, effectively disabling "close"
|
# We'll "move" ownership to the return value, effectively disabling "close"
|
||||||
close(tmp)
|
close(tmp)
|
||||||
|
tmp.open = true
|
||||||
tmp.getStmt = prepare(db.env, "SELECT value FROM " & name & " WHERE key = ?;")
|
if hasTable:
|
||||||
tmp.putStmt =
|
tmp.getStmt =
|
||||||
prepare(db.env, "INSERT OR REPLACE INTO " & name & "(key, value) VALUES (?, ?);")
|
prepare(db.env, "SELECT value FROM '" & name & "' WHERE key = ?;")
|
||||||
tmp.delStmt = prepare(db.env, "DELETE FROM " & name & " WHERE key = ?;")
|
tmp.putStmt =
|
||||||
tmp.containsStmt = prepare(db.env, "SELECT 1 FROM " & name & " WHERE key = ?;")
|
prepare(db.env, "INSERT OR REPLACE INTO '" & name & "'(key, value) VALUES (?, ?);")
|
||||||
tmp.findStmt0 = prepare(db.env, "SELECT key, value FROM " & name & ";")
|
tmp.delStmt = prepare(db.env, "DELETE FROM '" & name & "' WHERE key = ?;")
|
||||||
tmp.findStmt1 = prepare(db.env, "SELECT key, value FROM " & name & " WHERE key >= ?;")
|
tmp.containsStmt = prepare(db.env, "SELECT 1 FROM '" & name & "' WHERE key = ?;")
|
||||||
tmp.findStmt2 = prepare(db.env, "SELECT key, value FROM " & name & " WHERE key >= ? and key < ?;")
|
tmp.findStmt0 = prepare(db.env, "SELECT key, value FROM '" & name & "';")
|
||||||
|
tmp.findStmt1 = prepare(db.env, "SELECT key, value FROM '" & name & "' WHERE key >= ?;")
|
||||||
|
tmp.findStmt2 = prepare(db.env, "SELECT key, value FROM '" & name & "' WHERE key >= ? and key < ?;")
|
||||||
|
|
||||||
var res = SqKeyspaceRef()
|
var res = SqKeyspaceRef()
|
||||||
res[] = tmp
|
res[] = tmp
|
||||||
|
|
|
@ -16,6 +16,18 @@ procSuite "SqStoreRef":
|
||||||
|
|
||||||
testKvStore(kvStore kv.get(), true)
|
testKvStore(kvStore kv.get(), true)
|
||||||
|
|
||||||
|
test "Readonly kvstore with no table":
|
||||||
|
let db = SqStoreRef.init("", "test", inMemory = true, readOnly = true)[]
|
||||||
|
defer: db.close()
|
||||||
|
let kv = db.openKvStore().expect("working db")
|
||||||
|
|
||||||
|
check:
|
||||||
|
not kv.get([byte 0, 1, 2], nil).expect("ok to query data")
|
||||||
|
kv.find([byte 0, 1, 2], nil).expect("ok") == 0
|
||||||
|
kv.put([byte 0, 1, 2], []).isErr
|
||||||
|
kv.del([byte 0, 1, 2]).isOk
|
||||||
|
defer: kv[].close()
|
||||||
|
|
||||||
test "Prepare and execute statements":
|
test "Prepare and execute statements":
|
||||||
let db = SqStoreRef.init("", "test", inMemory = true)[]
|
let db = SqStoreRef.init("", "test", inMemory = true)[]
|
||||||
defer: db.close()
|
defer: db.close()
|
||||||
|
|
Loading…
Reference in New Issue