feat: embed tiny_sqlite (and expand api) instead of generating wrapper with nimterop

Also remove workarounds for problems previously experienced in "Windows+shared"
context.
This commit is contained in:
emizzle 2020-11-26 19:41:09 +11:00 committed by Michael Bradley
parent c06110bc81
commit 474cf12001
17 changed files with 1233 additions and 535 deletions

View File

@ -35,9 +35,9 @@ jobs:
include: , include: ,
lib: , lib: ,
} }
sqlite: [ true, false ] sqlcipher: [ true, false ]
openssl: [ true, false ] openssl: [ true, false ]
name: ${{ matrix.platform.icon }} - SQLITE ${{ matrix.sqlite }} | SSL ${{ matrix.openssl }} name: ${{ matrix.platform.icon }} - SQLCIPHER ${{ matrix.sqlcipher }} | SSL ${{ matrix.openssl }}
runs-on: ${{ matrix.platform.os }}-latest runs-on: ${{ matrix.platform.os }}-latest
defaults: defaults:
run: run:
@ -77,11 +77,11 @@ jobs:
make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC}" V=1 update make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC}" V=1 update
make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC}" V=1 deps make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC}" V=1 deps
- name: Build the sqlite.nim wrapper and run tests - name: Build libsqlcipher and run tests
run: | run: |
make -j${NPROC} \ make -j${NPROC} \
NIMFLAGS="--parallelBuild:${NPROC}" \ NIMFLAGS="--parallelBuild:${NPROC}" \
SQLITE_STATIC=${{ matrix.sqlite }} \ SQLCIPHER_STATIC=${{ matrix.sqlcipher }} \
SSL_INCLUDE_DIR="${{ matrix.platform.include }}" \ SSL_INCLUDE_DIR="${{ matrix.platform.include }}" \
SSL_LIB_DIR="${{ matrix.platform.lib }}" \ SSL_LIB_DIR="${{ matrix.platform.lib }}" \
SSL_STATIC=${{ matrix.openssl }} \ SSL_STATIC=${{ matrix.openssl }} \

2
.gitignore vendored
View File

