nim-datastore/datastore/sql/sqlitedsdb.nim

318 lines
8.0 KiB
Nim
Raw Normal View History

2022-09-16 21:14:31 -06:00
import std/os
import pkg/questionable
import pkg/questionable/results
import pkg/upraises
2023-09-20 20:04:44 -07:00
import ../backend
2022-09-16 21:14:31 -06:00
import ./sqliteutils
export sqliteutils
type
BoundIdCol* = proc (): string {.closure, gcsafe, upraises: [].}
2023-09-20 20:04:44 -07:00
BoundDataCol* = proc (): DataBuffer {.closure, gcsafe, upraises: [].}
2022-09-16 21:14:31 -06:00
BoundTimestampCol* = proc (): int64 {.closure, gcsafe, upraises: [].}
# feels odd to use `void` for prepared statements corresponding to SELECT
# queries but it fits with the rest of the SQLite wrapper adapted from
# status-im/nwaku, at least in its current form in ./sqlite
2023-09-25 17:35:37 -07:00
ContainsStmt*[K] = SQLiteStmt[(K), void]
DeleteStmt*[K] = SQLiteStmt[(K), void]
GetStmt*[K] = SQLiteStmt[(K), void]
PutStmt*[K, V] = SQLiteStmt[(K, V, int64), void]
2022-09-16 21:14:31 -06:00
QueryStmt* = SQLiteStmt[(string), void]
BeginStmt* = NoParamsStmt
EndStmt* = NoParamsStmt
RollbackStmt* = NoParamsStmt
2022-09-16 21:14:31 -06:00
2023-09-25 17:51:54 -07:00
SQLiteDsDb*[K: DbKey, V: DbVal] = object
2022-09-16 21:14:31 -06:00
readOnly*: bool
2023-09-21 17:21:20 -07:00
dbPath*: DataBuffer
2023-09-25 17:35:37 -07:00
containsStmt*: ContainsStmt[K]
deleteStmt*: DeleteStmt[K]
2022-09-16 21:14:31 -06:00
env*: SQLite
2023-09-20 22:43:56 -07:00
getDataCol*: (RawStmtPtr, int)
2023-09-25 17:35:37 -07:00
getStmt*: GetStmt[K]
putStmt*: PutStmt[K,V]
beginStmt*: BeginStmt
endStmt*: EndStmt
rollbackStmt*: RollbackStmt
2022-09-16 21:14:31 -06:00
const
DbExt* = ".sqlite3"
TableName* = "Store"
IdColName* = "id"
DataColName* = "data"
TimestampColName* = "timestamp"
IdColType = "TEXT"
DataColType = "BLOB"
TimestampColType = "INTEGER"
Memory* = ":memory:"
# https://stackoverflow.com/a/9756276
# EXISTS returns a boolean value represented by an integer:
# https://sqlite.org/datatype3.html#boolean_datatype
# https://sqlite.org/lang_expr.html#the_exists_operator
ContainsStmtStr* = """
SELECT EXISTS(
SELECT 1 FROM """ & TableName & """
WHERE """ & IdColName & """ = ?
)
2022-09-16 21:14:31 -06:00
"""
ContainsStmtExistsCol* = 0
CreateStmtStr* = """
CREATE TABLE IF NOT EXISTS """ & TableName & """ (
""" & IdColName & """ """ & IdColType & """ NOT NULL PRIMARY KEY,
""" & DataColName & """ """ & DataColType & """,
""" & TimestampColName & """ """ & TimestampColType & """ NOT NULL
) WITHOUT ROWID;
"""
DeleteStmtStr* = """
DELETE FROM """ & TableName & """
WHERE """ & IdColName & """ = ?
2022-09-16 21:14:31 -06:00
"""
GetStmtStr* = """
SELECT """ & DataColName & """ FROM """ & TableName & """
WHERE """ & IdColName & """ = ?
2022-09-16 21:14:31 -06:00
"""
GetStmtDataCol* = 0
PutStmtStr* = """
REPLACE INTO """ & TableName & """ (
""" & IdColName & """,
""" & DataColName & """,
""" & TimestampColName & """
) VALUES (?, ?, ?)
2022-09-16 21:14:31 -06:00
"""
QueryStmtIdStr* = """
SELECT """ & IdColName & """ FROM """ & TableName &
""" WHERE """ & IdColName & """ GLOB ?
"""
QueryStmtDataIdStr* = """
2022-09-16 21:14:31 -06:00
SELECT """ & IdColName & """, """ & DataColName & """ FROM """ & TableName &
""" WHERE """ & IdColName & """ GLOB ?
"""
QueryStmtOffset* = """
OFFSET ?
"""
QueryStmtLimit* = """
LIMIT ?
"""
QueryStmtOrderAscending* = """
ORDER BY """ & IdColName & """ ASC
"""
QueryStmtOrderDescending* = """
ORDER BY """ & IdColName & """ DESC
2022-09-16 21:14:31 -06:00
"""
BeginTransactionStr* = """
BEGIN;
"""
EndTransactionStr* = """
END;
"""
RollbackTransactionStr* = """
ROLLBACK;
"""
2022-09-16 21:14:31 -06:00
QueryStmtIdCol* = 0
QueryStmtDataCol* = 1
proc checkColMetadata(s: RawStmtPtr, i: int, expectedName: string) =
let
colName = sqlite3_column_origin_name(s, i.cint)
if colName.isNil:
raise (ref Defect)(msg: "no column exists for index " & $i & " in `" &
$sqlite3_sql(s) & "`")
if $colName != expectedName:
raise (ref Defect)(msg: "original column name for index " & $i & " was \"" &
$colName & "\" in `" & $sqlite3_sql(s) & "` but callee expected \"" &
expectedName & "\"")
proc idCol*(
s: RawStmtPtr,
index: int): BoundIdCol =
checkColMetadata(s, index, IdColName)
return proc (): string =
$sqlite3_column_text_not_null(s, index.cint)
2023-09-25 17:59:18 -07:00
proc dataCol*[V: DbVal](data: (RawStmtPtr, int)): V =
2023-09-20 22:43:56 -07:00
let s = data[0]
let index = data[1]
2022-09-16 21:14:31 -06:00
checkColMetadata(s, index, DataColName)
2023-09-20 22:43:56 -07:00
let
i = index.cint
blob = sqlite3_column_blob(s, i)
# 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
# the "data" column can be NULL so in order to detect an out-of-memory error
# it is necessary to check that the result is a null pointer and that the
# result code is an error code
if blob.isNil:
2022-09-16 21:14:31 -06:00
let
2023-09-20 22:43:56 -07:00
v = sqlite3_errcode(sqlite3_db_handle(s))
2022-09-16 21:14:31 -06:00
2023-09-20 22:43:56 -07:00
if not (v in [SQLITE_OK, SQLITE_ROW, SQLITE_DONE]):
raise (ref Defect)(msg: $sqlite3_errstr(v))
2022-09-16 21:14:31 -06:00
2023-09-20 22:43:56 -07:00
let
dataLen = sqlite3_column_bytes(s, i)
dataBytes = cast[ptr UncheckedArray[byte]](blob)
2022-09-16 21:14:31 -06:00
2023-09-20 22:43:56 -07:00
# copy data out, since sqlite will free it
2023-09-25 17:59:18 -07:00
V.toVal(toOpenArray(dataBytes, 0, dataLen - 1))
2022-09-16 21:14:31 -06:00
proc timestampCol*(
s: RawStmtPtr,
index: int): BoundTimestampCol =
checkColMetadata(s, index, TimestampColName)
return proc (): int64 =
sqlite3_column_int64(s, index.cint)
proc getDBFilePath*(path: string): ?!string =
try:
let
(parent, name, ext) = path.normalizePathEnd.splitFile
dbExt = if ext == "": DbExt else: ext
absPath =
if parent.isAbsolute: parent
else: getCurrentDir() / parent
dbPath = absPath / name & dbExt
return success dbPath
except CatchableError as exc:
return failure(exc.msg)
2023-09-25 18:02:34 -07:00
proc close*[K, V](self: SQLiteDsDb[K, V]) =
2022-09-16 21:14:31 -06:00
self.containsStmt.dispose
self.getStmt.dispose
self.beginStmt.dispose
self.endStmt.dispose
self.rollbackStmt.dispose
2022-09-16 21:14:31 -06:00
if not RawStmtPtr(self.deleteStmt).isNil:
self.deleteStmt.dispose
if not RawStmtPtr(self.putStmt).isNil:
self.putStmt.dispose
self.env.dispose
2023-09-25 17:35:37 -07:00
proc open*[K,V](
T: type SQLiteDsDb[K,V],
2022-09-16 21:14:31 -06:00
path = Memory,
2023-09-25 17:35:37 -07:00
flags = SQLITE_OPEN_READONLY): ?!SQLiteDsDb[K, V] =
2022-09-16 21:14:31 -06:00
# make it optional to enable WAL with it enabled being the default?
# make it possible to specify a custom page size?
# https://www.sqlite.org/pragma.html#pragma_page_size
# https://www.sqlite.org/intern-v-extern-blob.html
var
env: AutoDisposed[SQLite]
defer:
disposeIfUnreleased(env)
let
isMemory = path == Memory
absPath = if isMemory: Memory else: ?path.getDBFilePath
readOnly = (SQLITE_OPEN_READONLY and flags).bool
if not isMemory:
if readOnly and not fileExists(absPath):
return failure "read-only database does not exist: " & absPath
elif not dirExists(absPath.parentDir):
return failure "directory does not exist: " & absPath
open(absPath, env.val, flags)
let
pragmaStmt = journalModePragmaStmt(env.val)
checkExec(pragmaStmt)
var
2023-09-25 17:35:37 -07:00
containsStmt: ContainsStmt[K]
deleteStmt: DeleteStmt[K]
getStmt: GetStmt[K]
putStmt: PutStmt[K,V]
beginStmt: BeginStmt
endStmt: EndStmt
rollbackStmt: RollbackStmt
2022-09-16 21:14:31 -06:00
if not readOnly:
checkExec(env.val, CreateStmtStr)
2023-09-25 17:35:37 -07:00
deleteStmt = ? DeleteStmt[K].prepare(
2022-09-16 21:14:31 -06:00
env.val, DeleteStmtStr, SQLITE_PREPARE_PERSISTENT)
2023-09-25 17:35:37 -07:00
putStmt = ? PutStmt[K,V].prepare(
2023-09-20 20:14:08 -07:00
env.val, PutStmtStr, SQLITE_PREPARE_PERSISTENT)
2022-09-16 21:14:31 -06:00
beginStmt = ? BeginStmt.prepare(
env.val, BeginTransactionStr, SQLITE_PREPARE_PERSISTENT)
endStmt = ? EndStmt.prepare(
env.val, EndTransactionStr, SQLITE_PREPARE_PERSISTENT)
rollbackStmt = ? RollbackStmt.prepare(
env.val, RollbackTransactionStr, SQLITE_PREPARE_PERSISTENT)
2023-09-25 17:35:37 -07:00
containsStmt = ? ContainsStmt[K].prepare(
2022-09-16 21:14:31 -06:00
env.val, ContainsStmtStr, SQLITE_PREPARE_PERSISTENT)
2023-09-25 17:35:37 -07:00
getStmt = ? GetStmt[K].prepare(
2022-09-16 21:14:31 -06:00
env.val, GetStmtStr, SQLITE_PREPARE_PERSISTENT)
# if a readOnly/existing database does not satisfy the expected schema
# `pepare()` will fail and `new` will return an error with message
# "SQL logic error"
let
2023-09-20 22:43:56 -07:00
getDataCol = (RawStmtPtr(getStmt), GetStmtDataCol)
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
success SQLiteDsDb[K,V](
2022-09-16 21:14:31 -06:00
readOnly: readOnly,
2023-09-21 17:21:20 -07:00
dbPath: DataBuffer.new path,
2022-09-16 21:14:31 -06:00
containsStmt: containsStmt,
deleteStmt: deleteStmt,
env: env.release,
getStmt: getStmt,
getDataCol: getDataCol,
putStmt: putStmt,
beginStmt: beginStmt,
endStmt: endStmt,
rollbackStmt: rollbackStmt)