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: ,
|
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 }} \
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
147
Makefile
|
@ -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
|
||||||
|
|
|
@ -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
|
# 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)
|
||||||
|
|
|
@ -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" &
|
||||||
|
|
|
@ -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