@ -3,8 +3,8 @@
.vscode .vscode
/generator/* /generator/*
!/generator/generate.nim !/generator/generate.nim
/lib
/nimcache /nimcache
/sqlcipher
/sqlite /sqlite
/test/build /test/build
/update /update

18
.gitmodules vendored
View File

@ -4,24 +4,6 @@
[submodule "vendor/sqlcipher"] [submodule "vendor/sqlcipher"]
path = vendor/sqlcipher path = vendor/sqlcipher
url = https://github.com/sqlcipher/sqlcipher.git url = https://github.com/sqlcipher/sqlcipher.git
[submodule "vendor/nimterop"]
path = vendor/nimterop
url = https://github.com/nimterop/nimterop.git
[submodule "vendor/nim-regex"]
path = vendor/nim-regex
url = https://github.com/nitely/nim-regex.git
[submodule "vendor/nim-unicodedb"]
path = vendor/nim-unicodedb
url = https://github.com/nitely/nim-unicodedb.git
[submodule "vendor/nim-unicodeplus"]
path = vendor/nim-unicodeplus
url = https://github.com/nitely/nim-unicodeplus.git
[submodule "vendor/nim-segmentation"]
path = vendor/nim-segmentation
url = https://github.com/nitely/nim-segmentation.git
[submodule "vendor/cligen"]
path = vendor/cligen
url = https://github.com/c-blake/cligen.git
[submodule "vendor/nim-stew"] [submodule "vendor/nim-stew"]
path = vendor/nim-stew path = vendor/nim-stew
url = https://github.com/status-im/nim-stew url = https://github.com/status-im/nim-stew

147
Makefile
View File

@ -17,14 +17,11 @@ BUILD_SYSTEM_DIR := vendor/nimbus-build-system
all \ all \
clean \ clean \
clean-build-dirs \ clean-build-dirs \
clean-generator \ clean-sqlcipher \
clean-nimterop \
deps \ deps \
sqlite \ sqlcipher \
sqlite.nim \
sqlite3.c \ sqlite3.c \
test \ test \
toast \
update update
ifeq ($(NIM_PARAMS),) ifeq ($(NIM_PARAMS),)
@ -42,7 +39,7 @@ GIT_SUBMODULE_UPDATE := git submodule update --init --recursive
else # "variables.mk" was included. Business as usual until the end of this file. else # "variables.mk" was included. Business as usual until the end of this file.
all: sqlite.nim all: sqlcipher
# must be included after the default target # must be included after the default target
-include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk -include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk
@ -56,24 +53,20 @@ else
detected_OS := $(strip $(shell uname)) detected_OS := $(strip $(shell uname))
endif endif
clean: | clean-common clean-build-dirs clean-generator clean-nimterop clean: | clean-common clean-build-dirs clean-sqlcipher
clean-build-dirs: clean-build-dirs:
rm -rf \ rm -rf \
sqlcipher \ lib \
sqlite \ sqlite \
test/build test/build
clean-generator: clean-sqlcipher:
rm -rf \ cd vendor/sqlcipher && git clean -dfx $(HANDLE_OUTPUT)
generator/generate \ ([[ $(detected_OS) = Windows ]] && \
generator/generate.exe \ cd vendor/sqlcipher && \
generator/generate.dSYM git stash $(HANDLE_OUTPUT) && \
git stash drop $(HANDLE_OUTPUT)) || true
clean-nimterop:
rm -rf \
$(NIMTEROP_TOAST) \
$(NIMTEROP_TOAST).dSYM
deps: | deps-common deps: | deps-common
@ -105,14 +98,14 @@ else
SSL_LDFLAGS_SQLITE3_C ?= -L$(SSL_LIB_DIR) $(SSL_LDFLAGS) SSL_LDFLAGS_SQLITE3_C ?= -L$(SSL_LIB_DIR) $(SSL_LDFLAGS)
endif endif
SQLITE_STATIC ?= true SQLCIPHER_STATIC ?= true
SQLITE_CDEFS ?= -DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=3 SQLCIPHER_CDEFS ?= -DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=3
SQLITE_CFLAGS ?= -I$(SSL_INCLUDE_DIR) -pthread SQLCIPHER_CFLAGS ?= -I$(SSL_INCLUDE_DIR) -pthread
ifndef SQLITE_LDFLAGS ifndef SQLCIPHER_LDFLAGS
ifeq ($(detected_OS),Windows) ifeq ($(detected_OS),Windows)
SQLITE_LDFLAGS := -lwinpthread SQLCIPHER_LDFLAGS := -lwinpthread
else else
SQLITE_LDFLAGS := -lpthread SQLCIPHER_LDFLAGS := -lpthread
endif endif
endif endif
@ -124,35 +117,31 @@ $(SQLITE3_C): | deps
+ mkdir -p sqlite + mkdir -p sqlite
cd vendor/sqlcipher && \ cd vendor/sqlcipher && \
./configure \ ./configure \
CFLAGS="$(SQLITE_CDEFS) $(SQLITE_CFLAGS)" \ CFLAGS="$(SQLCIPHER_CDEFS) $(SQLCIPHER_CFLAGS)" \
LDFLAGS="$(SQLITE_LDFLAGS) $(SSL_LDFLAGS_SQLITE3_C)" \ LDFLAGS="$(SQLCIPHER_LDFLAGS) $(SSL_LDFLAGS_SQLITE3_C)" \
$(HANDLE_OUTPUT) $(HANDLE_OUTPUT)
cd vendor/sqlcipher && $(MAKE) sqlite3.c $(HANDLE_OUTPUT) cd vendor/sqlcipher && $(MAKE) sqlite3.c $(HANDLE_OUTPUT)
cp \ cp \
vendor/sqlcipher/sqlite3.c \ vendor/sqlcipher/sqlite3.c \
vendor/sqlcipher/sqlite3.h \ vendor/sqlcipher/sqlite3.h \
sqlite/ sqlite/
cd vendor/sqlcipher && git clean -dfx $(HANDLE_OUTPUT) $(MAKE) clean-sqlcipher
([[ $(detected_OS) = Windows ]] && \
cd vendor/sqlcipher && \
git stash $(HANDLE_OUTPUT) && \
git stash drop $(HANDLE_OUTPUT)) || true
sqlite3.c: $(SQLITE3_C) sqlite3.c: $(SQLITE3_C)
SQLITE_STATIC_LIB ?= $(shell pwd)/sqlcipher/sqlcipher.a SQLCIPHER_STATIC_LIB ?= $(shell pwd)/lib/libsqlcipher.a
SQLITE_STATIC_OBJ ?= sqlcipher/sqlcipher.o SQLCIPHER_STATIC_OBJ ?= lib/sqlcipher.o
$(SQLITE_STATIC_LIB): $(SQLITE3_C) $(SQLCIPHER_STATIC_LIB): $(SQLITE3_C)
echo -e $(BUILD_MSG) "SQLCipher static library" echo -e $(BUILD_MSG) "SQLCipher static library"
+ mkdir -p sqlcipher + mkdir -p lib
$(ENV_SCRIPT) $(CC) \ $(ENV_SCRIPT) $(CC) \
$(SQLITE_CDEFS) \ $(SQLCIPHER_CDEFS) \
$(SQLITE_CFLAGS) \ $(SQLCIPHER_CFLAGS) \
$(SQLITE3_C) \ $(SQLITE3_C) \
-c \ -c \
-o $(SQLITE_STATIC_OBJ) $(HANDLE_OUTPUT) -o $(SQLCIPHER_STATIC_OBJ) $(HANDLE_OUTPUT)
$(ENV_SCRIPT) ar rcs $(SQLITE_STATIC_LIB) $(SQLITE_STATIC_OBJ) $(HANDLE_OUTPUT) $(ENV_SCRIPT) ar rcs $(SQLCIPHER_STATIC_LIB) $(SQLCIPHER_STATIC_OBJ) $(HANDLE_OUTPUT)
ifndef SHARED_LIB_EXT ifndef SHARED_LIB_EXT
ifeq ($(detected_OS),macOS) ifeq ($(detected_OS),macOS)
@ -164,7 +153,7 @@ ifndef SHARED_LIB_EXT
endif endif
endif endif
SQLITE_SHARED_LIB ?= $(shell pwd)/sqlcipher/libsqlcipher.$(SHARED_LIB_EXT) SQLCIPHER_SHARED_LIB ?= $(shell pwd)/lib/libsqlcipher.$(SHARED_LIB_EXT)
ifndef PLATFORM_FLAGS_SHARED_LIB ifndef PLATFORM_FLAGS_SHARED_LIB
ifeq ($(detected_OS),macOS) ifeq ($(detected_OS),macOS)
@ -174,73 +163,36 @@ ifndef PLATFORM_FLAGS_SHARED_LIB
endif endif
endif endif
$(SQLITE_SHARED_LIB): $(SQLITE3_C) $(SQLCIPHER_SHARED_LIB): $(SQLITE3_C)
echo -e $(BUILD_MSG) "SQLCipher shared library" echo -e $(BUILD_MSG) "SQLCipher shared library"
+ mkdir -p sqlcipher + mkdir -p lib
$(ENV_SCRIPT) $(CC) \ $(ENV_SCRIPT) $(CC) \
$(SQLITE_CDEFS) \ $(SQLCIPHER_CDEFS) \
$(SQLITE_CFLAGS) \ $(SQLCIPHER_CFLAGS) \
$(SQLITE3_C) \ $(SQLITE3_C) \
$(SQLITE_LDFLAGS) \ $(SQLCIPHER_LDFLAGS) \
$(SSL_LDFLAGS) \ $(SSL_LDFLAGS) \
$(PLATFORM_FLAGS_SHARED_LIB) \ $(PLATFORM_FLAGS_SHARED_LIB) \
-o $(SQLITE_SHARED_LIB) $(HANDLE_OUTPUT) -o $(SQLCIPHER_SHARED_LIB) $(HANDLE_OUTPUT)
ifndef SQLITE_LIB ifndef SQLCIPHER_LIB
ifneq ($(SQLITE_STATIC),false) ifneq ($(SQLCIPHER_STATIC),false)
SQLITE_LIB := $(SQLITE_STATIC_LIB) SQLCIPHER_LIB := $(SQLCIPHER_STATIC_LIB)
else else
SQLITE_LIB := $(SQLITE_SHARED_LIB) SQLCIPHER_LIB := $(SQLCIPHER_SHARED_LIB)
endif endif
endif endif
sqlite: $(SQLITE_LIB) sqlcipher: $(SQLCIPHER_LIB)
ifndef NIMTEROP_TOAST
ifeq ($(detected_OS),Windows)
NIMTEROP_TOAST := vendor/nimterop/nimterop/toast.exe
else
NIMTEROP_TOAST := vendor/nimterop/nimterop/toast
endif
endif
$(NIMTEROP_TOAST): | deps
echo -e $(BUILD_MSG) "Nimterop toast"
+ cd vendor/nimterop && \
$(ENV_SCRIPT) nim c $(NIM_PARAMS) \
--define:danger \
--hints:off \
--nimcache:../../nimcache/nimterop \
nimterop/toast.nim
rm -rf $(NIMTEROP_TOAST).dSYM
toast: $(NIMTEROP_TOAST)
SQLITE_NIM ?= sqlcipher/sqlite.nim
$(SQLITE_NIM): $(NIMTEROP_TOAST) $(SQLITE_LIB)
echo -e $(BUILD_MSG) "Nim wrapper for SQLCipher"
+ mkdir -p sqlcipher
SQLITE_CDEFS="$(SQLITE_CDEFS)" \
SQLITE_STATIC="$(SQLITE_STATIC)" \
SQLITE3_H="$(SQLITE3_H)" \
SQLITE_LIB="$(SQLITE_LIB)" \
$(ENV_SCRIPT) nim c $(NIM_PARAMS) \
--nimcache:nimcache/sqlcipher \
--verbosity:0 \
generator/generate.nim > $(SQLITE_NIM) 2> /dev/null
$(MAKE) clean-generator
sqlite.nim: $(SQLITE_NIM)
# LD_LIBRARY_PATH is supplied when running tests on Linux # LD_LIBRARY_PATH is supplied when running tests on Linux
# PATH is supplied when running tests on Windows # PATH is supplied when running tests on Windows
ifeq ($(SQLITE_STATIC),false) ifeq ($(SQLCIPHER_STATIC),false)
PATH_TEST ?= $(shell dirname $(SQLITE_SHARED_LIB))::$${PATH} PATH_TEST ?= $(shell dirname $(SQLCIPHER_LIB))::$${PATH}
ifeq ($(SSL_STATIC),false) ifeq ($(SSL_STATIC),false)
LD_LIBRARY_PATH_TEST ?= $(shell dirname $(SQLITE_SHARED_LIB)):$(SSL_LIB_DIR)$${LD_LIBRARY_PATH:+:$${LD_LIBRARY_PATH}} LD_LIBRARY_PATH_TEST ?= $(shell dirname $(SQLCIPHER_LIB)):$(SSL_LIB_DIR)$${LD_LIBRARY_PATH:+:$${LD_LIBRARY_PATH}}
else else
LD_LIBRARY_PATH_TEST ?= $(shell dirname $(SQLITE_SHARED_LIB))$${LD_LIBRARY_PATH:+:$${LD_LIBRARY_PATH}} LD_LIBRARY_PATH_TEST ?= $(shell dirname $(SQLCIPHER_LIB))$${LD_LIBRARY_PATH:+:$${LD_LIBRARY_PATH}}
endif endif
else else
PATH_TEST ?= $${PATH} PATH_TEST ?= $${PATH}
@ -251,18 +203,27 @@ else
endif endif
endif endif
test: $(SQLITE_NIM) ifeq ($(SQLCIPHER_STATIC),false)
SQLCIPHER_LDFLAGS_TEST := -L$(shell dirname $(SQLCIPHER_LIB)) -lsqlcipher
else
SQLCIPHER_LDFLAGS_TEST := $(SQLCIPHER_LIB)
endif
test: $(SQLCIPHER_LIB)
ifeq ($(detected_OS),macOS) ifeq ($(detected_OS),macOS)
SQLCIPHER_LDFLAGS="$(SQLCIPHER_LDFLAGS_TEST)" \
SSL_LDFLAGS="$(SSL_LDFLAGS)" \ SSL_LDFLAGS="$(SSL_LDFLAGS)" \
SSL_STATIC="$(SSL_STATIC)" \ SSL_STATIC="$(SSL_STATIC)" \
$(ENV_SCRIPT) nimble tests $(ENV_SCRIPT) nimble tests
else ifeq ($(detected_OS),Windows) else ifeq ($(detected_OS),Windows)
PATH="$(PATH_TEST)" \ PATH="$(PATH_TEST)" \
SQLCIPHER_LDFLAGS="$(SQLCIPHER_LDFLAGS_TEST)" \
SSL_LDFLAGS="$(SSL_LDFLAGS)" \ SSL_LDFLAGS="$(SSL_LDFLAGS)" \
SSL_STATIC="$(SSL_STATIC)" \ SSL_STATIC="$(SSL_STATIC)" \
$(ENV_SCRIPT) nimble tests $(ENV_SCRIPT) nimble tests
else else
LD_LIBRARY_PATH="$(LD_LIBRARY_PATH_TEST)" \ LD_LIBRARY_PATH="$(LD_LIBRARY_PATH_TEST)" \
SQLCIPHER_LDFLAGS="$(SQLCIPHER_LDFLAGS_TEST)" \
SSL_LDFLAGS="$(SSL_LDFLAGS)" \ SSL_LDFLAGS="$(SSL_LDFLAGS)" \
SSL_STATIC="$(SSL_STATIC)" \ SSL_STATIC="$(SSL_STATIC)" \
$(ENV_SCRIPT) nimble tests $(ENV_SCRIPT) nimble tests

View File

@ -1,49 +0,0 @@
import macros
import nimterop/cimport
import os
import strutils
macro dynamicCdefine(): untyped =
var cdefs: seq[string]
for cdef in split(getEnv("SQLITE_CDEFS"), "-D"):
let stripped = strip(cdef)
if stripped != "":
cdefs.add(stripped)
result = newStmtList()
for cdef in cdefs:
result.add(newCall("cDefine", newStrLitNode(cdef)))
static:
cDebug()
cSkipSymbol(@[
"sqlite3_version",
"sqlite3_destructor_type"
])
dynamicCdefine()
when getEnv("SQLITE_STATIC") == "false":
cPassL("-L" & splitPath($getEnv("SQLITE_LIB")).head & " " & "-lsqlcipher")
when getEnv("SQLITE_STATIC") != "false":
cPassL($getEnv("SQLITE_LIB"))
cPlugin:
import strutils
var i = 0;
proc onSymbol*(sym: var Symbol) {.exportc, dynlib.} =
# Remove prefixes or suffixes from procs
if sym.kind == nskProc and sym.name.contains("sqlite3_"):
sym.name = sym.name.replace("sqlite3_", "")
# Workaround for duplicate iColumn symbol in generated Nim code
# (but generated code for sqlite3_index_info is likely not usable anyway)
if sym.name.contains("iColumn"):
if i == 0:
sym.name = sym.name.replace("iColumn", "iColumn_index_constraint")
else:
sym.name = sym.name.replace("iColumn", "iColumn_index_orderby")
i += 1
cImport($getEnv("SQLITE3_H"), flags = "-f:ast2")

View File

@ -1,376 +1,99 @@
import std / [options, macros, typetraits], sugar, sequtils include sqlcipher/tiny_sqlite
# sqlcipher/sqlite.nim must be generated before this module can be used. #
# To generate it use the `sqlite.nim` target of the Makefile in the same # Custom.DbConn
# directory as this file. #
from sqlcipher/sqlite as sqlite import nil proc all*[T](db: DbConn, _: typedesc[T], sql: string,
from stew/shims/macros as stew_macros import hasCustomPragmaFixed, getCustomPragmaFixed params: varargs[DbValue, toDbValue]): seq[T] =
## Executes ``statement`` and returns all result rows.
# Adapted from https://github.com/GULPF/tiny_sqlite for row in db.iterate(sql, params):
type
DbConn* = ptr sqlite.sqlite3
PreparedSql = sqlite.sqlite3_stmt
Callback = sqlite.sqlite3_callback
DbMode* = enum
dbRead,
dbReadWrite
SqliteError* = object of CatchableError ## \
## Raised when an error in the underlying SQLite library
## occurs.
errorCode*: int32 ## \
## This is the error code that was returned by the underlying
## SQLite library.
DbValueKind* = enum ## \
## Enum of all possible value types in a Sqlite database.
sqliteNull,
sqliteInteger,
sqliteReal,
sqliteText,
sqliteBlob
DbValue* = object ## \
## Represents a value in a SQLite database.
case kind*: DbValueKind
of sqliteInteger:
intVal*: int64
of sqliteReal:
floatVal*: float64
of sqliteText:
strVal*: string
of sqliteBlob:
blobVal*: seq[byte]
of sqliteNull:
discard
DbColumn* = object
name*: string
val*: DbValue
DbRow* = seq[DbColumn]
Tbind_destructor_func* = proc (para1: pointer){.cdecl, locks: 0, tags: [], raises: [], gcsafe.}
const
SQLITE_STATIC* = nil
SQLITE_TRANSIENT* = cast[Tbind_destructor_func](-1)
proc newSqliteError(db: DbConn, errorCode: int32): ref SqliteError =
## Raises a SqliteError exception.
(ref SqliteError)(
msg: $sqlite.errmsg(db),
errorCode: errorCode
)
template checkRc(db: DbConn, rc: int32) =
if rc != sqlite.SQLITE_OK:
raise newSqliteError(db, rc)
proc prepareSql(db: DbConn, sql: string, params: seq[DbValue]): ptr PreparedSql
{.raises: [SqliteError].} =
var tail: cstring
let rc = sqlite.prepare_v2(db, sql.cstring, sql.len.cint, addr result, addr tail)
assert tail.len == 0,
"`exec` and `execMany` can only be used with a single SQL statement. " &
"To execute several SQL statements, use `execScript`"
db.checkRc(rc)
var idx = 1'i32
for value in params:
let rc =
case value.kind
of sqliteNull: sqlite.bind_null(result, idx)
of sqliteInteger: sqlite.bind_int64(result, idx, value.intval)
of sqliteReal: sqlite.bind_double(result, idx, value.floatVal)
of sqliteText: sqlite.bind_text(result, idx, value.strVal.cstring,
value.strVal.len.int32, SQLITE_TRANSIENT)
of sqliteBlob: sqlite.bind_blob(result, idx.int32,
cast[string](value.blobVal).cstring,
value.blobVal.len.int32, SQLITE_TRANSIENT)
sqlite.db_handle(result).checkRc(rc)
idx.inc
proc next(prepared: ptr PreparedSql): bool =
## Advance cursor by one row.
## Return ``true`` if there are more rows.
let rc = sqlite.step(prepared)
if rc == sqlite.SQLITE_ROW:
result = true
elif rc == sqlite.SQLITE_DONE:
result = false
else:
raise newSqliteError(sqlite.db_handle(prepared), rc)
proc finalize(prepared: ptr PreparedSql) =
## Finalize statement or raise SqliteError if not successful.
let rc = sqlite.finalize(prepared)
sqlite.db_handle(prepared).checkRc(rc)
proc toDbValue*[T: Ordinal](val: T): DbValue =
DbValue(kind: sqliteInteger, intVal: val.int64)
proc toDbValue*[T: SomeFloat](val: T): DbValue =
DbValue(kind: sqliteReal, floatVal: val)
proc toDbValue*[T: string](val: T): DbValue =
DbValue(kind: sqliteText, strVal: val)
proc toDbValue*[T: seq[byte]](val: T): DbValue =
DbValue(kind: sqliteBlob, blobVal: val)
proc toDbValue*[T: Option](val: T): DbValue =
if val.isNone:
DbValue(kind: sqliteNull)
else:
toDbValue(val.get)
when (NimMajor, NimMinor, NimPatch) > (0, 19, 9):
proc toDbValue*[T: type(nil)](val: T): DbValue =
DbValue(kind: sqliteNull)
proc nilDbValue(): DbValue =
## Since above isn't available for older versions,
## we use this internally.
DbValue(kind: sqliteNull)
proc fromDbValue*(val: DbValue, T: typedesc[Ordinal]): T =
when T is bool:
if val.kind == DbValueKind.sqliteText:
return val.strVal.parseBool
val.intVal.T
proc fromDbValue*(val: DbValue, T: typedesc[SomeFloat]): float64 = val.floatVal
proc fromDbValue*(val: DbValue, T: typedesc[string]): string = val.strVal
proc fromDbValue*(val: DbValue, T: typedesc[seq[byte]]): seq[byte] = val.blobVal
proc fromDbValue*(val: DbValue, T: typedesc[DbValue]): T = val
proc fromDbValue*[T](val: DbValue, _: typedesc[Option[T]]): Option[T] =
if (val.kind == sqliteNull) or
(val.kind == sqliteText and val.strVal == "") or
(val.kind == sqliteInteger and val.intVal == 0):
none(T)
else:
some(val.fromDbValue(T))
# TODO: uncomment and test
#[
proc unpack*[T: tuple](row: openArray[DbValue], _: typedesc[T]): T =
## Call ``fromDbValue`` on each element of ``row`` and return it
## as a tuple.
var idx = 0
for value in result.fields:
value = row[idx].fromDbValue(type(value))
idx.inc
proc `$`*(dbVal: DbValue): string =
result.add "DbValue["
case dbVal.kind
of sqliteInteger: result.add $dbVal.intVal
of sqliteReal: result.add $dbVal.floatVal
of sqliteText: result.addQuoted dbVal.strVal
of sqliteBlob: result.add "<blob>"
of sqliteNull: result.add "nil"
result.add "]"
]#
proc exec*(db: DbConn, sql: string, params: varargs[DbValue, toDbValue]) =
## Executes ``sql`` and raises SqliteError if not successful.
assert (not db.isNil), "Database is nil"
let prepared = db.prepareSql(sql, @params)
defer: prepared.finalize()
discard prepared.next
#[
# TODO: uncomment and test
proc execMany*(db: DbConn, sql: string, params: seq[seq[DbValue]]) =
## Executes ``sql`` repeatedly using each element of ``params`` as parameters.
assert (not db.isNil), "Database is nil"
for p in params:
db.exec(sql, p)
]#
# Executes a non-query -- there are no results returned from the execution.
proc execScript*(db: DbConn, sql: string) =
## Executes the query and raises SqliteError if not successful.
assert (not db.isNil), "Database is nil"
let rc = sqlite.exec(db, sql.cstring, nil, nil, nil)
db.checkRc(rc)
# TODO: uncomment and test
#[
template transaction*(db: DbConn, body: untyped) =
db.exec("BEGIN")
var ok = true
try:
try:
body
except Exception as ex:
ok = false
db.exec("ROLLBACK")
raise ex
finally:
if ok:
db.exec("COMMIT")
]#
proc readColumn(prepared: ptr PreparedSql, col: int32): DbValue {.deprecated: "Use readDbColumn".} =
let columnType = sqlite.column_type(prepared, col)
case columnType
of sqlite.SQLITE_INTEGER:
result = toDbValue(sqlite.column_int64(prepared, col))
of sqlite.SQLITE_FLOAT:
result = toDbValue(sqlite.column_double(prepared, col))
of sqlite.SQLITE_TEXT:
result = toDbValue($sqlite.column_text(prepared, col))
of sqlite.SQLITE_BLOB:
let blob = sqlite.column_blob(prepared, col)
let bytes = sqlite.column_bytes(prepared, col)
var s = newSeq[byte](bytes)
if bytes != 0:
copyMem(addr(s[0]), blob, bytes)
result = toDbValue(s)
of sqlite.SQLITE_NULL:
result = nilDbValue()
else:
raiseAssert "Unexpected column type: " & $columnType
proc readDbColumn(prepared: ptr PreparedSql, col: int32): DbColumn =
let
columnType = sqlite.column_type(prepared, col)
# FIXME: This is NOT the correct way to get a string from a cstring and
# may result in loss of data after a NULL termination!
columnName = $sqlite.column_name(prepared, col)
case columnType
of sqlite.SQLITE_INTEGER:
result = DbColumn(name: columnName, val: toDbValue(sqlite.column_int64(prepared, col)))
of sqlite.SQLITE_FLOAT:
result = DbColumn(name: columnName, val: toDbValue(sqlite.column_double(prepared, col)))
of sqlite.SQLITE_TEXT:
result = DbColumn(name: columnName, val: toDbValue($sqlite.column_text(prepared, col)))
of sqlite.SQLITE_BLOB:
let blob = sqlite.column_blob(prepared, col)
let bytes = sqlite.column_bytes(prepared, col)
var s = newSeq[byte](bytes)
if bytes != 0:
copyMem(addr(s[0]), blob, bytes)
result = DbColumn(name: columnName, val: toDbValue(s))
of sqlite.SQLITE_NULL:
result = DbColumn(name: columnName, val: nilDbValue())
else:
raiseAssert "Unexpected column type: " & $columnType
iterator rows*(db: DbConn, sql: string,
params: varargs[DbValue, toDbValue]): seq[DbValue] {.deprecated: "Use execQuery instead".} =
## Executes the query and iterates over the result dataset.
assert (not db.isNil), "Database is nil"
let prepared = db.prepareSql(sql, @params)
defer: prepared.finalize()
var row = newSeq[DbValue](sqlite.column_count(prepared))
while prepared.next:
for col, _ in row:
row[col] = readColumn(prepared, col.int32)
yield row
proc rows*(db: DbConn, sql: string,
params: varargs[DbValue, toDbValue]): seq[seq[DbValue]] {.deprecated: "Use execQuery instead".} =
## Executes the query and returns the resulting rows.
for row in db.rows(sql, params):
result.add row
proc execQuery*[T](db: DbConn, sql: string,
params: varargs[DbValue, toDbValue]): seq[T] =
## Executes the query and iterates over the result dataset.
assert (not db.isNil), "Database is nil"
let prepared = db.prepareSql(sql, @params)
defer: prepared.finalize()
var row = newSeq[DbColumn](sqlite.column_count(prepared))
while prepared.next:
for col, _ in row:
row[col] = readDbColumn(prepared, col.int32)
var r = T() var r = T()
row.to(r) row.unpack(r)
result.add r result.add r
proc openDatabase*(path: string, mode = dbReadWrite): DbConn = proc one*[T](db: DbConn, _: typedesc[T], sql: string,
## Open a new database connection to a database file. To create a params: varargs[DbValue, toDbValue]): Option[T] =
## in-memory database the special path `":memory:"` can be used. ## Executes `sql`, which must be a single SQL statement, and returns the first result row.
## If the database doesn't already exist and ``mode`` is ``dbReadWrite``, ## Returns `none(seq[DbValue])` if the result was empty.
## the database will be created. If the database doesn't exist and ``mode`` for row in db.iterate(sql, params):
## is ``dbRead``, a ``SqliteError`` exception will be raised. var r = T()
## row.unpack(r)
## NOTE: To avoid memory leaks, ``db.close`` must be called when the return some(r)
## database connection is no longer needed.
runnableExamples:
let memDb = openDatabase(":memory:")
case mode
of dbReadWrite:
let rc = sqlite.open(path, addr result)
result.checkRc(rc)
of dbRead:
let rc = sqlite.open_v2(path, addr result, sqlite.SQLITE_OPEN_READONLY, nil)
result.checkRc(rc)
proc key*(db: DbConn, password: string) = proc value*[T](db: DbConn, _: typedesc[T], sql: string,
let rc = sqlite.key(db, password.cstring, int32(password.len)) params: varargs[DbValue, toDbValue]): Option[T] =
db.checkRc(rc) ## Executes `sql`, which must be a single SQL statement, and returns the first column of the first result row.
## Returns `none(DbValue)` if the result was empty.
for row in db.iterate(sql, params):
return some(row.values[0].fromDbValue(T))
proc rekey*(db: DbConn, password: string) = #
let rc = sqlite.rekey(db, password.cstring, int32(password.len)) # Custom.SqlStatement
db.checkRc(rc) #
proc all*[T](_: typedesc[T], statement: SqlStatement, params: varargs[DbValue, toDbValue]): seq[T] =
## Executes ``statement`` and returns all result rows.
assertCanUseStatement statement
for row in statement.iterate(params):
var r = T()
row.unpack(r)
result.add r
proc close*(db: DbConn) = proc one*[T](_: typedesc[T], statement: SqlStatement,
## Closes the database connection. params: varargs[DbValue, toDbValue]): Option[T] =
let rc = sqlite.close(db) ## Executes `statement` and returns the first row found.
db.checkRc(rc) ## Returns `none(seq[DbValue])` if no result was found.
assertCanUseStatement statement
for row in statement.iterate(params):
var r = T()
row.unpack(r)
return some(r)
# TODO: test proc value*[T](_: typedesc[T], statement: SqlStatement,
#[ params: varargs[DbValue, toDbValue]): Option[T] =
## Executes `statement` and returns the first column of the first row found.
## Returns `none(DbValue)` if no result was found.
assertCanUseStatement statement
for row in statement.iterate(params):
return some(row.values[0].fromDbValue(T))
proc lastInsertRowId*(db: DbConn): int64 = #
## Get the row id of the last inserted row. # Custom.ResultRow
## For tables with an integer primary key, #
## the row id will be the primary key. proc `[]`*[T](row: ResultRow, columnName: string, _: typedesc[T]): T =
## row[columnName].fromDbValue(T)
## For more information, refer to the SQLite documentation
## (https://www.sqlite.org/c3ref/last_insert_rowid.html).
sqlite.last_insert_rowid(db)
proc changes*(db: DbConn): int32 = proc hasRows*(rows: seq[ResultRow]): bool = rows.len > 0
## Get the number of changes triggered by the most recent INSERT, UPDATE or
## DELETE statement.
##
## For more information, refer to the SQLite documentation
## (https://www.sqlite.org/c3ref/changes.html).
sqlite.changes(db)
proc isReadonly*(db: DbConn): bool = #
## Returns true if ``db`` is in readonly mode. # Custom.ORM
sqlite.db_readonly(db, "main") == 1 # This section was not originally part of tiny_sqlite
]# #
proc col*[T](row: DbRow, columnName: string): T =
let results = row.filter((column: DbColumn) => column.name == columnName)
if results.len == 0:
return default(T)
results[0].val.fromDbValue(T)
# TODO: add primaryKey param to pragma, however there is an issue with multiple
# params in getCustomPragmaFixed: https://github.com/status-im/nim-stew/issues/62,
# and we need to wait on a fix or a workaround.
template dbColumnName*(name: string) {.pragma.} template dbColumnName*(name: string) {.pragma.}
## Specifies the database column name for the object property ## Specifies the database column name for the object property
template dbTableName*(name: string) {.pragma.}
## Specifies the database table name for the object
template dbForeignKey*(t: typedesc) {.pragma.}
## Specifies the table's foreign key type
template columnName*(obj: auto | typedesc): string =
when macros.hasCustomPragma(obj, dbColumnName):
macros.getCustomPragmaVal(obj, dbColumnName)
else:
typetraits.name(obj.type).toLower
template tableName*(obj: auto | typedesc): string =
when macros.hasCustomPragma(obj, dbTableName):
macros.getCustomPragmaVal(obj, dbTableName)
else:
typetraits.name(obj.type).toLower
template enumInstanceDbColumns*(obj: auto, template enumInstanceDbColumns*(obj: auto,
fieldNameVar, fieldVar, fieldNameVar, fieldVar,
body: untyped) = body: untyped) =
@ -391,9 +114,56 @@ template enumInstanceDbColumns*(obj: auto,
const fieldNameVar = fieldName const fieldNameVar = fieldName
body body
proc to*(row: DbRow, obj: var object) = proc unpack*(row: ResultRow, obj: var object) =
obj.enumInstanceDbColumns(dbColName, property): obj.enumInstanceDbColumns(dbColName, property):
type ColType = type property type ColType = type property
property = col[ColType](row, dbColName) property = row[dbColName, ColType]
proc hasRows*(rows: seq[DbRow]): bool = rows.len > 0 #
# Custom.sqlcipher
# The following are APIs from sqlcipher
#
proc key*(db: DbConn, password: string) =
## * Specify the key for an encrypted database. This routine should be
## * called right after sqlite3_open().
## *
## * The code to implement this API is not available in the public release
## * of SQLite.
let rc = sqlite.key(db.handle, password.cstring, int32(password.len))
db.checkRc(rc)
proc key_v2*(db: DbConn, zDbName, password: string) =
## * Specify the key for an encrypted database. This routine should be
## * called right after sqlite3_open().
## *
## * The code to implement this API is not available in the public release
## * of SQLite.
let rc = sqlite.key_v2(db.handle, zDbName.cstring, password.cstring, int32(password.len))
db.checkRc(rc)
proc rekey*(db: DbConn, password: string) =
let rc = sqlite.rekey(db.handle, password.cstring, int32(password.len))
db.checkRc(rc)
## * Change the key on an open database. If the current database is not
## * encrypted, this routine will encrypt it. If pNew==0 or nNew==0, the
## * database is decrypted.
## *
## * The code to implement this API is not available in the public release
## * of SQLite.
proc rekey_v2*(db: DbConn, zDbName, password: string) =
## * Change the key on an open database. If the current database is not
## * encrypted, this routine will encrypt it. If pNew==0 or nNew==0, the
## * database is decrypted.
## *
## * The code to implement this API is not available in the public release
## * of SQLite.
let rc = sqlite.rekey_v2(db.handle, zDbName.cstring, password.cstring, int32(password.len))
db.checkRc(rc)
#
# Custom.Deprecations
#
proc execQuery*[T](db: DbConn, sql: string, params: varargs[DbValue, toDbValue]): seq[T] {.deprecated: "Use all[T] instead".} =
## Executes the query and iterates over the result dataset.
all[T](db, sql, T.type, params)

View File

@ -31,6 +31,7 @@ proc buildAndRunTest(name: string,
(if getEnv("SSL_STATIC").strip != "false": " --dynlibOverride:ssl" else: "") & (if getEnv("SSL_STATIC").strip != "false": " --dynlibOverride:ssl" else: "") &
" --nimcache:nimcache/test/" & name & " --nimcache:nimcache/test/" & name &
" --out:" & outDir & name & " --out:" & outDir & name &
(if getEnv("SQLCIPHER_LDFLAGS").strip != "": " --passL:\"" & getEnv("SQLCIPHER_LDFLAGS") & "\"" else: "") &
(if getEnv("SSL_LDFLAGS").strip != "": " --passL:\"" & getEnv("SSL_LDFLAGS") & "\"" else: "") & (if getEnv("SSL_LDFLAGS").strip != "": " --passL:\"" & getEnv("SSL_LDFLAGS") & "\"" else: "") &
" --threads:on" & " --threads:on" &
" --tlsEmulation:off" & " --tlsEmulation:off" &

View File

@ -0,0 +1,81 @@
## Implements a least-recently-used cache for prepared statements based on
## https://github.com/jackhftang/lrucache.nim.
import std / [lists, tables]
from ../sqlite_wrapper as sqlite import nil
type
Node = object
key: string
val: sqlite.Stmt
StmtCache* = object
capacity: int
list: DoublyLinkedList[Node]
table: Table[string, DoublyLinkedNode[Node]]
proc initStmtCache*(capacity: Natural): StmtCache =
## Create a new Least-Recently-Used (LRU) cache that store the last `capacity`-accessed items.
StmtCache(
capacity: capacity,
list: initDoublyLinkedList[Node](),
table: initTable[string, DoublyLinkedNode[Node]](rightSize(capacity))
)
proc resize(cache: var StmtCache) =
while cache.table.len > cache.capacity:
let t = cache.list.tail
cache.table.del(t.value.key)
discard sqlite.finalize(t.value.val)
cache.list.remove t
proc capacity*(cache: StmtCache): int =
## Get the maximum capacity of cache
cache.capacity
proc len*(cache: StmtCache): int =
## Return number of keys in cache
cache.table.len
proc contains*(cache: StmtCache, key: string): bool =
## Check whether key in cache. Does *NOT* update recentness.
cache.table.contains(key)
proc clear*(cache: var StmtCache) =
## remove all items
cache.list = initDoublyLinkedList[Node]()
cache.table.clear()
proc `[]`*(cache: var StmtCache, key: string): sqlite.Stmt =
## Read value from `cache` by `key` and update recentness
## Raise `KeyError` if `key` is not in `cache`.
let node = cache.table[key]
result = node.value.val
cache.list.remove node
cache.list.prepend node
proc `[]=`*(cache: var StmtCache, key: string, val: sqlite.Stmt) =
## Put value `v` in cache with key `k`.
## Remove least recently used value from cache if length exceeds capacity.
var node = cache.table.getOrDefault(key, nil)
if node.isNil:
let node = newDoublyLinkedNode[Node](
Node(key: key, val: val)
)
cache.table[key] = node
cache.list.prepend node
cache.resize()
else:
# set value
node.value.val = val
# move to head
cache.list.remove node
cache.list.prepend node
proc getOrDefault*(cache: StmtCache, key: string, val: sqlite.Stmt = nil): sqlite.Stmt =
## Similar to get, but return `val` if `key` is not in `cache`
let node = cache.table.getOrDefault(key, nil)
if node.isNil:
result = val
else:
result = node.value.val

View File

@ -0,0 +1,292 @@
type
Sqlite3* = ptr object
Stmt* = ptr object
Callback* = proc (p: pointer, para2: cint, para3, para4: cstringArray): cint
{.cdecl, raises: [].}
SqliteDestructor* = proc (p: pointer)
{.cdecl, locks: 0, tags: [], raises: [], gcsafe.}
const
SQLITE_OK* = 0.cint
SQLITE_ERROR* = 1.cint # SQL error or missing database
SQLITE_INTERNAL* = 2.cint # An internal logic error in SQLite
SQLITE_PERM* = 3.cint # Access permission denied
SQLITE_ABORT* = 4.cint # Callback routine requested an abort
SQLITE_BUSY* = 5.cint # The database file is locked
SQLITE_LOCKED* = 6.cint # A table in the database is locked
SQLITE_NOMEM* = 7.cint # A malloc() failed
SQLITE_READONLY* = 8.cint # Attempt to write a readonly database
SQLITE_INTERRUPT* = 9.cint # Operation terminated by sqlite3_interrupt()
SQLITE_IOERR* = 10.cint # Some kind of disk I/O error occurred
SQLITE_CORRUPT* = 11.cint # The database disk image is malformed
SQLITE_NOTFOUND* = 12.cint # (Internal Only) Table or record not found
SQLITE_FULL* = 13.cint # Insertion failed because database is full
SQLITE_CANTOPEN* = 14.cint # Unable to open the database file
SQLITE_PROTOCOL* = 15.cint # Database lock protocol error
SQLITE_EMPTY* = 16.cint # Database is empty
SQLITE_SCHEMA* = 17.cint # The database schema changed
SQLITE_TOOBIG* = 18.cint # Too much data for one row of a table
SQLITE_CONSTRAINT* = 19.cint # Abort due to contraint violation
SQLITE_MISMATCH* = 20.cint # Data type mismatch
SQLITE_MISUSE* = 21.cint # Library used incorrectly
SQLITE_NOLFS* = 22.cint # Uses OS features not supported on host
SQLITE_AUTH* = 23.cint # Authorization denied
SQLITE_FORMAT* = 24.cint # Auxiliary database format error
SQLITE_RANGE* = 25.cint # 2nd parameter to sqlite3_bind out of range
SQLITE_NOTADB* = 26.cint # File opened that is not a database file
SQLITE_NOTICE* = 27.cint
SQLITE_WARNING* = 28.cint
SQLITE_ROW* = 100.cint # sqlite3_step() has another row ready
SQLITE_DONE* = 101.cint # sqlite3_step() has finished executing
const
SQLITE_INTEGER* = 1.cint
SQLITE_FLOAT* = 2.cint
SQLITE_TEXT* = 3.cint
SQLITE_BLOB* = 4.cint
SQLITE_NULL* = 5.cint
SQLITE_UTF8* = 1.cint
SQLITE_UTF16LE* = 2.cint
SQLITE_UTF16BE* = 3.cint # Use native byte order
SQLITE_UTF16* = 4.cint # sqlite3_create_function only
SQLITE_ANY* = 5.cint #sqlite_exec return values
SQLITE_COPY* = 0.cint
SQLITE_CREATE_INDEX* = 1.cint
SQLITE_CREATE_TABLE* = 2.cint
SQLITE_CREATE_TEMP_INDEX* = 3.cint
SQLITE_CREATE_TEMP_TABLE* = 4.cint
SQLITE_CREATE_TEMP_TRIGGER* = 5.cint
SQLITE_CREATE_TEMP_VIEW* = 6.cint
SQLITE_CREATE_TRIGGER* = 7.cint
SQLITE_CREATE_VIEW* = 8.cint
SQLITE_DELETE* = 9.cint
SQLITE_DROP_INDEX* = 10.cint
SQLITE_DROP_TABLE* = 11.cint
SQLITE_DROP_TEMP_INDEX* = 12.cint
SQLITE_DROP_TEMP_TABLE* = 13.cint
SQLITE_DROP_TEMP_TRIGGER* = 14.cint
SQLITE_DROP_TEMP_VIEW* = 15.cint
SQLITE_DROP_TRIGGER* = 16.cint
SQLITE_DROP_VIEW* = 17.cint
SQLITE_INSERT* = 18.cint
SQLITE_PRAGMA* = 19.cint
SQLITE_READ* = 20.cint
SQLITE_SELECT* = 21.cint
SQLITE_TRANSACTION* = 22.cint
SQLITE_UPDATE* = 23.cint
SQLITE_ATTACH* = 24.cint
SQLITE_DETACH* = 25.cint
SQLITE_ALTER_TABLE* = 26.cint
SQLITE_REINDEX* = 27.cint
SQLITE_DENY* = 1.cint
SQLITE_IGNORE* = 2.cint
SQLITE_DETERMINISTIC* = 0x800.cint
const
SQLITE_OPEN_READONLY* = 0x00000001.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_READWRITE* = 0x00000002.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_CREATE* = 0x00000004.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_DELETEONCLOSE* = 0x00000008.cint #/* VFS only */
SQLITE_OPEN_EXCLUSIVE* = 0x00000010.cint #/* VFS only */
SQLITE_OPEN_AUTOPROXY* = 0x00000020.cint #/* VFS only */
SQLITE_OPEN_URI* = 0x00000040.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_MEMORY* = 0x00000080.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_MAIN_DB* = 0x00000100.cint #/* VFS only */
SQLITE_OPEN_TEMP_DB* = 0x00000200.cint #/* VFS only */
SQLITE_OPEN_TRANSIENT_DB* = 0x00000400.cint #/* VFS only */
SQLITE_OPEN_MAIN_JOURNAL* = 0x00000800.cint #/* VFS only */
SQLITE_OPEN_TEMP_JOURNAL* = 0x00001000.cint #/* VFS only */
SQLITE_OPEN_SUBJOURNAL* = 0x00002000.cint #/* VFS only */
SQLITE_OPEN_MASTER_JOURNAL* = 0x00004000.cint #/* VFS only */
SQLITE_OPEN_NOMUTEX* = 0x00008000.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_FULLMUTEX* = 0x00010000.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_SHAREDCACHE* = 0x00020000.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_PRIVATECACHE* = 0x00040000.cint #/* Ok for sqlite3_open_v2() */
SQLITE_OPEN_WAL* = 0x00080000.cint #/* VFS only */
const
SQLITE_STATIC* = nil
SQLITE_TRANSIENT* = cast[SqliteDestructor](-1)
proc close*(db: Sqlite3): cint
{.cdecl, importc: "sqlite3_close".}
proc exec*(db: Sqlite3, sql: cstring, cb: Callback, p: pointer, errmsg: var cstring): cint
{.cdecl, importc: "sqlite3_exec".}
proc last_insert_rowid*(db: Sqlite3): int64
{.cdecl, importc: "sqlite3_last_insert_rowid".}
proc changes*(db: Sqlite3): cint
{.cdecl, importc: "sqlite3_changes".}
proc total_changes*(db: Sqlite3): cint
{.cdecl, importc: "sqlite3_total_changes".}
proc busy_handler*(db: Sqlite3,
handler: proc (p: pointer, x: cint): cint {.cdecl.},
p: pointer): cint
{.cdecl, importc: "sqlite3_busy_handler".}
proc busy_timeout*(db: Sqlite3, ms: cint): cint
{.cdecl, importc: "sqlite3_busy_timeout".}
proc open*(filename: cstring, db: var Sqlite3): cint
{.cdecl, importc: "sqlite3_open".}
proc open_v2*(filename: cstring, db: var Sqlite3, flags: cint, zVfsName: cstring ): cint
{.cdecl, importc: "sqlite3_open_v2".}
proc errcode*(db: Sqlite3): cint
{.cdecl, importc: "sqlite3_errcode".}
proc errmsg*(db: Sqlite3): cstring
{.cdecl, importc: "sqlite3_errmsg".}
proc prepare_v2*(db: Sqlite3, zSql: cstring, nByte: cint, stmt: var Stmt,
pzTail: var cstring): cint
{.importc: "sqlite3_prepare_v2", cdecl.}
proc bind_blob*(stmt: Stmt, col: cint, value: pointer, len: cint,
para5: SqliteDestructor): cint
{.cdecl, importc: "sqlite3_bind_blob".}
proc bind_double*(stmt: Stmt, col: cint, value: float64): cint
{.cdecl, importc: "sqlite3_bind_double".}
proc bind_int*(stmt: Stmt, col: cint, value: cint): cint
{.cdecl, importc: "sqlite3_bind_int".}
proc bind_int64*(stmt: Stmt, col: cint, value: int64): cint
{.cdecl, importc: "sqlite3_bind_int64".}
proc bind_null*(stmt: Stmt, col: cint): cint
{.cdecl, importc: "sqlite3_bind_null".}
proc bind_text*(stmt: Stmt, col: cint, value: cstring, len: cint,
destructor: SqliteDestructor): cint
{.cdecl, importc: "sqlite3_bind_text".}
proc bind_parameter_count*(stmt: Stmt): cint
{.cdecl, importc: "sqlite3_bind_parameter_count".}
proc bind_parameter_name*(stmt: Stmt, col: cint): cstring
{.cdecl, importc: "sqlite3_bind_parameter_name".}
proc bind_parameter_index*(stmt: Stmt, colName: cstring): cint
{.cdecl, importc: "sqlite3_bind_parameter_index".}
proc clear_bindings*(stmt: Stmt): cint
{.cdecl, importc: "sqlite3_clear_bindings".}
proc column_count*(stmt: Stmt): cint
{.cdecl, importc: "sqlite3_column_count".}
proc column_name*(stmt: Stmt, col: cint): cstring
{.cdecl, importc: "sqlite3_column_name".}
proc column_table_name*(stmt: Stmt, col: cint): cstring
{.cdecl, importc: "sqlite3_column_table_name".}
proc column_decltype*(stmt: Stmt, col: cint): cstring
{.cdecl, importc: "sqlite3_column_decltype".}
proc step*(stmt: Stmt): cint
{.cdecl, importc: "sqlite3_step".}
proc data_count*(stmt: Stmt): cint
{.cdecl, importc: "sqlite3_data_count".}
proc column_blob*(stmt: Stmt, col: cint): pointer
{.cdecl, importc: "sqlite3_column_blob".}
proc column_bytes*(stmt: Stmt, col: cint): cint
{.cdecl, importc: "sqlite3_column_bytes".}
proc column_double*(stmt: Stmt, col: cint): float64
{.cdecl, importc: "sqlite3_column_double".}
proc column_int*(stmt: Stmt, col: cint): cint
{.cdecl, importc: "sqlite3_column_int".}
proc column_int64*(stmt: Stmt, col: cint): int64
{.cdecl, importc: "sqlite3_column_int64".}
proc column_text*(stmt: Stmt, col: cint): cstring
{.cdecl, importc: "sqlite3_column_text".}
proc column_type*(stmt: Stmt, col: cint): cint
{.cdecl, importc: "sqlite3_column_type".}
proc finalize*(stmt: Stmt): cint
{.cdecl, importc: "sqlite3_finalize".}
proc reset*(stmt: Stmt): cint
{.cdecl, importc: "sqlite3_reset".}
proc libversion*(): cstring
{.cdecl, importc: "sqlite3_libversion".}
proc libversion_number*(): cint
{.cdecl, importc: "sqlite3_libversion_number".}
proc db_handle*(stmt: Stmt): Sqlite3
{.cdecl, importc: "sqlite3_db_handle".}
proc get_autocommit*(db: Sqlite3): cint
{.cdecl, importc: "sqlite3_get_autocommit".}
proc db_readonly*(db: Sqlite3, dbname: cstring): cint
{.cdecl, importc: "sqlite3_db_readonly".}
proc next_stmt*(db: Sqlite3, stmt: Stmt): Stmt
{.cdecl, importc: "sqlite3_next_stmt".}
proc stmt_busy*(stmt: Stmt): bool
{.cdecl, importc: "sqlite3_stmt_busy".}
proc key*(db: Sqlite3, pKey: pointer, nKey: cint): cint {.importc: "sqlite3_key",
cdecl.}
## ```
## BEGIN SQLCIPHER
##
## * Specify the key for an encrypted database. This routine should be
## * called right after sqlite3_open().
## *
## * The code to implement this API is not available in the public release
## * of SQLite.
## ```
proc key_v2*(db: Sqlite3; zDbName: cstring; pKey: pointer; nKey: cint): cint {.
importc: "sqlite3_key_v2", cdecl.}
## ```
## BEGIN SQLCIPHER
##
## * Specify the key for an encrypted database. This routine should be
## * called right after sqlite3_open().
## *
## * The code to implement this API is not available in the public release
## * of SQLite.
## ```
proc rekey*(db: Sqlite3, pKey: pointer, nKey: cint): cint {.importc: "sqlite3_rekey",
cdecl.}
## ```
## * Change the key on an open database. If the current database is not
## * encrypted, this routine will encrypt it. If pNew==0 or nNew==0, the
## * database is decrypted.
## *
## * The code to implement this API is not available in the public release
## * of SQLite.
## ```
proc rekey_v2*(db: Sqlite3; zDbName: cstring; pKey: pointer; nKey: cint): cint {.
importc: "sqlite3_rekey_v2", cdecl.}
## ```
## * Change the key on an open database. If the current database is not
## * encrypted, this routine will encrypt it. If pNew==0 or nNew==0, the
## * database is decrypted.
## *
## * The code to implement this API is not available in the public release
## * of SQLite.
## ```

