import std/os import std/times import pkg/chronos import pkg/questionable import pkg/questionable/results import pkg/sqlite3_abi import pkg/stew/byteutils from pkg/stew/results as stewResults import isErr import pkg/upraises import ./datastore import ./sqlitedsdb export datastore, sqlitedsdb push: {.upraises: [].} type SQLiteDatastore* = ref object of Datastore readOnly: bool db: SQLiteDsDB proc path*(self: SQLiteDatastore): string = self.db.dbPath proc `readOnly=`*(self: SQLiteDatastore): bool {.error: "readOnly should not be assigned".} proc timestamp*(t = epochTime()): int64 = (t * 1_000_000).int64 method contains*(self: SQLiteDatastore, key: Key): Future[?!bool] {.async.} = var exists = false proc onData(s: RawStmtPtr) = exists = sqlite3_column_int64(s, ContainsStmtExistsCol.cint).bool let queryRes = self.db.containsStmt.query((key.id), onData) if queryRes.isErr: return queryRes return success exists method delete*(self: SQLiteDatastore, key: Key): Future[?!void] {.async.} = return self.db.deleteStmt.exec((key.id)) method get*(self: SQLiteDatastore, key: Key): Future[?!seq[byte]] {.async.} = # see comment in ./filesystem_datastore re: finer control of memory # allocation in `method get`, could apply here as well if bytes were read # incrementally with `sqlite3_blob_read` var bytes: seq[byte] let dataCol = self.db.getDataCol proc onData(s: RawStmtPtr) = bytes = dataCol() let queryRes = self.db.getStmt.query((key.id), onData) if queryRes.isErr: return failure queryRes.error.msg return success bytes method put*(self: SQLiteDatastore, key: Key, data: seq[byte]): Future[?!void] {.async.} = return self.db.putStmt.exec((key.id, @data, timestamp())) iterator query*( self: SQLiteDatastore, query: Query): Future[QueryResponse] = let queryStmt = QueryStmt.prepare( self.db.env, QueryStmtStr).expect("should not fail") s = RawStmtPtr(queryStmt) defer: discard sqlite3_reset(s) discard sqlite3_clear_bindings(s) s.dispose let v = sqlite3_bind_text(s, 1.cint, query.key.id.cstring, -1.cint, SQLITE_TRANSIENT_GCSAFE) if not (v == SQLITE_OK): raise (ref Defect)(msg: $sqlite3_errstr(v)) while true: let v = sqlite3_step(s) case v of SQLITE_ROW: let key = Key.init($sqlite3_column_text_not_null( s, QueryStmtIdCol)).expect("should not fail") blob = sqlite3_column_blob(s, QueryStmtDataCol) # 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: let v = sqlite3_errcode(sqlite3_db_handle(s)) if not (v in [SQLITE_OK, SQLITE_ROW, SQLITE_DONE]): raise (ref Defect)(msg: $sqlite3_errstr(v)) let dataLen = sqlite3_column_bytes(s, QueryStmtDataCol) dataBytes = cast[ptr UncheckedArray[byte]](blob) data = @(toOpenArray(dataBytes, 0, dataLen - 1)) fut = newFuture[QueryResponse]() fut.complete((key, data)) yield fut of SQLITE_DONE: break else: raise (ref Defect)(msg: $sqlite3_errstr(v)) proc new*( T: type SQLiteDatastore, path: string, readOnly = false): ?!T = let flags = if readOnly: SQLITE_OPEN_READONLY else: SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE success T( db: ? SQLIteDsDb.open(path, flags), readOnly: readOnly) proc new*( T: type SQLiteDatastore, db: SQLIteDsDb): ?!T = success T( db: db, readOnly: db.readOnly)