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:
parent
c06110bc81
commit
474cf12001
|
@ -35,9 +35,9 @@ jobs:
|
|||
include: ,
|
||||
lib: ,
|
||||
}
|
||||
sqlite: [ true, false ]
|
||||
openssl: [ true, false ]
|
||||
name: ${{ matrix.platform.icon }} - SQLITE ${{ matrix.sqlite }} | SSL ${{ matrix.openssl }}
|
||||
sqlcipher: [ true, false ]
|
||||
openssl: [ true, false ]
|
||||
name: ${{ matrix.platform.icon }} - SQLCIPHER ${{ matrix.sqlcipher }} | SSL ${{ matrix.openssl }}
|
||||
runs-on: ${{ matrix.platform.os }}-latest
|
||||
defaults:
|
||||
run:
|
||||
|
@ -77,11 +77,11 @@ jobs:
|
|||
make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC}" V=1 update
|
||||
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: |
|
||||
make -j${NPROC} \
|
||||
NIMFLAGS="--parallelBuild:${NPROC}" \
|
||||
SQLITE_STATIC=${{ matrix.sqlite }} \
|
||||
SQLCIPHER_STATIC=${{ matrix.sqlcipher }} \
|
||||
SSL_INCLUDE_DIR="${{ matrix.platform.include }}" \
|
||||
SSL_LIB_DIR="${{ matrix.platform.lib }}" \
|
||||
SSL_STATIC=${{ matrix.openssl }} \
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
.vscode
|
||||
/generator/*
|
||||
!/generator/generate.nim
|
||||
/lib
|
||||
/nimcache
|
||||
/sqlcipher
|
||||
/sqlite
|
||||
/test/build
|
||||
/update
|
||||
|
|
|
@ -4,24 +4,6 @@
|
|||
[submodule "vendor/sqlcipher"]
|
||||
path = vendor/sqlcipher
|
||||
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"]
|
||||
path = vendor/nim-stew
|
||||
url = https://github.com/status-im/nim-stew
|
||||
|
|
147
Makefile
147
Makefile
|
@ -17,14 +17,11 @@ BUILD_SYSTEM_DIR := vendor/nimbus-build-system
|
|||
all \
|
||||
clean \
|
||||
clean-build-dirs \
|
||||
clean-generator \
|
||||
clean-nimterop \
|
||||
clean-sqlcipher \
|
||||
deps \
|
||||
sqlite \
|
||||
sqlite.nim \
|
||||
sqlcipher \
|
||||
sqlite3.c \
|
||||
test \
|
||||
toast \
|
||||
update
|
||||
|
||||
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.
|
||||
|
||||
all: sqlite.nim
|
||||
all: sqlcipher
|
||||
|
||||
# must be included after the default target
|
||||
-include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk
|
||||
|
@ -56,24 +53,20 @@ else
|
|||
detected_OS := $(strip $(shell uname))
|
||||
endif
|
||||
|
||||
clean: | clean-common clean-build-dirs clean-generator clean-nimterop
|
||||
clean: | clean-common clean-build-dirs clean-sqlcipher
|
||||
|
||||
clean-build-dirs:
|
||||
rm -rf \
|
||||
sqlcipher \
|
||||
lib \
|
||||
sqlite \
|
||||
test/build
|
||||
|
||||
clean-generator:
|
||||
rm -rf \
|
||||
generator/generate \
|
||||
generator/generate.exe \
|
||||
generator/generate.dSYM
|
||||
|
||||
clean-nimterop:
|
||||
rm -rf \
|
||||
$(NIMTEROP_TOAST) \
|
||||
$(NIMTEROP_TOAST).dSYM
|
||||
clean-sqlcipher:
|
||||
cd vendor/sqlcipher && git clean -dfx $(HANDLE_OUTPUT)
|
||||
([[ $(detected_OS) = Windows ]] && \
|
||||
cd vendor/sqlcipher && \
|
||||
git stash $(HANDLE_OUTPUT) && \
|
||||
git stash drop $(HANDLE_OUTPUT)) || true
|
||||
|
||||
deps: | deps-common
|
||||
|
||||
|
@ -105,14 +98,14 @@ else
|
|||
SSL_LDFLAGS_SQLITE3_C ?= -L$(SSL_LIB_DIR) $(SSL_LDFLAGS)
|
||||
endif
|
||||
|
||||
SQLITE_STATIC ?= true
|
||||
SQLITE_CDEFS ?= -DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=3
|
||||
SQLITE_CFLAGS ?= -I$(SSL_INCLUDE_DIR) -pthread
|
||||
ifndef SQLITE_LDFLAGS
|
||||
SQLCIPHER_STATIC ?= true
|
||||
SQLCIPHER_CDEFS ?= -DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=3
|
||||
SQLCIPHER_CFLAGS ?= -I$(SSL_INCLUDE_DIR) -pthread
|
||||
ifndef SQLCIPHER_LDFLAGS
|
||||
ifeq ($(detected_OS),Windows)
|
||||
SQLITE_LDFLAGS := -lwinpthread
|
||||
SQLCIPHER_LDFLAGS := -lwinpthread
|
||||
else
|
||||
SQLITE_LDFLAGS := -lpthread
|
||||
SQLCIPHER_LDFLAGS := -lpthread
|
||||
endif
|
||||
endif
|
||||
|
||||
|
@ -124,35 +117,31 @@ $(SQLITE3_C): | deps
|
|||
+ mkdir -p sqlite
|
||||
cd vendor/sqlcipher && \
|
||||
./configure \
|
||||
CFLAGS="$(SQLITE_CDEFS) $(SQLITE_CFLAGS)" \
|
||||
LDFLAGS="$(SQLITE_LDFLAGS) $(SSL_LDFLAGS_SQLITE3_C)" \
|
||||
CFLAGS="$(SQLCIPHER_CDEFS) $(SQLCIPHER_CFLAGS)" \
|
||||
LDFLAGS="$(SQLCIPHER_LDFLAGS) $(SSL_LDFLAGS_SQLITE3_C)" \
|
||||
$(HANDLE_OUTPUT)
|
||||
cd vendor/sqlcipher && $(MAKE) sqlite3.c $(HANDLE_OUTPUT)
|
||||
cp \
|
||||
vendor/sqlcipher/sqlite3.c \
|
||||
vendor/sqlcipher/sqlite3.h \
|
||||
sqlite/
|
||||
cd vendor/sqlcipher && git clean -dfx $(HANDLE_OUTPUT)
|
||||
([[ $(detected_OS) = Windows ]] && \
|
||||
cd vendor/sqlcipher && \
|
||||
git stash $(HANDLE_OUTPUT) && \
|
||||
git stash drop $(HANDLE_OUTPUT)) || true
|
||||
$(MAKE) clean-sqlcipher
|
||||
|
||||
sqlite3.c: $(SQLITE3_C)
|
||||
|
||||
SQLITE_STATIC_LIB ?= $(shell pwd)/sqlcipher/sqlcipher.a
|
||||
SQLITE_STATIC_OBJ ?= sqlcipher/sqlcipher.o
|
||||
SQLCIPHER_STATIC_LIB ?= $(shell pwd)/lib/libsqlcipher.a
|
||||
SQLCIPHER_STATIC_OBJ ?= lib/sqlcipher.o
|
||||
|
||||
$(SQLITE_STATIC_LIB): $(SQLITE3_C)
|
||||
$(SQLCIPHER_STATIC_LIB): $(SQLITE3_C)
|
||||
echo -e $(BUILD_MSG) "SQLCipher static library"
|
||||
+ mkdir -p sqlcipher
|
||||
+ mkdir -p lib
|
||||
$(ENV_SCRIPT) $(CC) \
|
||||
$(SQLITE_CDEFS) \
|
||||
$(SQLITE_CFLAGS) \
|
||||
$(SQLCIPHER_CDEFS) \
|
||||
$(SQLCIPHER_CFLAGS) \
|
||||
$(SQLITE3_C) \
|
||||
-c \
|
||||
-o $(SQLITE_STATIC_OBJ) $(HANDLE_OUTPUT)
|
||||
$(ENV_SCRIPT) ar rcs $(SQLITE_STATIC_LIB) $(SQLITE_STATIC_OBJ) $(HANDLE_OUTPUT)
|
||||
-o $(SQLCIPHER_STATIC_OBJ) $(HANDLE_OUTPUT)
|
||||
$(ENV_SCRIPT) ar rcs $(SQLCIPHER_STATIC_LIB) $(SQLCIPHER_STATIC_OBJ) $(HANDLE_OUTPUT)
|
||||
|
||||
ifndef SHARED_LIB_EXT
|
||||
ifeq ($(detected_OS),macOS)
|
||||
|
@ -164,7 +153,7 @@ ifndef SHARED_LIB_EXT
|
|||
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
|
||||
ifeq ($(detected_OS),macOS)
|
||||
|
@ -174,73 +163,36 @@ ifndef PLATFORM_FLAGS_SHARED_LIB
|
|||
endif
|
||||
endif
|
||||
|
||||
$(SQLITE_SHARED_LIB): $(SQLITE3_C)
|
||||
$(SQLCIPHER_SHARED_LIB): $(SQLITE3_C)
|
||||
echo -e $(BUILD_MSG) "SQLCipher shared library"
|
||||
+ mkdir -p sqlcipher
|
||||
+ mkdir -p lib
|
||||
$(ENV_SCRIPT) $(CC) \
|
||||
$(SQLITE_CDEFS) \
|
||||
$(SQLITE_CFLAGS) \
|
||||
$(SQLCIPHER_CDEFS) \
|
||||
$(SQLCIPHER_CFLAGS) \
|
||||
$(SQLITE3_C) \
|
||||
$(SQLITE_LDFLAGS) \
|
||||
$(SQLCIPHER_LDFLAGS) \
|
||||
$(SSL_LDFLAGS) \
|
||||
$(PLATFORM_FLAGS_SHARED_LIB) \
|
||||
-o $(SQLITE_SHARED_LIB) $(HANDLE_OUTPUT)
|
||||
-o $(SQLCIPHER_SHARED_LIB) $(HANDLE_OUTPUT)
|
||||
|
||||
ifndef SQLITE_LIB
|
||||
ifneq ($(SQLITE_STATIC),false)
|
||||
SQLITE_LIB := $(SQLITE_STATIC_LIB)
|
||||
ifndef SQLCIPHER_LIB
|
||||
ifneq ($(SQLCIPHER_STATIC),false)
|
||||
SQLCIPHER_LIB := $(SQLCIPHER_STATIC_LIB)
|
||||
else
|
||||
SQLITE_LIB := $(SQLITE_SHARED_LIB)
|
||||
SQLCIPHER_LIB := $(SQLCIPHER_SHARED_LIB)
|
||||
endif
|
||||
endif
|
||||
|
||||
sqlite: $(SQLITE_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)
|
||||
sqlcipher: $(SQLCIPHER_LIB)
|
||||
|
||||
# LD_LIBRARY_PATH is supplied when running tests on Linux
|
||||
# PATH is supplied when running tests on Windows
|
||||
ifeq ($(SQLITE_STATIC),false)
|
||||
PATH_TEST ?= $(shell dirname $(SQLITE_SHARED_LIB))::$${PATH}
|
||||
ifeq ($(SQLCIPHER_STATIC),false)
|
||||
PATH_TEST ?= $(shell dirname $(SQLCIPHER_LIB))::$${PATH}
|
||||
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
|
||||
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
|
||||
else
|
||||
PATH_TEST ?= $${PATH}
|
||||
|
@ -251,18 +203,27 @@ else
|
|||
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)
|
||||
SQLCIPHER_LDFLAGS="$(SQLCIPHER_LDFLAGS_TEST)" \
|
||||
SSL_LDFLAGS="$(SSL_LDFLAGS)" \
|
||||
SSL_STATIC="$(SSL_STATIC)" \
|
||||
$(ENV_SCRIPT) nimble tests
|
||||
else ifeq ($(detected_OS),Windows)
|
||||
PATH="$(PATH_TEST)" \
|
||||
SQLCIPHER_LDFLAGS="$(SQLCIPHER_LDFLAGS_TEST)" \
|
||||
SSL_LDFLAGS="$(SSL_LDFLAGS)" \
|
||||
SSL_STATIC="$(SSL_STATIC)" \
|
||||
$(ENV_SCRIPT) nimble tests
|
||||
else
|
||||
LD_LIBRARY_PATH="$(LD_LIBRARY_PATH_TEST)" \
|
||||
SQLCIPHER_LDFLAGS="$(SQLCIPHER_LDFLAGS_TEST)" \
|
||||
SSL_LDFLAGS="$(SSL_LDFLAGS)" \
|
||||
SSL_STATIC="$(SSL_STATIC)" \
|
||||
$(ENV_SCRIPT) nimble tests
|
||||
|
|
|
@ -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")
|
494
sqlcipher.nim
494
sqlcipher.nim
|
@ -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
|
||||
# directory as this file.
|
||||
from sqlcipher/sqlite as sqlite import nil
|
||||
from stew/shims/macros as stew_macros import hasCustomPragmaFixed, getCustomPragmaFixed
|
||||
|
||||
# Adapted from https://github.com/GULPF/tiny_sqlite
|
||||
|
||||
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)
|
||||
#
|
||||
# Custom.DbConn
|
||||
#
|
||||
proc all*[T](db: DbConn, _: typedesc[T], sql: string,
|
||||
params: varargs[DbValue, toDbValue]): seq[T] =
|
||||
## Executes ``statement`` and returns all result rows.
|
||||
for row in db.iterate(sql, params):
|
||||
var r = T()
|
||||
row.to(r)
|
||||
result.add r
|
||||
row.unpack(r)
|
||||
result.add r
|
||||
|
||||
proc openDatabase*(path: string, mode = dbReadWrite): DbConn =
|
||||
## Open a new database connection to a database file. To create a
|
||||
## 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:")
|
||||
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 one*[T](db: DbConn, _: typedesc[T], sql: string,
|
||||
params: varargs[DbValue, toDbValue]): Option[T] =
|
||||
## 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):
|
||||
var r = T()
|
||||
row.unpack(r)
|
||||
return some(r)
|
||||
|
||||
proc key*(db: DbConn, password: string) =
|
||||
let rc = sqlite.key(db, password.cstring, int32(password.len))
|
||||
db.checkRc(rc)
|
||||
proc value*[T](db: DbConn, _: typedesc[T], sql: string,
|
||||
params: varargs[DbValue, toDbValue]): Option[T] =
|
||||
## 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))
|
||||
db.checkRc(rc)
|
||||
#
|
||||
# Custom.SqlStatement
|
||||
#
|
||||
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) =
|
||||
## Closes the database connection.
|
||||
let rc = sqlite.close(db)
|
||||
db.checkRc(rc)
|
||||
proc one*[T](_: typedesc[T], statement: SqlStatement,
|
||||
params: varargs[DbValue, toDbValue]): Option[T] =
|
||||
## 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):
|
||||
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.
|
||||
## 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).
|
||||
sqlite.last_insert_rowid(db)
|
||||
#
|
||||
# Custom.ResultRow
|
||||
#
|
||||
proc `[]`*[T](row: ResultRow, columnName: string, _: typedesc[T]): T =
|
||||
row[columnName].fromDbValue(T)
|
||||
|
||||
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).
|
||||
sqlite.changes(db)
|
||||
proc hasRows*(rows: seq[ResultRow]): bool = rows.len > 0
|
||||
|
||||
proc isReadonly*(db: DbConn): bool =
|
||||
## Returns true if ``db`` is in readonly mode.
|
||||
sqlite.db_readonly(db, "main") == 1
|
||||
]#
|
||||
|
||||
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)
|
||||
#
|
||||
# Custom.ORM
|
||||
# This section was not originally part of tiny_sqlite
|
||||
#
|
||||
|
||||
# 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.}
|
||||
## 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,
|
||||
fieldNameVar, fieldVar,
|
||||
body: untyped) =
|
||||
|
@ -391,9 +114,56 @@ template enumInstanceDbColumns*(obj: auto,
|
|||
const fieldNameVar = fieldName
|
||||
body
|
||||
|
||||
proc to*(row: DbRow, obj: var object) =
|
||||
proc unpack*(row: ResultRow, obj: var object) =
|
||||
obj.enumInstanceDbColumns(dbColName, 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)
|
||||
|
|
|
@ -31,6 +31,7 @@ proc buildAndRunTest(name: string,
|
|||
(if getEnv("SSL_STATIC").strip != "false": " --dynlibOverride:ssl" else: "") &
|
||||
" --nimcache:nimcache/test/" & name &
|
||||
" --out:" & outDir & name &
|
||||
(if getEnv("SQLCIPHER_LDFLAGS").strip != "": " --passL:\"" & getEnv("SQLCIPHER_LDFLAGS") & "\"" else: "") &
|
||||
(if getEnv("SSL_LDFLAGS").strip != "": " --passL:\"" & getEnv("SSL_LDFLAGS") & "\"" else: "") &
|
||||
" --threads:on" &
|
||||
" --tlsEmulation:off" &
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
## ```
|
|
@ -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 +0,0 @@
|
|||
Subproject commit 992fcc078475bebba259ed09340f2eb30504fba4
|
|
@ -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 +0,0 @@
|
|||
Subproject commit f7cee5c983650336f93fde5d4fea087863ac0e5e
|
|
@ -1 +1 @@
|
|||
Subproject commit 87b4a1ea57827bbf1177bc6a472590ea2af4b8c3
|
||||
Subproject commit 50376d07a5919f1777ac983921facf0bf0fc1976
|
Loading…
Reference in New Issue