666
sqlcipher/tiny_sqlite.nim Normal file
View File

@ -0,0 +1,666 @@
import std / [options, macros, typetraits], sequtils, unicode
from sqlite_wrapper as sqlite import nil
from stew/shims/macros as stew_macros import hasCustomPragmaFixed, getCustomPragmaFixed
import private/stmtcache
## Adapted from https://github.com/GULPF/tiny_sqlite
## DO NOT MODIFY THE BELOW. ONLY MERGE IN CHANGES FROM UPSTREAM
## (https://github.com/GULPF/tiny_sqlite). If you need to make changes,
## add them in /sqlcipher.nim.
when not declared(tupleLen):
import macros
macro tupleLen(typ: typedesc[tuple]): int =
let impl = getType(typ)
result = newIntlitNode(impl[1].len - 1)
export options.get, options.isSome, options.isNone
type
DbConnImpl = ref object
handle: sqlite.Sqlite3 ## The underlying SQLite3 handle
cache: StmtCache
DbConn* = distinct DbConnImpl ## Encapsulates a database connection.
SqlStatementImpl = ref object
handle: sqlite.Stmt
db: DbConn
SqlStatement* = distinct SqlStatementImpl ## A prepared SQL statement.
DbMode* = enum
dbRead,
dbReadWrite
SqliteError* = object of CatchableError ## \
## Raised when whenever a database related error occurs.
## Errors are typically a result of API misuse,
## e.g trying to close an already closed database connection.
DbValueKind* = enum ## \
## Enum of all possible value types in a SQLite database.
sqliteNull,
sqliteInteger,
sqliteReal,
sqliteText,
sqliteBlob
DbValue* = object ## \
## Can represent any value in a SQLite database.
case kind*: DbValueKind
of sqliteInteger:
intVal*: int64
of sqliteReal:
floatVal*: float64
of sqliteText:
strVal*: string
of sqliteBlob:
blobVal*: seq[byte]
of sqliteNull:
discard
Rc = cint
ResultRow* = object
values: seq[DbValue]
columns: seq[string]
const SqliteRcOk = [ sqlite.SQLITE_OK, sqlite.SQLITE_DONE, sqlite.SQLITE_ROW ]
# Forward declarations
proc isInTransaction*(db: DbConn): bool {.noSideEffect.}
proc isOpen*(db: DbConn): bool {.noSideEffect, inline.}
template handle(db: DbConn): sqlite.Sqlite3 = DbConnImpl(db).handle
template handle(statement: SqlStatement): sqlite.Stmt = SqlStatementImpl(statement).handle
template db(statement: SqlStatement): DbConn = SqlStatementImpl(statement).db
template cache(db: DbConn): StmtCache = DbConnImpl(db).cache
template hasCache(db: DbConn): bool = db.cache.capacity > 0
template assertCanUseDb(db: DbConn) =
doAssert (not DbConnImpl(db).isNil) and (not db.handle.isNil), "Database is closed"
template assertCanUseStatement(statement: SqlStatement, busyOk: static[bool] = false) =
doAssert (not SqlStatementImpl(statement).isNil) and (not statement.handle.isNil),
"Statement cannot be used because it has already been finalized."
doAssert not statement.db.handle.isNil,
"Statement cannot be used because the database connection has been closed"
when not busyOk:
doAssert not sqlite.stmt_busy(statement.handle),
"Statement cannot be used while inside the 'all' iterator"
proc newSqliteError(db: DbConn): ref SqliteError =
## Raises a SqliteError exception.
(ref SqliteError)(msg: "sqlite error: " & $sqlite.errmsg(db.handle))
proc newSqliteError(msg: string): ref SqliteError =
## Raises a SqliteError exception.
(ref SqliteError)(msg: msg)
template checkRc(db: DbConn, rc: Rc) =
if rc notin SqliteRcOk:
raise newSqliteError(db)
proc skipLeadingWhiteSpaceAndComments(sql: var cstring) =
let original = sql
template `&+`(s: cstring, offset: int): cstring =
cast[cstring](cast[ByteAddress](sql) + offset)
while true:
case sql[0]
of {' ', '\t', '\v', '\r', '\l', '\f'}:
sql = sql &+ 1
of '-':
if sql[1] == '-':
sql = sql &+ 2
while sql[0] != '\n':
sql = sql &+ 1
if sql[0] == '\0':
return
sql = sql &+ 1
else:
return;
of '/':
if sql[1] == '*':
sql = sql &+ 2
while sql[0] != '*' or sql[1] != '/':
sql = sql &+ 1
if sql[0] == '\0':
sql = original
return
sql = sql &+ 2
else:
return;
else:
return
#
# DbValue
#
proc toDbValue*[T: Ordinal](val: T): DbValue =
## Convert an ordinal value to a Dbvalue.
DbValue(kind: sqliteInteger, intVal: val.int64)
proc toDbValue*[T: SomeFloat](val: T): DbValue =
## Convert a float to a DbValue.
DbValue(kind: sqliteReal, floatVal: val)
proc toDbValue*[T: string](val: T): DbValue =
## Convert a string to a DbValue.
DbValue(kind: sqliteText, strVal: val)
proc toDbValue*[T: seq[byte]](val: T): DbValue =
## Convert a sequence of bytes to a DbValue.
DbValue(kind: sqliteBlob, blobVal: val)
proc toDbValue*[T: Option](val: T): DbValue =
## Convert an optional value to a DbValue.
if val.isNone:
DbValue(kind: sqliteNull)
else:
toDbValue(val.get)
proc toDbValue*[T: type(nil)](val: T): DbValue =
## Convert a nil literal to a DbValue.
DbValue(kind: sqliteNull)
proc toDbValues*(values: varargs[DbValue, toDbValue]): seq[DbValue] =
## Convert several values to a sequence of DbValue's.
runnableExamples:
doAssert toDbValues("string", 23) == @[toDbValue("string"), toDbValue(23)]
@values
proc fromDbValue*(value: DbValue, T: typedesc[Ordinal]): T =
# Convert a DbValue to an ordinal.
value.intVal.T
proc fromDbValue*(value: DbValue, T: typedesc[SomeFloat]): float64 =
## Convert a DbValue to a float.
value.floatVal
proc fromDbValue*(value: DbValue, T: typedesc[string]): string =
## Convert a DbValue to a string.
value.strVal
proc fromDbValue*(value: DbValue, T: typedesc[seq[byte]]): seq[byte] =
## Convert a DbValue to a sequence of bytes.
value.blobVal
proc fromDbValue*[T](value: DbValue, _: typedesc[Option[T]]): Option[T] =
## Convert a DbValue to an optional value.
if (value.kind == sqliteNull):
none(T)
else:
some(value.fromDbValue(T))
proc fromDbValue*(value: DbValue, T: typedesc[DbValue]): T =
## Special overload that simply return `value`.
## The purpose of this overload is to do partial unpacking.
## For example, if the type of one column in a result row is unknown,
## the DbValue type can be kept just for that column.
##
## .. code-block:: nim
##
## for row in db.iterate("SELECT name, extra FROM Person"):
## # Type of 'extra' is unknown, so we don't unpack it.
## # The 'extra' variable will be of type 'DbValue'
## let (name, extra) = row.unpack((string, DbValue))
value
proc `$`*(dbVal: DbValue): string =
result.add "DbValue["
case dbVal.kind
of sqliteInteger: result.add $dbVal.intVal
of sqliteReal: result.add $dbVal.floatVal
of sqliteText: result.addQuoted dbVal.strVal
of sqliteBlob: result.add "<blob>"
of sqliteNull: result.add "nil"
result.add "]"
proc `==`*(a, b: DbValue): bool =
## Returns true if `a` and `b` represents the same value.
if a.kind != b.kind:
false
else:
case a.kind
of sqliteInteger: a.intVal == b.intVal
of sqliteReal: a.floatVal == b.floatVal
of sqliteText: a.strVal == b.strVal
of sqliteBlob: a.blobVal == b.blobVal
of sqliteNull: true
#
# PStmt
#
proc bindParams(db: DbConn, stmtHandle: sqlite.Stmt, params: varargs[DbValue]): Rc =
result = sqlite.SQLITE_OK
let expectedParamsLen = sqlite.bind_parameter_count(stmtHandle)
if expectedParamsLen != params.len:
raise newSqliteError("SQL statement contains " & $expectedParamsLen &
" parameters but only " & $params.len & " was provided.")
var idx = 1'i32
for value in params:
let rc =
case value.kind
of sqliteNull:
sqlite.bind_null(stmtHandle, idx)
of sqliteInteger:
sqlite.bind_int64(stmtHandle, idx, value.intval)
of sqliteReal:
sqlite.bind_double(stmtHandle, idx, value.floatVal)
of sqliteText:
sqlite.bind_text(stmtHandle, idx, value.strVal.cstring, value.strVal.len.int32, sqlite.SQLITE_TRANSIENT)
of sqliteBlob:
sqlite.bind_blob(stmtHandle, idx.int32, cast[string](value.blobVal).cstring,
value.blobVal.len.int32, sqlite.SQLITE_TRANSIENT)
if rc notin SqliteRcOk:
return rc
idx.inc
proc prepareSql(db: DbConn, sql: string): sqlite.Stmt =
var tail: cstring
let rc = sqlite.prepare_v2(db.handle, sql.cstring, sql.len.cint + 1, result, tail)
db.checkRc(rc)
tail.skipLeadingWhiteSpaceAndComments()
assert tail.len == 0,
"Only single SQL statement is allowed in this context. " &
"To execute several SQL statements, use 'execScript'"
proc prepareSql(db: DbConn, sql: string, params: seq[DbValue]): sqlite.Stmt
{.raises: [SqliteError].} =
if db.hasCache:
result = db.cache.getOrDefault(sql)
if result.isNil:
result = prepareSql(db, sql)
db.cache[sql] = result
else:
result = prepareSql(db, sql)
let rc = db.bindParams(result, params)
db.checkRc(rc)
proc readColumn(stmtHandle: sqlite.Stmt, col: int32): DbValue =
let columnType = sqlite.column_type(stmtHandle, col)
case columnType
of sqlite.SQLITE_INTEGER:
result = toDbValue(sqlite.column_int64(stmtHandle, col))
of sqlite.SQLITE_FLOAT:
result = toDbValue(sqlite.column_double(stmtHandle, col))
of sqlite.SQLITE_TEXT:
result = toDbValue($sqlite.column_text(stmtHandle, col))
of sqlite.SQLITE_BLOB:
let blob = sqlite.column_blob(stmtHandle, col)
let bytes = sqlite.column_bytes(stmtHandle, col)
var s = newSeq[byte](bytes)
if bytes != 0:
copyMem(addr(s[0]), blob, bytes)
result = toDbValue(s)
of sqlite.SQLITE_NULL:
result = toDbValue(nil)
else:
raiseAssert "Unexpected column type: " & $columnType
iterator iterate(db: DbConn, stmtOrHandle: sqlite.Stmt | SqlStatement, params: varargs[DbValue],
errorRc: var int32): ResultRow =
let stmtHandle = when stmtOrHandle is sqlite.Stmt: stmtOrHandle else: stmtOrHandle.handle
errorRc = db.bindParams(stmtHandle, params)
if errorRc in SqliteRcOk:
var rowLen = sqlite.column_count(stmtHandle)
var columns = newSeq[string](rowLen)
for idx in 0 ..< rowLen:
columns[idx] = $sqlite.column_name(stmtHandle, idx)
while true:
var row = ResultRow(values: newSeq[DbValue](rowLen), columns: columns)
when stmtOrHandle is sqlite.Stmt:
assertCanUseDb db
else:
assertCanUseStatement stmtOrHandle, busyOk = true
let rc = sqlite.step(stmtHandle)
if rc == sqlite.SQLITE_ROW:
for idx in 0 ..< rowLen:
row.values[idx] = readColumn(stmtHandle, idx)
yield row
elif rc == sqlite.SQLITE_DONE:
break
else:
errorRc = rc
break
#
# DbConn
#
proc exec*(db: DbConn, sql: string, params: varargs[DbValue, toDbValue]) =
## Executes ``sql``, which must be a single SQL statement.
runnableExamples:
let db = openDatabase(":memory:")
db.exec("CREATE TABLE Person(name, age)")
db.exec("INSERT INTO Person(name, age) VALUES(?, ?)",
"John Doe", 23)
assertCanUseDb db
let stmtHandle = db.prepareSql(sql, @params)
let rc = sqlite.step(stmtHandle)
if db.hasCache:
discard sqlite.reset(stmtHandle)
else:
discard sqlite.finalize(stmtHandle)
db.checkRc(rc)
template transaction*(db: DbConn, body: untyped) =
## Starts a transaction and runs `body` within it. At the end the transaction is commited.
## If an error is raised by `body` the transaction is rolled back. Nesting transactions is a no-op.
if db.isInTransaction:
body
else:
db.exec("BEGIN")
var ok = true
try:
try:
body
except Exception:
ok = false
db.exec("ROLLBACK")
raise
finally:
if ok:
db.exec("COMMIT")
proc execMany*(db: DbConn, sql: string, params: seq[seq[DbValue]]) =
## Executes ``sql``, which must be a single SQL statement, repeatedly using each element of
## ``params`` as parameters. The statements are executed inside a transaction.
assertCanUseDb db
db.transaction:
for p in params:
db.exec(sql, p)
proc execScript*(db: DbConn, sql: string) =
## Executes ``sql``, which can consist of multiple SQL statements.
## The statements are executed inside a transaction.
assertCanUseDb db
db.transaction:
var remaining = sql.cstring
while remaining.len > 0:
var tail: cstring
var stmtHandle: sqlite.Stmt
var rc = sqlite.prepare_v2(db.handle, remaining, -1, stmtHandle, tail)
db.checkRc(rc)
rc = sqlite.step(stmtHandle)
discard sqlite.finalize(stmtHandle)
db.checkRc(rc)
remaining = tail
remaining.skipLeadingWhiteSpaceAndComments()
iterator iterate*(db: DbConn, sql: string,
params: varargs[DbValue, toDbValue]): ResultRow =
## Executes ``sql``, which must be a single SQL statement, and yields each result row one by one.
assertCanUseDb db
let stmtHandle = db.prepareSql(sql, @params)
var errorRc: int32
try:
for row in db.iterate(stmtHandle, params, errorRc):
yield row
finally:
# The database might have been closed while iterating, in which
# case we don't need to clean up the statement.
if not db.handle.isNil:
if db.hasCache:
discard sqlite.reset(stmtHandle)
else:
discard sqlite.finalize(stmtHandle)
db.checkRc(errorRc)
proc all*(db: DbConn, sql: string,
params: varargs[DbValue, toDbValue]): seq[ResultRow] =
## Executes ``sql``, which must be a single SQL statement, and returns all result rows.
for row in db.iterate(sql, params):
result.add row
proc one*(db: DbConn, sql: string,
params: varargs[DbValue, toDbValue]): Option[ResultRow] =
## Executes `sql`, which must be a single SQL statement, and returns the first result row.
## Returns `none(seq[DbValue])` if the result was empty.
for row in db.iterate(sql, params):
return some(row)
proc value*(db: DbConn, sql: string,
params: varargs[DbValue, toDbValue]): Option[DbValue] =
## Executes `sql`, which must be a single SQL statement, and returns the first column of the first result row.
## Returns `none(DbValue)` if the result was empty.
for row in db.iterate(sql, params):
return some(row.values[0])
proc close*(db: DbConn) =
## Closes the database connection. This should be called once the connection will no longer be used
## to avoid leaking memory. Closing an already closed database is a harmless no-op.
if not db.isOpen:
return
var stmtHandle = sqlite.next_stmt(db.handle, nil)
while not stmtHandle.isNil:
discard sqlite.finalize(stmtHandle)
stmtHandle = sqlite.next_stmt(db.handle, nil)
db.cache.clear()
let rc = sqlite.close(db.handle)
db.checkRc(rc)
DbConnImpl(db).handle = nil
proc lastInsertRowId*(db: DbConn): int64 =
## Get the row id of the last inserted row.
## For tables with an integer primary key,
## the row id will be the primary key.
##
## For more information, refer to the SQLite documentation
## (https://www.sqlite.org/c3ref/last_insert_rowid.html).
assertCanUseDb db
sqlite.last_insert_rowid(db.handle)
proc changes*(db: DbConn): int32 =
## Get the number of changes triggered by the most recent INSERT, UPDATE or
## DELETE statement.
##
## For more information, refer to the SQLite documentation
## (https://www.sqlite.org/c3ref/changes.html).
assertCanUseDb db
sqlite.changes(db.handle)
proc isReadonly*(db: DbConn): bool =
## Returns true if ``db`` is in readonly mode.
runnableExamples:
let db = openDatabase(":memory:")
doAssert not db.isReadonly
let db2 = openDatabase(":memory:", dbRead)
doAssert db2.isReadonly
assertCanUseDb db
sqlite.db_readonly(db.handle, "main") == 1
proc isOpen*(db: DbConn): bool {.inline.} =
## Returns true if `db` has been opened and not yet closed.
runnableExamples:
var db: DbConn
doAssert not db.isOpen
db = openDatabase(":memory:")
doAssert db.isOpen
db.close()
doAssert not db.isOpen
(not DbConnImpl(db).isNil) and (not db.handle.isNil)
proc isInTransaction*(db: DbConn): bool =
## Returns true if a transaction is currently active.
runnableExamples:
let db = openDatabase(":memory:")
doAssert not db.isInTransaction
db.transaction:
doAssert db.isInTransaction
assertCanUseDb db
sqlite.get_autocommit(db.handle) == 0
proc unsafeHandle*(db: DbConn): sqlite.Sqlite3 {.inline.} =
## Returns the raw SQLite3 handle. This can be used to interact directly with the SQLite C API
## with the `tiny_sqlite/sqlite_wrapper` module. Note that the handle should not be used after `db.close` has
## been called as doing so would break memory safety.
assert not DbConnImpl(db).handle.isNil, "Database is closed"
DbConnImpl(db).handle
#
# SqlStatement
#
proc stmt*(db: DbConn, sql: string): SqlStatement =
## Constructs a prepared statement from `sql`.
assertCanUseDb db
let handle = prepareSql(db, sql)
SqlStatementImpl(handle: handle, db: db).SqlStatement
proc exec*(statement: SqlStatement, params: varargs[DbValue, toDbValue]) =
## Executes `statement` with `params` as parameters.
assertCanUseStatement statement
var rc = statement.db.bindParams(statement.handle, params)
if rc notin SqliteRcOk:
discard sqlite.reset(statement.handle)
statement.db.checkRc(rc)
else:
rc = sqlite.step(statement.handle)
discard sqlite.reset(statement.handle)
statement.db.checkRc(rc)
proc execMany*(statement: SqlStatement, params: seq[seq[DbValue]]) =
## Executes ``statement`` repeatedly using each element of ``params`` as parameters.
## The statements are executed inside a transaction.
assertCanUseStatement statement
statement.db.transaction:
for p in params:
statement.exec(p)
iterator iterate*(statement: SqlStatement, params: varargs[DbValue, toDbValue]): ResultRow =
## Executes ``statement`` and yields each result row one by one.
assertCanUseStatement statement
var errorRc: int32
try:
for row in statement.db.iterate(statement, params, errorRc):
yield row
finally:
# The database might have been closed while iterating, in which
# case we don't need to clean up the statement.
if not statement.db.handle.isNil:
discard sqlite.reset(statement.handle)
statement.db.checkRc errorRc
proc all*(statement: SqlStatement, params: varargs[DbValue, toDbValue]): seq[ResultRow] =
## Executes ``statement`` and returns all result rows.
assertCanUseStatement statement
for row in statement.iterate(params):
result.add row
proc one*(statement: SqlStatement,
params: varargs[DbValue, toDbValue]): Option[ResultRow] =
## Executes `statement` and returns the first row found.
## Returns `none(seq[DbValue])` if no result was found.
assertCanUseStatement statement
for row in statement.iterate(params):
return some(row)
proc value*(statement: SqlStatement,
params: varargs[DbValue, toDbValue]): Option[DbValue] =
## Executes `statement` and returns the first column of the first row found.
## Returns `none(DbValue)` if no result was found.
assertCanUseStatement statement
for row in statement.iterate(params):
return some(row.values[0])
proc finalize*(statement: SqlStatement): void =
## Finalize the statement. This needs to be called once the statement is no longer used to
## prevent memory leaks. Finalizing an already finalized statement is a harmless no-op.
if SqlStatementImpl(statement).isNil:
return
discard sqlite.finalize(statement.handle)
SqlStatementImpl(statement).handle = nil
proc isAlive*(statement: SqlStatement): bool =
## Returns true if ``statement`` has been initialized and not yet finalized.
(not SqlStatementImpl(statement).isNil) and (not statement.handle.isNil) and
(not statement.db.handle.isNil)
proc openDatabase*(path: string, mode = dbReadWrite, cacheSize: Natural = 100): DbConn =
## Open a new database connection to a database file. To create an
## in-memory database the special path `":memory:"` can be used.
## If the database doesn't already exist and ``mode`` is ``dbReadWrite``,
## the database will be created. If the database doesn't exist and ``mode``
## is ``dbRead``, a ``SqliteError`` exception will be raised.
##
## NOTE: To avoid memory leaks, ``db.close`` must be called when the
## database connection is no longer needed.
runnableExamples:
let memDb = openDatabase(":memory:")
var handle: sqlite.Sqlite3
let db = new DbConnImpl
db.handle = handle
if cacheSize > 0:
db.cache = initStmtCache(cacheSize)
result = DbConn(db)
case mode
of dbReadWrite:
let rc = sqlite.open(path, db.handle)
result.checkRc(rc)
of dbRead:
let rc = sqlite.open_v2(path, db.handle, sqlite.SQLITE_OPEN_READONLY, nil)
result.checkRc(rc)
#
# ResultRow
#
proc `[]`*(row: ResultRow, idx: Natural): DbValue =
## Access a column in the result row based on index.
row.values[idx]
proc `[]`*(row: ResultRow, column: string): DbValue =
## Access a column in te result row based on column name.
## The column name must be unambiguous.
let idx = row.columns.find(column)
assert idx != -1, "Column does not exist in row: '" & column & "'"
doAssert count(row.columns, column) == 1, "Column exists multiple times in row: '" & column & "'"
row.values[idx]
proc len*(row: ResultRow): int =
## Returns the number of columns in the result row.
row.values.len
proc values*(row: ResultRow): seq[DbValue] =
## Returns all column values in the result row.
row.values
proc columns*(row: ResultRow): seq[string] =
## Returns all column names in the result row.
row.columns
proc unpack*[T: tuple](row: ResultRow, _: typedesc[T]): T =
## Calls ``fromDbValue`` on each element of ``row`` and returns it
## as a tuple.
doAssert row.len == result.typeof.tupleLen,
"Unpack expected a tuple with " & $row.len & " field(s) but found: " & $T
var idx = 0
for value in result.fields:
value = row[idx].fromDbValue(type(value))
idx.inc
#
# Deprecations
#
proc rows*(db: DbConn, sql: string, params: varargs[DbValue, toDbValue]): seq[seq[DbValue]]
{.deprecated: "use 'all' instead".} =
db.all(sql, params).mapIt(it.values)
iterator rows*(db: DbConn, sql: string, params: varargs[DbValue, toDbValue]): seq[DbValue]
{.deprecated: "use 'iterate' instead".} =
for row in db.all(sql, params):
yield row.values
proc unpack*[T: tuple](row: seq[DbValue], _: typedesc[T]): T {.deprecated.} =
ResultRow(values: row).unpack(T)

1
vendor/cligen vendored

@ -1 +0,0 @@
Subproject commit 992fcc078475bebba259ed09340f2eb30504fba4

1
vendor/nim-regex vendored

@ -1 +0,0 @@
Subproject commit 37799c609105d8aaa5b7a1806d13fbceec5123de

@ -1 +0,0 @@
Subproject commit 47bae531c657e01a92734e57aed552957981ad1c

@ -1 +0,0 @@
Subproject commit 7c6ee4bfc184d7121896a098d68b639a96df7af1

@ -1 +0,0 @@
Subproject commit fd553314df9d9a45aa0d14218e20e7c029f0baa1

1
vendor/nimterop vendored

@ -1 +0,0 @@
Subproject commit f7cee5c983650336f93fde5d4fea087863ac0e5e

2
vendor/sqlcipher vendored

@ -1 +1 @@
Subproject commit 87b4a1ea57827bbf1177bc6a472590ea2af4b8c3 Subproject commit 50376d07a5919f1777ac983921facf0bf0fc1976