nim-datastore/datastore/sql/sqliteds.nim

232 lines
6.2 KiB
Nim
Raw Normal View History

2022-09-16 21:14:31 -06:00
import std/times
import std/options
2022-09-16 21:14:31 -06:00
import pkg/questionable
import pkg/questionable/results
import pkg/sqlite3_abi
from pkg/stew/results as stewResults import isErr
import pkg/upraises
2023-09-20 17:49:57 -07:00
import ../backend
2022-09-16 21:14:31 -06:00
import ./sqlitedsdb
2023-09-20 17:49:57 -07:00
export backend, sqlitedsdb
2022-09-16 21:14:31 -06:00
push: {.upraises: [].}
type
2023-09-25 17:51:54 -07:00
SQLiteBackend*[K: DbKey, V: DbVal] = object
2023-09-25 17:35:37 -07:00
db: SQLiteDsDb[K, V]
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
proc path*[K,V](self: SQLiteBackend[K,V]): string =
2023-09-21 17:52:06 -07:00
$self.db.dbPath
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
proc readOnly*[K,V](self: SQLiteBackend[K,V]): bool = self.db.readOnly
2022-09-16 21:14:31 -06:00
proc timestamp*(t = epochTime()): int64 =
(t * 1_000_000).int64
2023-09-25 17:35:37 -07:00
proc has*[K,V](self: SQLiteBackend[K,V], key: DbKey): ?!bool =
2022-09-16 21:14:31 -06:00
var
exists = false
2023-09-25 17:35:37 -07:00
key = key
2022-09-16 21:14:31 -06:00
proc onData(s: RawStmtPtr) =
exists = sqlite3_column_int64(s, ContainsStmtExistsCol.cint).bool
2023-09-20 20:04:44 -07:00
if err =? self.db.containsStmt.query((key), onData).errorOption:
2022-12-02 16:25:44 -06:00
return failure err
2022-09-16 21:14:31 -06:00
return success exists
2023-09-25 17:49:17 -07:00
proc delete*[K,V](self: SQLiteBackend[K,V], key: K): ?!void =
2023-09-25 17:35:37 -07:00
return self.db.deleteStmt.exec((key))
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
proc delete*[K,V](self: SQLiteBackend[K,V], keys: openArray[DbKey]): ?!void =
if err =? self.db.beginStmt.exec().errorOption:
return failure(err)
for key in keys:
2023-09-25 17:35:37 -07:00
if err =? self.db.deleteStmt.exec((key)).errorOption:
if err =? self.db.rollbackStmt.exec().errorOption:
return failure err.msg
return failure err.msg
if err =? self.db.endStmt.exec().errorOption:
return failure err.msg
return success()
2023-09-25 17:49:17 -07:00
proc get*[K,V](self: SQLiteBackend[K,V], key: K): ?!seq[byte] =
2022-09-16 21:14:31 -06:00
# see comment in ./filesystem_datastore re: finer control of memory
2023-09-20 17:12:19 -07:00
# allocation in `proc get`, could apply here as well if bytes were read
2022-09-16 21:14:31 -06:00
# incrementally with `sqlite3_blob_read`
var
bytes: seq[byte]
proc onData(s: RawStmtPtr) =
2023-09-25 17:59:18 -07:00
bytes = dataCol[V](self.db.getDataCol)
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
if err =? self.db.getStmt.query((key), onData).errorOption:
return failure(err)
if bytes.len <= 0:
return failure(
2023-09-20 17:49:57 -07:00
newException(DatastoreKeyNotFound, "DbKey doesn't exist"))
2022-09-16 21:14:31 -06:00
return success bytes
2023-09-25 17:49:17 -07:00
proc put*[K,V](self: SQLiteBackend[K,V], key: K, data: V): ?!void =
2023-09-25 17:35:37 -07:00
return self.db.putStmt.exec((key, data, timestamp()))
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
proc put*[K,V](self: SQLiteBackend[K,V], batch: openArray[DbBatchEntry]): ?!void =
if err =? self.db.beginStmt.exec().errorOption:
return failure err
for entry in batch:
2023-09-25 17:35:37 -07:00
let putStmt = self.db.putStmt
2023-09-20 23:28:50 -07:00
if err =? putStmt.exec((entry.key, entry.data, timestamp())).errorOption:
if err =? self.db.rollbackStmt.exec().errorOption:
return failure err
return failure err
if err =? self.db.endStmt.exec().errorOption:
return failure err
return success()
2023-09-25 17:35:37 -07:00
proc close*[K,V](self: SQLiteBackend[K,V]): ?!void =
2022-09-19 22:40:01 -06:00
self.db.close()
2022-09-19 22:40:01 -06:00
return success()
2023-09-25 17:35:37 -07:00
proc query*[K,V](
self: SQLiteBackend[K,V],
2023-09-21 18:29:07 -07:00
query: DbQuery
2023-09-25 18:12:48 -07:00
): Result[DbQueryHandle[K,V,RawStmtPtr], ref CatchableError] =
var
queryStr = if query.value:
QueryStmtDataIdStr
else:
QueryStmtIdStr
if query.sort == SortOrder.Descending:
queryStr &= QueryStmtOrderDescending
else:
queryStr &= QueryStmtOrderAscending
if query.limit != 0:
queryStr &= QueryStmtLimit
if query.offset != 0:
queryStr &= QueryStmtOffset
let
queryStmt = QueryStmt.prepare(
self.db.env, queryStr).expect("should not fail")
s = RawStmtPtr(queryStmt)
2023-09-21 18:08:00 -07:00
queryKey = $query.key & "*"
var
v = sqlite3_bind_text(
2023-09-21 18:08:00 -07:00
s, 1.cint, queryKey.cstring, queryKey.len().cint, SQLITE_TRANSIENT_GCSAFE)
if not (v == SQLITE_OK):
return failure newException(DatastoreError, $sqlite3_errstr(v))
if query.limit != 0:
v = sqlite3_bind_int(s, 2.cint, query.limit.cint)
if not (v == SQLITE_OK):
return failure newException(DatastoreError, $sqlite3_errstr(v))
if query.offset != 0:
v = sqlite3_bind_int(s, 3.cint, query.offset.cint)
if not (v == SQLITE_OK):
return failure newException(DatastoreError, $sqlite3_errstr(v))
2023-09-25 18:12:48 -07:00
success DbQueryHandle[K,V,RawStmtPtr](query: query, env: s)
2023-09-25 15:46:03 -07:00
2023-09-25 18:12:48 -07:00
proc close*[K,V](handle: var DbQueryHandle[K,V,RawStmtPtr]) =
2023-09-25 16:06:09 -07:00
if not handle.closed:
handle.closed = true
2023-09-25 15:43:27 -07:00
discard sqlite3_reset(handle.env)
discard sqlite3_clear_bindings(handle.env)
handle.env.dispose()
2023-09-21 18:54:42 -07:00
2023-09-25 18:12:48 -07:00
iterator iter*[K, V](handle: var DbQueryHandle[K, V, RawStmtPtr]): ?!DbQueryResponse[K, V] =
2023-09-25 15:54:07 -07:00
while not handle.cancel:
let v = sqlite3_step(handle.env)
case v
of SQLITE_ROW:
let
2023-09-25 17:49:17 -07:00
key = K.toKey(sqlite3_column_text_not_null(handle.env, QueryStmtIdCol))
2023-09-25 15:54:07 -07:00
blob: ?pointer =
if handle.query.value: sqlite3_column_blob(handle.env, QueryStmtDataCol).some
else: pointer.none
# 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.isSome and blob.get().isNil:
let v = sqlite3_errcode(sqlite3_db_handle(handle.env))
if not (v in [SQLITE_OK, SQLITE_ROW, SQLITE_DONE]):
handle.cancel = true
2023-09-25 18:12:48 -07:00
yield DbQueryResponse[K,V].failure newException(DatastoreError, $sqlite3_errstr(v))
2023-09-25 15:54:07 -07:00
let
dataLen = sqlite3_column_bytes(handle.env, QueryStmtDataCol)
data =
if blob.isSome:
let arr = cast[ptr UncheckedArray[byte]](blob)
2023-09-25 18:12:48 -07:00
V.toVal(arr.toOpenArray(0, dataLen-1))
2023-09-25 15:54:07 -07:00
else: DataBuffer.new("")
yield success (key.some, data)
of SQLITE_DONE:
2023-09-25 16:06:09 -07:00
handle.close()
2023-09-25 15:54:07 -07:00
break
else:
handle.cancel = true
2023-09-25 18:12:48 -07:00
yield DbQueryResponse[K,V].failure newException(DatastoreError, $sqlite3_errstr(v))
2023-09-25 16:06:09 -07:00
break
2023-09-25 15:54:07 -07:00
handle.close()
2023-09-20 22:12:53 -07:00
2023-09-25 17:35:37 -07:00
proc contains*[K,V](self: SQLiteBackend[K,V], key: DbKey): bool =
2023-09-20 22:23:18 -07:00
return self.has(key).get()
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
proc newSQLiteBackend*[K,V](
2023-09-20 17:12:19 -07:00
path: string,
2023-09-25 17:35:37 -07:00
readOnly = false): ?!SQLiteBackend[K,V] =
2022-09-16 21:14:31 -06:00
let
flags =
if readOnly: SQLITE_OPEN_READONLY
else: SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE
2023-09-25 17:49:17 -07:00
success SQLiteBackend[K,V](db: ? SQLiteDsDb[K,V].open(path, flags))
2023-09-20 17:12:19 -07:00
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
proc newSQLiteBackend*[K,V](
db: SQLiteDsDb[K,V]): ?!SQLiteBackend[K,V] =
2022-09-16 21:14:31 -06:00
2023-09-25 17:35:37 -07:00
success SQLiteBackend[K,V](db: db)