2024-05-15 11:37:01 -06:00
{. push raises : [ ] . }
2022-05-11 10:50:05 -05:00
import pkg / questionable
import pkg / questionable / results
import pkg / sqlite3_abi
2022-09-16 21:14:31 -06:00
export sqlite3_abi
2022-05-11 10:50:05 -05:00
# Adapted from:
# https://github.com/status-im/nwaku/blob/master/waku/v2/node/storage/sqlite.nim
2022-07-08 13:09:55 -05:00
# see https://www.sqlite.org/c3ref/column_database_name.html
# can pass `--forceBuild:on` to the Nim compiler if a SQLite build without
# `-DSQLITE_ENABLE_COLUMN_METADATA` option is stuck in the build cache,
# e.g. `nimble test --forceBuild:on`
2023-03-09 10:24:44 +01:00
{. passc : " -DSQLITE_ENABLE_COLUMN_METADATA " . }
2022-07-08 13:09:55 -05:00
2022-05-11 10:50:05 -05:00
type
AutoDisposed * [ T : ptr | ref ] = object
val * : T
2025-02-13 20:26:58 +01:00
DataProc * = proc ( s : RawStmtPtr ) {. closure , gcsafe , raises : [ ] . }
2022-05-11 10:50:05 -05:00
NoParams * = tuple # empty tuple
NoParamsStmt * = SQLiteStmt [ NoParams , void ]
RawStmtPtr * = ptr sqlite3_stmt
SQLite * = ptr sqlite3
SQLiteStmt * [ Params , Res ] = distinct RawStmtPtr
2022-07-15 15:28:42 -05:00
# see https://github.com/arnetheduck/nim-sqlite3-abi/issues/4
sqlite3_destructor_type_gcsafe =
2024-05-15 11:37:01 -06:00
proc ( a1 : pointer ) {. cdecl , gcsafe , raises : [ ] . }
2022-07-15 15:28:42 -05:00
const
SQLITE_TRANSIENT_GCSAFE * = cast [ sqlite3_destructor_type_gcsafe ] ( - 1 )
2022-05-11 10:50:05 -05:00
proc bindParam (
s : RawStmtPtr ,
n : int ,
val : auto ) : cint =
2023-03-09 10:24:44 +01:00
when val is openArray [ byte ] | seq [ byte ] :
2022-05-11 10:50:05 -05:00
if val . len > 0 :
# `SQLITE_TRANSIENT` "indicate[s] that the object is to be copied prior
# to the return from sqlite3_bind_*(). The object and pointer to it
# must remain valid until then. SQLite will then manage the lifetime of
# its private copy."
sqlite3_bind_blob ( s , n . cint , unsafeAddr val [ 0 ] , val . len . cint ,
SQLITE_TRANSIENT )
else :
2022-06-22 14:00:11 -05:00
sqlite3_bind_null ( s , n . cint )
2022-05-11 10:50:05 -05:00
elif val is int32 :
sqlite3_bind_int ( s , n . cint , val )
2022-06-22 13:46:10 -05:00
elif val is uint32 | int64 :
sqlite3_bind_int64 ( s , n . cint , val . int64 )
elif val is float32 | float64 :
sqlite3_bind_double ( s , n . cint , val . float64 )
2022-05-11 10:50:05 -05:00
elif val is string :
# `-1` implies string length is num bytes up to first null-terminator;
# `SQLITE_TRANSIENT` "indicate[s] that the object is to be copied prior
# to the return from sqlite3_bind_*(). The object and pointer to it must
# remain valid until then. SQLite will then manage the lifetime of its
# private copy."
sqlite3_bind_text ( s , n . cint , val . cstring , - 1 . cint , SQLITE_TRANSIENT )
else :
{. fatal : " Please add support for the ' " & $ typeof ( val ) & " ' type " . }
template bindParams (
s : RawStmtPtr ,
params : auto ) =
when params is tuple :
when params isnot NoParams :
2022-09-16 21:14:31 -06:00
var i = 1
2022-05-11 10:50:05 -05:00
for param in fields ( params ) :
checkErr bindParam ( s , i , param )
inc i
else :
checkErr bindParam ( s , 1 , params )
template checkErr * ( op : untyped ) =
if ( let v = ( op ) ; v ! = SQLITE_OK ) :
return failure $ sqlite3_errstr ( v )
2022-07-27 13:03:28 -05:00
template dispose * ( rawStmt : RawStmtPtr ) =
2024-01-15 22:00:42 -06:00
doAssert SQLITE_OK = = sqlite3_finalize ( rawStmt )
rawStmt = nil
2022-07-27 13:03:28 -05:00
2022-05-11 10:50:05 -05:00
template checkExec * ( s : RawStmtPtr ) =
if ( let x = sqlite3_step ( s ) ; x ! = SQLITE_DONE ) :
s . dispose
return failure $ sqlite3_errstr ( x )
if ( let x = sqlite3_finalize ( s ) ; x ! = SQLITE_OK ) :
return failure $ sqlite3_errstr ( x )
2022-07-27 13:03:28 -05:00
template prepare * (
env : SQLite ,
q : string ,
prepFlags : cuint = 0 ) : RawStmtPtr =
var
s : RawStmtPtr
checkErr sqlite3_prepare_v3 (
env , q . cstring , q . len . cint , prepFlags , addr s , nil )
s
2022-05-11 10:50:05 -05:00
template checkExec * ( env : SQLite , q : string ) =
2024-01-15 22:00:42 -06:00
var
2022-05-11 10:50:05 -05:00
s = prepare ( env , q )
checkExec ( s )
template dispose * ( db : SQLite ) =
2024-01-15 22:00:42 -06:00
# TODO: the assert bellow fails because we're
# not releasing all the statements at the time of
# releasing the connection. I suspect these are the
# query iterators that aren't being released on close
# doAssert SQLITE_OK == sqlite3_close(db)
2022-05-11 10:50:05 -05:00
discard sqlite3_close ( db )
template dispose * ( sqliteStmt : SQLiteStmt ) =
2024-01-15 22:00:42 -06:00
doAssert SQLITE_OK = = sqlite3_finalize ( RawStmtPtr ( sqliteStmt ) )
2025-02-13 20:26:58 +01:00
# nil literals can no longer be directly assigned to variables or fields of distinct pointer types.
# They must be converted instead.
# See https://nim-lang.org/blog/2022/12/21/version-20-rc.html#:~:text=nil%20literals%20can%20no%20longer%20be%20directly%20assigned%20to%20variables%20or%20fields%20of%20distinct%20pointer%20types.%20They%20must%20be%20converted%20instead.
# SQLiteStmt(nil) is generating a SIGSEGV, so we need to cast it
sqliteStmt = cast [ typeof sqliteStmt ] ( nil )
2022-05-11 10:50:05 -05:00
2022-07-27 13:03:28 -05:00
proc release * [ T ] ( x : var AutoDisposed [ T ] ) : T =
result = x . val
x . val = nil
2022-05-11 10:50:05 -05:00
proc disposeIfUnreleased * [ T ] ( x : var AutoDisposed [ T ] ) =
mixin dispose
if x . val ! = nil : dispose ( x . release )
2022-09-30 10:24:26 -04:00
proc exec * [ P ] ( s : SQLiteStmt [ P , void ] , params : P = ( ) ) : ? ! void =
2022-05-11 10:50:05 -05:00
let
s = RawStmtPtr ( s )
bindParams ( s , params )
let
res =
if ( let v = sqlite3_step ( s ) ; v ! = SQLITE_DONE ) :
failure $ sqlite3_errstr ( v )
else :
success ( )
2022-09-30 10:24:26 -04:00
# release implicit transaction
2022-05-11 10:50:05 -05:00
discard sqlite3_reset ( s ) # same return information as step
discard sqlite3_clear_bindings ( s ) # no errors possible
res
2022-07-27 13:03:28 -05:00
proc sqlite3_column_text_not_null * (
s : RawStmtPtr ,
index : cint ) : cstring =
let
text = sqlite3_column_text ( s , index ) . cstring
if text . isNil :
# see the conversion table and final paragraph of:
# https://www.sqlite.org/c3ref/column_blob.html
# a null pointer here implies an out-of-memory error
let
v = sqlite3_errcode ( sqlite3_db_handle ( s ) )
raise ( ref Defect ) ( msg : $ sqlite3_errstr ( v ) )
text
2022-05-11 10:50:05 -05:00
template journalModePragmaStmt * ( env : SQLite ) : RawStmtPtr =
2024-01-15 22:00:42 -06:00
var
2022-05-11 10:50:05 -05:00
s = prepare ( env , " PRAGMA journal_mode = WAL; " )
if ( let x = sqlite3_step ( s ) ; x ! = SQLITE_ROW ) :
s . dispose
return failure $ sqlite3_errstr ( x )
if ( let x = sqlite3_column_type ( s , 0 ) ; x ! = SQLITE3_TEXT ) :
s . dispose
return failure $ sqlite3_errstr ( x )
2022-07-08 12:49:35 -05:00
let
2022-07-14 14:25:20 -05:00
x = $ sqlite3_column_text_not_null ( s , 0 )
2022-07-08 12:49:35 -05:00
2022-07-14 14:25:20 -05:00
if not ( x in [ " memory " , " wal " ] ) :
2022-05-11 10:50:05 -05:00
s . dispose
2022-07-14 14:25:20 -05:00
return failure " Invalid pragma result: \" " & x & " \" "
2022-05-11 10:50:05 -05:00
s
template open * (
dbPath : string ,
env : var SQLite ,
flags = 0 ) =
checkErr sqlite3_open_v2 ( dbPath . cstring , addr env , flags . cint , nil )
proc prepare * [ Params , Res ] (
T : type SQLiteStmt [ Params , Res ] ,
env : SQLite ,
2022-07-08 15:46:19 -05:00
stmt : string ,
prepFlags : cuint = 0 ) : ? ! T =
2022-05-11 10:50:05 -05:00
var
s : RawStmtPtr
2022-07-08 15:46:19 -05:00
checkErr sqlite3_prepare_v3 (
env , stmt . cstring , stmt . len . cint , prepFlags , addr s , nil )
2022-05-11 10:50:05 -05:00
success T ( s )
proc query * [ P ] (
s : SQLiteStmt [ P , void ] ,
params : P ,
onData : DataProc ) : ? ! bool =
let
s = RawStmtPtr ( s )
bindParams ( s , params )
var
res = success false
while true :
let
v = sqlite3_step ( s )
case v
of SQLITE_ROW :
onData ( s )
res = success true
of SQLITE_DONE :
break
else :
res = failure $ sqlite3_errstr ( v )
break
# release implict transaction
discard sqlite3_reset ( s ) # same return information as step
discard sqlite3_clear_bindings ( s ) # no errors possible
res
proc query * (
env : SQLite ,
query : string ,
onData : DataProc ) : ? ! bool =
2025-02-13 20:26:58 +01:00
var s = ? NoParamsStmt . prepare ( env , query )
2022-05-11 10:50:05 -05:00
2025-02-13 20:26:58 +01:00
var res = s . query ( ( ) , onData )
2022-05-11 10:50:05 -05:00
# NB: dispose of the prepared query statement and free associated memory
s . dispose
2025-02-13 20:26:58 +01:00
return res
2022-05-11 10:50:05 -05:00