├── .github └── workflows │ └── test.yaml ├── .gitignore ├── README.md ├── easy_sqlite3.nimble ├── src ├── easy_sqlite3.nim └── easy_sqlite3 │ ├── bindings.nim │ ├── logfs.nim │ ├── macros.nim │ ├── memfs.nim │ ├── nim.cfg │ ├── sqlite3.c │ └── utils.nim └── tests ├── .gitignore ├── config.nims ├── nim.cfg ├── test_basic.nim ├── test_catch.nim ├── test_emptystr.nim ├── test_option.nim ├── test_thread.nim └── test_type.nim /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'LICENSE' 7 | - '*.md' 8 | branches: 9 | - develop 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - windows-latest 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: jiro4989/setup-nim-action@v1.4.2 22 | - run: nimble test -Y 23 | - run: nimble test -Y -d:release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yet another SQLite wrapper for Nim 2 | 3 | Features: 4 | 5 | 1. Design for ARC/ORC, you don’t need to close the connection manually 6 | 2. Use `importdb` macro to create helper function (see examples) 7 | 3. Including a memfs implemention, may better than `:memory:` database since it support WAL mode (Experimental, see tests/test_thread) 8 | 9 | ## Example 10 | 11 | Basic usage: 12 | 13 | ```nim 14 | import std/tables 15 | import easy_sqlite3 16 | 17 | # Bind function argument to sql statment 18 | # The tuple return value indicate the query will got exactly 1 result 19 | proc select_1(arg: int): tuple[value: int] {.importdb: "SELECT $arg".} 20 | 21 | var db = initDatabase(":memory:") 22 | # Use as a method (the statment will be cached, thats why `var` is required) 23 | echo db.select_1(1).value 24 | # Got 1 25 | 26 | # You can bind create statment as well 27 | proc create_table() {.importdb: """ 28 | CREATE TABLE mydata(name TEXT PRIMARY KEY NOT NULL, value INT NOT NULL); 29 | """.} 30 | 31 | # Or insert 32 | proc insert_data(name: string, value: int) {.importdb: """ 33 | INSERT INTO mydata(name, value) VALUES ($name, $value); 34 | """.} 35 | 36 | # And you can create iterator by the same way (the `= discard` is required, since iterator must have body in nim) 37 | iterator iterate_data(): tuple[name: string, value: int] {.importdb: """ 38 | SELECT name, value FROM mydata; 39 | """.} = discard 40 | 41 | const dataset = { 42 | "A": 0, 43 | "B": 1, 44 | "C": 2, 45 | "D": 3, 46 | }.toTable 47 | db.create_table() 48 | # Use transaction (commit by default) 49 | db.transaction: 50 | for name, value in dataset: 51 | db.insert_data name, value 52 | commit() # optional 53 | # Never goes here 54 | 55 | for name, value in db.iterate_data(): 56 | assert name in dataset 57 | assert dataset[name] == value 58 | ``` 59 | -------------------------------------------------------------------------------- /easy_sqlite3.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.10" 4 | author = "CodeHz" 5 | description = "Yet another SQLite wrapper for Nim" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.4.2" 13 | -------------------------------------------------------------------------------- /src/easy_sqlite3.nim: -------------------------------------------------------------------------------- 1 | import easy_sqlite3/[bindings,macros] 2 | export macros 3 | 4 | export raw, len, toOpenArray, SQLiteError, SQLiteBlob, Statement, Database, 5 | SqliteDataType, OpenFlag, enableSharedCache, initDatabase, exec, execM, 6 | changes, lastInsertRowid, `[]=`, reset, step, withColumnBlob, 7 | getParameterIndex, getColumnType, getColumn, ColumnDef, columns, `[]`, 8 | unpack, `=destroy`, newStatement, rows, setAuthorizer, 9 | AuthorizerActionCode, AuthorizerRequest, AuthorizerResult, RawAuthorizer, 10 | Authorizer 11 | -------------------------------------------------------------------------------- /src/easy_sqlite3/bindings.nim: -------------------------------------------------------------------------------- 1 | import std/[tables, options, hashes] 2 | import ./utils 3 | import std/macros 4 | 5 | when not defined(bundled_sqlite3): 6 | when defined(windows): 7 | when defined(nimOldDlls): 8 | const sqlite3dll = "sqlite3.dll" 9 | elif defined(cpu64): 10 | const sqlite3dll = "sqlite3_64.dll" 11 | else: 12 | const sqlite3dll = "sqlite3_32.dll" 13 | elif defined(macosx): 14 | const sqlite3dll = "libsqlite3(|.0).dylib" 15 | else: 16 | const sqlite3dll = "libsqlite3.so(|.0)" 17 | macro sqlite3linkage(f: untyped) = 18 | if f[4].kind == nnkEmpty: 19 | f[4] = newTree nnkPragma 20 | f[4].add newColonExpr(ident "dynlib", newLit sqlite3dll) 21 | f[4].add ident "importc" 22 | f 23 | else: 24 | from std/strutils import replace 25 | {.compile( 26 | "sqlite3.c", 27 | """ 28 | -DSQLITE_ENABLE_FTS5=1 29 | -DSQLITE_ENABLE_RTREE=1 30 | -DSQLITE_ENABLE_GEOPOLY=1 31 | -DSQLITE_ENABLE_DBSTAT_VTAB=1 32 | -DSQLITE_ENABLE_JSON1=1 33 | -DSQLITE_ENABLE_RBU=1 34 | -DSQLITE_OMIT_DEPRECATED=1 35 | -DSQLITE_ENABLE_MATH_FUNCTIONS=1 36 | -DSQLITE_DQS=0 37 | """.replace('\n', ' ') 38 | ).} 39 | macro sqlite3linkage(f: untyped) = 40 | if f[4].kind == nnkEmpty: 41 | f[4] = newTree nnkPragma 42 | f[4].add ident "importc" 43 | f 44 | 45 | type RawDatabase* = object 46 | type RawStatement* = object 47 | type RawValue* = object 48 | 49 | type CachedHash[T] = object 50 | cache: int 51 | value: T 52 | 53 | func `==`[T](a, b: CachedHash[T]): bool = a.value == b.value 54 | func hash[T](self: CachedHash[T]): int {.inline.} = self.cache 55 | converter cacheHash[T](original: T): CachedHash[T] = 56 | CachedHash[T](cache: original.hash, value: original) 57 | func compileTimeHash[T](original: static[T]): CachedHash[T] = 58 | CachedHash[T](cache: original.hash, value: original) 59 | 60 | type Statement* = object 61 | raw*: ptr RawStatement 62 | 63 | type 64 | AuthorizerResult* {.pure, size: sizeof(cint).} = enum 65 | ok = 0, 66 | deny = 1, 67 | ignore = 2 68 | AuthorizerActionCode* {.pure, size: sizeof(cint).} = enum 69 | # action code # arg3 arg4 70 | copy = 0, # No longer used 71 | create_index = 1, # Index Name Table Name 72 | create_table = 2, # Table Name NULL 73 | create_temp_index = 3, # Index Name Table Name 74 | create_temp_table = 4, # Table Name NULL 75 | create_temp_trigger = 5, # Trigger Name Table Name 76 | create_temp_view = 6, # View Name NULL 77 | create_trigger = 7, # Trigger Name Table Name 78 | create_view = 8, # View Name NULL 79 | delete = 9, # Table Name NULL 80 | drop_index = 10, # Index Name Table Name 81 | drop_table = 11, # Table Name NULL 82 | drop_temp_index = 12, # Index Name Table Name 83 | drop_temp_table = 13, # Table Name NULL 84 | drop_temp_trigger = 14, # Trigger Name Table Name 85 | drop_temp_view = 15, # View Name NULL 86 | drop_trigger = 16, # Trigger Name Table Name 87 | drop_view = 17, # View Name NULL 88 | insert = 18, # Table Name NULL 89 | pragma = 19, # Pragma Name 1st arg or NULL 90 | read = 20, # Table Name Column Name 91 | select = 21, # NULL NULL 92 | transaction = 22, # Operation NULL 93 | update = 23, # Table Name Column Name 94 | attach = 24, # Filename NULL 95 | detach = 25, # Database Name NULL 96 | alter_table = 26, # Database Name Table Name 97 | reindex = 27, # Index Name NULL 98 | analyze = 28, # Table Name NULL 99 | create_vtable = 29, # Table Name Module Name 100 | drop_vtable = 30, # Table Name Module Name 101 | function = 31, # NULL Function Name 102 | savepoint = 32, # Operation Savepoint Name 103 | recursive = 33, # NULL NULL 104 | AuthorizerRequest* = ref object 105 | db_name*: string 106 | caller*: string 107 | case action_code*: AuthorizerActionCode 108 | of create_index, create_temp_index, drop_index, drop_temp_index: 109 | index_name*: string 110 | index_table_name*: string 111 | of create_table, create_temp_table, delete, drop_table, drop_temp_table, insert, analyze: 112 | table_name*: string 113 | of create_temp_trigger, create_trigger, drop_temp_trigger, drop_trigger: 114 | trigger_name*: string 115 | trigger_table_name*: string 116 | of create_temp_view, create_view, drop_temp_view, drop_view: 117 | view_name*: string 118 | of pragma: 119 | pragma_name*: string 120 | pragma_arg*: Option[string] 121 | of read, update: 122 | target_table_name*: string 123 | column_name*: string 124 | of select, recursive, copy: 125 | discard 126 | of transaction: 127 | transaction_operation*: string 128 | of attach: 129 | filename*: string 130 | of detach: 131 | database_name*: string 132 | of alter_table: 133 | alter_database_name*: string 134 | alter_table_name*: string 135 | of reindex: 136 | reindex_index_name*: string 137 | of create_vtable, drop_vtable: 138 | vtable_name*: string 139 | module_name*: string 140 | of function: 141 | # no arg3 142 | function_name*: string 143 | of savepoint: 144 | savepoint_operation*: string 145 | savepoint_name*: string 146 | SqliteRawAuthorizer* = proc( 147 | userdata: pointer, 148 | action_code: AuthorizerActionCode, 149 | arg3, arg4, arg5, arg6: cstring): AuthorizerResult {.cdecl.} 150 | RawAuthorizer* = proc( 151 | action_code: AuthorizerActionCode, 152 | arg3, arg4, arg5, arg6: Option[string]): AuthorizerResult 153 | Authorizer* = proc(request: AuthorizerRequest): AuthorizerResult 154 | WrapAuthorizer = object 155 | authorizer: RawAuthorizer 156 | 157 | type Database* = object 158 | raw*: ptr RawDatabase 159 | stmtcache: Table[CachedHash[string], ref Statement] 160 | authorizer: ref WrapAuthorizer 161 | 162 | type ResultCode* {.pure.} = enum 163 | sr_ok = 0, 164 | sr_error = 1, 165 | sr_internal = 2, 166 | sr_perm = 3, 167 | sr_abort = 4, 168 | sr_busy = 5, 169 | sr_locked = 6, 170 | sr_nomem = 7, 171 | sr_readonly = 8, 172 | sr_interrupt = 9, 173 | sr_ioerr = 10, 174 | sr_corrupt = 11, 175 | sr_notfound = 12, 176 | sr_full = 13, 177 | sr_cantopen = 14, 178 | sr_protocol = 15, 179 | sr_empty = 16, 180 | sr_schema = 17, 181 | sr_toobig = 18, 182 | sr_constraint = 19, 183 | sr_mismatch = 20, 184 | sr_misuse = 21, 185 | sr_nolfs = 22, 186 | sr_auth = 23, 187 | sr_format = 24, 188 | sr_range = 25, 189 | sr_notadb = 26, 190 | sr_notice = 27, 191 | sr_warning = 28, 192 | sr_row = 100, 193 | sr_done = 101, 194 | sr_ok_load_permanently = 256, 195 | sr_error_missing_collseq = 257, 196 | sr_busy_recovery = 261, 197 | sr_locked_sharedcache = 262, 198 | sr_readonly_recovery = 264, 199 | sr_ioerr_read = 266, 200 | sr_corrupt_vtab = 267, 201 | sr_cantopen_notempdir = 270, 202 | sr_constraint_check = 275, 203 | sr_notice_recover_wal = 283, 204 | sr_warning_autoindex = 284, 205 | sr_error_retry = 513, 206 | sr_abort_rollback = 516, 207 | sr_busy_snapshot = 517, 208 | sr_locked_vtab = 518, 209 | sr_readonly_cantlock = 520, 210 | sr_ioerr_short_read = 522, 211 | sr_corrupt_sequence = 523, 212 | sr_cantopen_isdir = 526, 213 | sr_constraint_commithook = 531, 214 | sr_notice_recover_rollback = 539, 215 | sr_error_snapshot = 769, 216 | sr_busy_timeout = 773, 217 | sr_readonly_rollback = 776, 218 | sr_ioerr_write = 778, 219 | sr_corrupt_index = 779, 220 | sr_cantopen_fullpath = 782, 221 | sr_constraint_foreignkey = 787, 222 | sr_readonly_dbmoved = 1032, 223 | sr_ioerr_fsync = 1034, 224 | sr_cantopen_convpath = 1038, 225 | sr_constraint_function = 1043, 226 | sr_readonly_cantinit = 1288, 227 | sr_ioerr_dir_fsync = 1290, 228 | sr_cantopen_dirtywal = 1294, 229 | sr_constraint_notnull = 1299, 230 | sr_readonly_directory = 1544, 231 | sr_ioerr_truncate = 1546, 232 | sr_cantopen_symlink = 1550, 233 | sr_constraint_primarykey = 1555, 234 | sr_ioerr_fstat = 1802, 235 | sr_constraint_trigger = 1811, 236 | sr_ioerr_unlock = 2058, 237 | sr_constraint_unique = 2067, 238 | sr_ioerr_rdlock = 2314, 239 | sr_constraint_vtab = 2323, 240 | sr_ioerr_delete = 2570, 241 | sr_constraint_rowid = 2579, 242 | sr_ioerr_blocked = 2826, 243 | sr_constraint_pinned = 2835, 244 | sr_ioerr_nomem = 3082, 245 | sr_ioerr_access = 3338, 246 | sr_ioerr_checkreservedlock = 3594, 247 | sr_ioerr_lock = 3850, 248 | sr_ioerr_close = 4106, 249 | sr_ioerr_dir_close = 4362, 250 | sr_ioerr_shmopen = 4618, 251 | sr_ioerr_shmsize = 4874, 252 | sr_ioerr_shmlock = 5130, 253 | sr_ioerr_shmmap = 5386, 254 | sr_ioerr_seek = 5642, 255 | sr_ioerr_delete_noent = 5898, 256 | sr_ioerr_mmap = 6154, 257 | sr_ioerr_gettemppath = 6410, 258 | sr_ioerr_convpath = 6666, 259 | sr_ioerr_vnode = 6972, 260 | sr_ioerr_auth = 7178, 261 | sr_ioerr_begin_atomic = 7434, 262 | sr_ioerr_commit_atomic = 7690, 263 | sr_ioerr_rollback_atomic = 7946, 264 | sr_ioerr_data = 8202 265 | 266 | type SQLiteError* = object of CatchableError 267 | code: Option[ResultCode] 268 | 269 | type SQLiteBlob* = object 270 | raw: ptr UncheckedArray[byte] 271 | len: int 272 | 273 | proc raw*(blob: SQLiteBlob): ptr UncheckedArray[byte] = blob.raw 274 | proc len*(blob: SQLiteBlob): int = blob.len 275 | 276 | template toOpenArray*(blob: SQLiteBlob): untyped = 277 | blob.raw.toOpenArray(0, raw.len) 278 | 279 | type OpenFlag* {.pure, size: sizeof(cint).} = enum 280 | so_readonly, 281 | so_readwrite, 282 | so_create, 283 | so_delete_on_close, 284 | so_exclusive, 285 | so_auto_proxy, 286 | so_uri, 287 | so_memory, 288 | so_main_db, 289 | so_temp_db, 290 | so_transient_db, 291 | so_main_journal, 292 | so_temp_journal, 293 | so_subjournal, 294 | so_super_journal, 295 | so_no_mutex, 296 | so_full_mutex, 297 | so_shared_cache, 298 | so_private_cache, 299 | so_wal, 300 | so_no_follow = 25 301 | 302 | type OpenFlags* = set[OpenFlag] 303 | 304 | type PrepareFlag* = enum 305 | sp_persistent, 306 | sp_normalize, 307 | sp_no_vtab 308 | 309 | type PrepareFlags* = set[PrepareFlag] 310 | 311 | type DatabaseEncoding* = enum 312 | enc_utf8, 313 | enc_utf16, 314 | enc_utf16be, 315 | enc_utf16le, 316 | 317 | type SqliteDestroctor* = proc (p: pointer) {.cdecl.} 318 | 319 | const StaticDestructor* = cast[SqliteDestroctor](0) 320 | const TransientDestructor* = cast[SqliteDestroctor](-1) 321 | 322 | type SqliteDataType* = enum 323 | dt_integer = 1, 324 | dt_float = 2, 325 | dt_text = 3, 326 | dt_blob = 4, 327 | dt_null = 5 328 | 329 | type 330 | SqliteFileCtlOp* {.pure, size: sizeof(cint).} = enum 331 | sf_lockstate = 1, 332 | sf_get_lockproxyfile = 2, 333 | sf_set_lockproxyfile = 3, 334 | sf_last_errno = 4, 335 | sf_size_hint = 5, 336 | sf_chunk_size = 6, 337 | sf_file_pointer = 7, 338 | sf_sync_omitted = 8, 339 | sf_win32_av_retry = 9, 340 | sf_persist_wal = 10, 341 | sf_overwrite = 11, 342 | sf_vfsname = 12, 343 | sf_powersafe_overwrite = 13, 344 | sf_pragma = 14, 345 | sf_busyhandler = 15, 346 | sf_tempfilename = 16, 347 | sf_mmap_size = 18, 348 | sf_trace = 19, 349 | sf_has_moved = 20, 350 | sf_sync = 21, 351 | sf_commit_phasetwo = 22, 352 | sf_win32_set_handle = 23, 353 | sf_wal_block = 24, 354 | sf_zipvfs = 25, 355 | sf_rbu = 26, 356 | sf_vfs_pointer = 27, 357 | sf_journal_pointer = 28, 358 | sf_win32_get_handle = 29, 359 | sf_pdb = 30, 360 | sf_begin_atomic_write = 31, 361 | sf_commit_atomic_write = 32, 362 | sf_rollback_atomic_write = 33, 363 | sf_lock_timeout = 34, 364 | sf_data_version = 35, 365 | sf_size_limit = 36, 366 | sf_ckpt_done = 37, 367 | sf_reserve_bytes = 38, 368 | sf_ckpt_start = 39, 369 | sf_external_reader = 40, 370 | sf_cksm_file = 41, 371 | SqliteLockLevel* {.pure, size: sizeof(cint).} = enum 372 | sl_none, 373 | sl_shared, 374 | sl_reserved, 375 | sl_pending, 376 | sl_exclusive 377 | SqliteAccessFlag* {.pure, size: sizeof(cint).} = enum 378 | access_exists, 379 | access_readwrite, 380 | SqliteShmLockFlag* {.pure, size: sizeof(cint).} = enum 381 | shm_unlock, 382 | shm_lock, 383 | shm_shared, 384 | shm_exclusive 385 | SqliteShmLockFlags* = set[SqliteShmLockFlag] 386 | SqliteDeviceCharacteristic* {.pure, size: sizeof(cint).} = enum 387 | dev_atomic, 388 | dev_atomic512, 389 | dev_atomic1k, 390 | dev_atomic2k, 391 | dev_atomic4k, 392 | dev_atomic8k, 393 | dev_atomic16k, 394 | dev_atomic32k, 395 | dev_atomic64k, 396 | dev_safe_append, 397 | dev_sequential, 398 | dev_undeletable_when_open, 399 | dev_powersafe_overwrite, 400 | dev_immutable, 401 | dev_batch_atomic 402 | SqliteDeviceCharacteristics* = set[SqliteDeviceCharacteristic] 403 | SqliteVFS* = object 404 | version*: cint 405 | osfilesize*: cint 406 | maxpathname*: cint 407 | next: ptr SqliteVFS 408 | name*: cstring 409 | appdata*: pointer 410 | 411 | open* : proc (vfs: ptr SqliteVFS, name: cstring, file: ptr SqliteFile, flags: OpenFlags, outflags: ptr OpenFlags): ResultCode {.cdecl.} 412 | delete* : proc (vfs: ptr SqliteVFS, name: cstring, syncDir: bool): ResultCode {.cdecl.} 413 | access* : proc (vfs: ptr SqliteVFS, name: cstring, flag: SqliteAccessFlag, res: var bool): ResultCode {.cdecl.} 414 | fullpathname* : proc (vfs: ptr SqliteVFS, name: cstring, nOut: cint, zOut: cstring): ResultCode {.cdecl.} 415 | dlopen* : proc (vfs: ptr SqliteVFS, name: cstring): pointer {.cdecl.} 416 | dlerror* : proc (vfs: ptr SqliteVFS, nByte: cint, zErrMsg: cstring): cint {.cdecl.} 417 | dlsym* : proc (vfs: ptr SqliteVFS, lib: pointer, name: cstring): pointer {.cdecl.} 418 | dlclose* : proc (vfs: ptr SqliteVFS, lib: pointer) {.cdecl.} 419 | randomness* : proc (vfs: ptr SqliteVFS, nByte: cint, zOut: cstring): ResultCode {.cdecl.} 420 | sleep* : proc (vfs: ptr SqliteVFS, microsecnods: cint): ResultCode {.cdecl.} 421 | currenttime* : proc (vfs: ptr SqliteVFS, value: ptr float64): ResultCode {.cdecl.} 422 | getlasterror* : proc (vfs: ptr SqliteVFS, nByte: cint, zOut: cstring): ResultCode {.cdecl.} 423 | currenttime64* : proc (vfs: ptr SqliteVFS, value: ptr int64): ResultCode {.cdecl.} 424 | setsystemcall* : proc (vfs: ptr SqliteVFS, name: cstring, p: pointer): ResultCode {.cdecl.} 425 | getsystemcall* : proc (vfs: ptr SqliteVFS, name: cstring): pointer {.cdecl.} 426 | nextsystemcall* : proc (vfs: ptr SqliteVFS, name: cstring): cstring {.cdecl.} 427 | SqliteFile* = object 428 | vtable*: ptr SqliteIoMethods 429 | SqliteIoMethods* = object 430 | version*: cint 431 | 432 | close* : proc (file: ptr SqliteFile): ResultCode {.cdecl.} 433 | read* : proc (file: ptr SqliteFile, buffer: pointer, amt: cint, offset: int64): ResultCode {.cdecl.} 434 | write* : proc (file: ptr SqliteFile, buffer: pointer, amt: cint, offset: int64): ResultCode {.cdecl.} 435 | truncate* : proc (file: ptr SqliteFile, size: cint): ResultCode {.cdecl.} 436 | sync* : proc (file: ptr SqliteFile, flags: cint): ResultCode {.cdecl.} 437 | size* : proc (file: ptr SqliteFile, size: var int64): ResultCode {.cdecl.} 438 | lock* : proc (file: ptr SqliteFile, level: SqliteLockLevel): ResultCode {.cdecl.} 439 | unlock* : proc (file: ptr SqliteFile, level: SqliteLockLevel): ResultCode {.cdecl.} 440 | checklock* : proc (file: ptr SqliteFile, outres: var bool): ResultCode {.cdecl.} 441 | filectl* : proc (file: ptr SqliteFile, op: SqliteFileCtlOp, arg: pointer): ResultCode {.cdecl.} 442 | sectorsize* : proc (file: ptr SqliteFile): cint {.cdecl.} 443 | device* : proc (file: ptr SqliteFile): SqliteDeviceCharacteristics {.cdecl.} 444 | shmmap* : proc (file: ptr SqliteFile, pages: cint, pagesize: cint, extend: bool, target: var pointer): ResultCode {.cdecl.} 445 | shmlock* : proc (file: ptr SqliteFile, offset: cint, n: cint, flags: SqliteShmLockFlags): ResultCode {.cdecl.} 446 | shmbarrier* : proc (file: ptr SqliteFile) {.cdecl.} 447 | shmunmap* : proc (file: ptr SqliteFile, delete: bool): ResultCode {.cdecl.} 448 | fetch* : proc (file: ptr SqliteFile, offset: int64, amt: cint, pp: var pointer): ResultCode {.cdecl.} 449 | unfetch* : proc (file: ptr SqliteFile, offset: int64, p: pointer): ResultCode {.cdecl.} 450 | 451 | proc sqlite3_auto_extension*(entry: pointer): ResultCode {.sqlite3linkage.} 452 | proc sqlite3_vfs_find*(name: cstring): ptr SqliteVFS {.sqlite3linkage.} 453 | proc sqlite3_vfs_register*(vfs: ptr SqliteVFS, default: bool): ResultCode {.sqlite3linkage.} 454 | proc sqlite3_malloc*(size: cint): pointer {.sqlite3linkage.} 455 | proc sqlite3_malloc64*(size: uint64): pointer {.sqlite3linkage.} 456 | proc sqlite3_realloc*(src: pointer, size: cint): pointer {.sqlite3linkage.} 457 | proc sqlite3_realloc64*(src: pointer, size: uint64): pointer {.sqlite3linkage.} 458 | proc sqlite3_free*(src: pointer) {.sqlite3linkage.} 459 | proc sqlite3_msize*(src: pointer): uint64 {.sqlite3linkage.} 460 | proc sqlite3_mprintf*(fmt: cstring): cstring {.sqlite3linkage, varargs.} 461 | proc sqlite3_snprintf*(size: cint, target: cstring, fmt: cstring): cstring {.sqlite3linkage, varargs.} 462 | proc sqlite3_errmsg*(db: ptr RawDatabase): cstring {.sqlite3linkage.} 463 | proc sqlite3_errstr*(code: ResultCode): cstring {.sqlite3linkage.} 464 | proc sqlite3_db_handle*(st: ptr RawStatement): ptr RawDatabase {.sqlite3linkage.} 465 | proc sqlite3_enable_shared_cache*(enabled: int): ResultCode {.sqlite3linkage.} 466 | proc sqlite3_open_v2*(filename: cstring, db: ptr ptr RawDatabase, flags: OpenFlags, vfs: cstring): ResultCode {.sqlite3linkage.} 467 | proc sqlite3_close_v2*(db: ptr RawDatabase): ResultCode {.sqlite3linkage.} 468 | proc sqlite3_prepare_v3*(db: ptr RawDatabase, sql: cstring, nbyte: int, flags: PrepareFlags, pstmt: ptr ptr RawStatement, tail: ptr cstring): ResultCode {.sqlite3linkage.} 469 | proc sqlite3_finalize*(st: ptr RawStatement): ResultCode {.sqlite3linkage.} 470 | proc sqlite3_reset*(st: ptr RawStatement): ResultCode {.sqlite3linkage.} 471 | proc sqlite3_step*(st: ptr RawStatement): ResultCode {.sqlite3linkage.} 472 | proc sqlite3_set_authorizer*(db: ptr RawDatabase, auth: SqliteRawAuthorizer, userdata: pointer): ResultCode {.sqlite3linkage.} 473 | proc sqlite3_bind_parameter_index*(st: ptr RawStatement, name: cstring): int {.sqlite3linkage.} 474 | proc sqlite3_bind_blob64*(st: ptr RawStatement, idx: int, buffer: pointer, len: int, free: SqliteDestroctor): ResultCode {.sqlite3linkage.} 475 | proc sqlite3_bind_double*(st: ptr RawStatement, idx: int, value: float64): ResultCode {.sqlite3linkage.} 476 | proc sqlite3_bind_int64*(st: ptr RawStatement, idx: int, val: int64): ResultCode {.sqlite3linkage.} 477 | proc sqlite3_bind_null*(st: ptr RawStatement, idx: int): ResultCode {.sqlite3linkage.} 478 | proc sqlite3_bind_text*(st: ptr RawStatement, idx: int, val: cstring, len: int32, free: SqliteDestroctor): ResultCode {.sqlite3linkage.} 479 | proc sqlite3_bind_value*(st: ptr RawStatement, idx: int, val: ptr RawValue): ResultCode {.sqlite3linkage.} 480 | proc sqlite3_bind_pointer*(st: ptr RawStatement, idx: int, val: pointer, name: cstring, free: SqliteDestroctor): ResultCode {.sqlite3linkage.} 481 | proc sqlite3_bind_zeroblob64*(st: ptr RawStatement, idx: int, len: int): ResultCode {.sqlite3linkage.} 482 | proc sqlite3_changes*(st: ptr RawDatabase): int {.sqlite3linkage.} 483 | proc sqlite3_last_insert_rowid*(st: ptr RawDatabase): int {.sqlite3linkage.} 484 | proc sqlite3_column_count*(st: ptr RawStatement): int {.sqlite3linkage.} 485 | proc sqlite3_column_type*(st: ptr RawStatement, idx: int): SqliteDataType {.sqlite3linkage.} 486 | proc sqlite3_column_name*(st: ptr RawStatement, idx: int): cstring {.sqlite3linkage.} 487 | proc sqlite3_column_blob*(st: ptr RawStatement, idx: int): pointer {.sqlite3linkage.} 488 | proc sqlite3_column_bytes*(st: ptr RawStatement, idx: int): int {.sqlite3linkage.} 489 | proc sqlite3_column_double*(st: ptr RawStatement, idx: int): float64 {.sqlite3linkage.} 490 | proc sqlite3_column_int64*(st: ptr RawStatement, idx: int): int64 {.sqlite3linkage.} 491 | proc sqlite3_column_text*(st: ptr RawStatement, idx: int): cstring {.sqlite3linkage.} 492 | proc sqlite3_column_value*(st: ptr RawStatement, idx: int): ptr RawValue {.sqlite3linkage.} 493 | 494 | proc newSQLiteError*(code: ResultCode): ref SQLiteError = 495 | result = newException(SQLiteError, $sqlite3_errstr code) 496 | result.code = some code 497 | 498 | proc newSQLiteError*(db: ptr RawDatabase): ref SQLiteError = 499 | newException(SQLiteError, $sqlite3_errmsg db) 500 | 501 | proc newSQLiteError*(db: ptr RawStatement): ref SQLiteError = 502 | newException(SQLiteError, $sqlite3_errmsg sqlite3_db_handle db) 503 | 504 | template sqliteCheck*(res: ResultCode) = 505 | let tmp = res 506 | if tmp != ResultCode.sr_ok: 507 | raise newSQLiteError tmp 508 | 509 | template sqliteCheck*(db: ptr RawDatabase, res: ResultCode) = 510 | let tmp = res 511 | if tmp != ResultCode.sr_ok: 512 | raise newSQLiteError db 513 | 514 | template sqliteCheck*(st: ptr RawStatement, res: ResultCode) = 515 | let tmp = res 516 | if tmp != ResultCode.sr_ok: 517 | raise newSQLiteError st 518 | 519 | proc `=destroy`*(st: var Statement) = 520 | if st.raw != nil: 521 | sqliteCheck sqlite3_finalize st.raw 522 | 523 | preventCopy Statement 524 | 525 | proc `=destroy`*(db: var Database) = 526 | if db.raw != nil: 527 | db.stmtcache.clear() 528 | sqliteCheck sqlite3_close_v2 db.raw 529 | 530 | preventCopy Database 531 | 532 | proc enableSharedCache*(enabled: bool = true) = 533 | sqliteCheck sqlite3_enable_shared_cache(if enabled: 1 else: 0) 534 | 535 | proc initDatabase*( 536 | filename: string, 537 | flags: OpenFlags = {so_readwrite, so_create}, 538 | vfs: cstring = nil 539 | ): Database = 540 | sqliteCheck sqlite3_open_v2(filename, addr result.raw, flags, vfs) 541 | result.stmtcache = initTable[CachedHash[string], ref Statement]() 542 | 543 | proc toS(s: cstring): Option[string] = 544 | if s == nil: 545 | result = none(string) 546 | else: 547 | result = some($s) 548 | 549 | proc setAuthorizer*(db: var Database, callback: RawAuthorizer = nil) = 550 | let userdata: ref WrapAuthorizer = new(WrapAuthorizer) 551 | userdata.authorizer = callback 552 | 553 | proc raw_callback( 554 | userdata: pointer, 555 | action_code: AuthorizerActionCode, 556 | arg3, arg4, arg5, arg6: cstring): AuthorizerResult {.cdecl.} = 557 | let callback = cast[ref WrapAuthorizer](userdata).authorizer 558 | callback(action_code, arg3.toS(), arg4.toS(), arg5.toS(), arg6.toS()) 559 | 560 | var res: ResultCode 561 | if callback == nil: 562 | res = db.raw.sqlite3_set_authorizer(nil, nil) 563 | else: 564 | res = db.raw.sqlite3_set_authorizer(raw_callback, cast[pointer](userdata)) 565 | db.authorizer = userdata 566 | if res != ResultCode.sr_ok: 567 | raise newSQLiteError res 568 | 569 | proc setAuthorizer*(db: var Database, callback: Authorizer = nil) = 570 | var raw_callback: RawAuthorizer = nil 571 | if callback != nil: 572 | raw_callback = proc(code: AuthorizerActionCode, arg3, arg4, arg5, arg6: Option[string]): AuthorizerResult = 573 | var req: AuthorizerRequest 574 | case code 575 | of create_index, create_temp_index, drop_index, drop_temp_index: 576 | req = AuthorizerRequest( 577 | action_code: code, 578 | index_name: arg3.get(""), 579 | index_table_name: arg4.get("")) 580 | of create_table, create_temp_table, delete, drop_table, drop_temp_table, insert, analyze: 581 | req = AuthorizerRequest( 582 | action_code: code, 583 | table_name: arg3.get("")) 584 | of create_temp_trigger, create_trigger, drop_temp_trigger, drop_trigger: 585 | req = AuthorizerRequest( 586 | action_code: code, 587 | trigger_name: arg3.get(""), 588 | trigger_table_name: arg4.get("")) 589 | of create_temp_view, create_view, drop_temp_view, drop_view: 590 | req = AuthorizerRequest( 591 | action_code: code, 592 | view_name: arg3.get("")) 593 | of pragma: 594 | req = AuthorizerRequest( 595 | action_code: code, 596 | pragma_name: arg3.get(""), 597 | pragma_arg: arg4) 598 | of read, update: 599 | req = AuthorizerRequest( 600 | action_code: code, 601 | target_table_name: arg3.get(""), 602 | column_name: arg4.get("")) 603 | of select, recursive, copy: 604 | req = AuthorizerRequest(action_code: code) 605 | of transaction: 606 | req = AuthorizerRequest( 607 | action_code: code, 608 | transaction_operation: arg3.get("")) 609 | of attach: 610 | req = AuthorizerRequest( 611 | action_code: code, 612 | filename: arg3.get("")) 613 | of detach: 614 | req = AuthorizerRequest( 615 | action_code: code, 616 | database_name: arg3.get("")) 617 | of alter_table: 618 | req = AuthorizerRequest( 619 | action_code: code, 620 | alter_database_name: arg3.get(""), 621 | alter_table_name: arg4.get("")) 622 | of reindex: 623 | req = AuthorizerRequest( 624 | action_code: code, 625 | reindex_index_name: arg3.get("")) 626 | of create_vtable, drop_vtable: 627 | req = AuthorizerRequest( 628 | action_code: code, 629 | vtable_name: arg3.get(""), 630 | module_name: arg4.get("")) 631 | of function: 632 | req = AuthorizerRequest( 633 | action_code: code, 634 | # no arg3 635 | function_name: arg4.get("")) 636 | of savepoint: 637 | req = AuthorizerRequest( 638 | action_code: code, 639 | savepoint_operation: arg3.get(""), 640 | savepoint_name: arg4.get("")) 641 | req.db_name = arg5.get("") 642 | req.caller = arg6.get("") 643 | return callback(req) 644 | db.setAuthorizer(raw_callback) 645 | 646 | proc changes*(st: var Database): int = 647 | sqlite3_changes st.raw 648 | 649 | proc changes*(st: ref Statement): int = 650 | sqlite3_changes sqlite3_db_handle st.raw 651 | 652 | proc newStatement*(db: var Database, sql: string, flags: PrepareFlags = {}): ref Statement = 653 | new result 654 | sqliteCheck db.raw, sqlite3_prepare_v3(db.raw, sql, sql.len, flags, 655 | addr result.raw, nil) 656 | 657 | proc fetchStatement*(db: var Database, sql: string): ref Statement = 658 | let rhash = cacheHash(sql) 659 | db.stmtcache.withValue(rhash, value) do: 660 | return value[] 661 | do: 662 | result = db.newStatement(sql, {sp_persistent}) 663 | db.stmtcache[rhash] = result 664 | 665 | proc fetchStatement*(db: var Database, sql: static[string]): ref Statement = 666 | const chash = sql.compileTimeHash 667 | db.stmtcache.withValue(chash, value) do: 668 | return value[] 669 | do: 670 | result = db.newStatement(sql, {sp_persistent}) 671 | db.stmtcache[chash] = result 672 | 673 | proc getParameterIndex*(st: ref Statement, name: string): int = 674 | result = sqlite3_bind_parameter_index(st.raw, name) 675 | if result == 0: 676 | raise newException(SQLiteError, "Unknown parameter " & name) 677 | 678 | {.push inline.} 679 | 680 | proc `[]=`*(st: ref Statement, idx: int, blob: openarray[byte]) = 681 | st.raw.sqliteCheck sqlite3_bind_blob64(st.raw, idx, blob.unsafeAddr, blob.len, TransientDestructor) 682 | 683 | proc `[]=`*(st: ref Statement, idx: int, val: SomeFloat) = 684 | st.raw.sqliteCheck sqlite3_bind_double(st.raw, idx, float64 val) 685 | 686 | proc `[]=`*(st: ref Statement, idx: int, val: SomeOrdinal) = 687 | st.raw.sqliteCheck sqlite3_bind_int64(st.raw, idx, cast[int64](val)) 688 | 689 | proc `[]=`*(st: ref Statement, idx: int, val: type(nil)) = 690 | st.raw.sqliteCheck sqlite3_bind_null(st.raw, idx) 691 | 692 | proc `[]=`*(st: ref Statement, idx: int, val: string) = 693 | st.raw.sqliteCheck sqlite3_bind_text(st.raw, idx, val, int32 val.len, TransientDestructor) 694 | 695 | proc `[]=`*[T](st: ref Statement, idx: int, val: Option[T]) = 696 | if val.is_none: 697 | st.raw.sqliteCheck sqlite3_bind_null(st.raw, idx) 698 | else: 699 | st[idx] = val.get 700 | 701 | proc `[]=`*[T](st: ref Statement, name: string, value: T) = 702 | st[st.getParameterIndex(name)] = value 703 | 704 | proc reset*(st: ref Statement) = 705 | st.raw.sqliteCheck sqlite3_reset(st.raw) 706 | 707 | proc step*(st: ref Statement): bool {.inline.} = 708 | let res = sqlite3_step(st.raw) 709 | case res: 710 | of sr_row: true 711 | of sr_done: false 712 | else: raise newSQLiteError(st.raw) 713 | 714 | proc lastInsertRowid*(st: var Database): int = 715 | sqlite3_last_insert_rowid(st.raw) 716 | 717 | proc withColumnBlob*(st: ref Statement, idx: int, recv: proc(vm: openarray[byte])) = 718 | let p = sqlite3_column_blob(st.raw, idx) 719 | let l = sqlite3_column_bytes(st.raw, idx) 720 | recv(cast[ptr UncheckedArray[byte]](p).toOpenArray(0, l)) 721 | 722 | proc getColumnType*(st: ref Statement, idx: int): SqliteDataType = 723 | sqlite3_column_type(st.raw, idx) 724 | 725 | proc getColumn*(st: ref Statement, idx: int, T: typedesc[seq[byte]]): seq[byte] = 726 | let p = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(st.raw, idx)) 727 | let l = sqlite3_column_bytes(st.raw, idx) 728 | result = newSeq[byte]l 729 | if l > 0: copyMem(addr result[0], p, l) 730 | 731 | proc getColumn*(st: ref Statement, idx: int, T: typedesc[SomeFloat]): SomeFloat = 732 | cast[T](sqlite3_column_double(st.raw, idx)) 733 | 734 | proc getColumn*(st: ref Statement, idx: int, T: typedesc[SomeOrdinal]): SomeOrdinal = 735 | cast[T](sqlite3_column_int64(st.raw, idx)) 736 | 737 | proc getColumn*(st: ref Statement, idx: int, T: typedesc[string]): string = 738 | let p = sqlite3_column_text(st.raw, idx) 739 | let l = sqlite3_column_bytes(st.raw, idx) 740 | result = newString l 741 | if l > 0: copyMem(addr result[0], p, l) 742 | 743 | proc getColumn*[T](st: ref Statement, idx: int, _: typedesc[Option[T]]): Option[T] = 744 | if st.getColumnType(idx) == dt_null: 745 | none(T) 746 | else: 747 | some(st.getColumn(idx, T)) 748 | 749 | type ColumnDef* = object 750 | st*: ref Statement 751 | idx*: int 752 | data_type*: SqliteDataType 753 | name*: string 754 | 755 | proc columns*(st: ref Statement): seq[ref ColumnDef] = 756 | result = @[] 757 | var idx = 0 758 | let count = sqlite3_column_count(st.raw) 759 | while idx < count: 760 | let col = new(ColumnDef) 761 | col.st = st 762 | col.idx = idx 763 | col.data_type = sqlite3_column_type(st.raw, idx) 764 | col.name = $sqlite3_column_name(st.raw, idx) 765 | result.add(col) 766 | idx += 1 767 | 768 | proc `[]`*(st: ref Statement, idx: int): ref ColumnDef = 769 | result = st.columns[idx] 770 | 771 | proc `[]`*[T](col: ref ColumnDef, t: typedesc[T]): T = 772 | result = col.st.getColumn(col.idx, t) 773 | 774 | proc unpack*[T: tuple](st: ref Statement, _: typedesc[T]): T = 775 | var idx = 0 776 | for value in result.fields: 777 | value = st.getColumn(idx, type(value)) 778 | idx.inc 779 | 780 | {.pop.} 781 | 782 | proc exec*(db: var Database, sql: string, cache: static bool = true): int {.discardable.} = 783 | when cache: 784 | let st = db.fetchStatement(sql) 785 | defer: st.reset() 786 | else: 787 | let st = db.newStatement(sql) 788 | if st.step(): 789 | result = st.getColumn(0, int) 790 | 791 | proc execM*(db: var Database, sqls: varargs[string]) {.discardable.} = 792 | discard db.exec "BEGIN IMMEDIATE" 793 | try: 794 | for sql in sqls: 795 | discard db.exec(sql, cache = false) 796 | discard db.exec "COMMIT" 797 | except CatchableError: 798 | discard db.exec "ROLLBACK" 799 | raise getCurrentException() 800 | 801 | iterator rows*(st: ref Statement): seq[ref ColumnDef] = 802 | try: 803 | while st.step(): 804 | yield st.columns() 805 | finally: 806 | st.reset() 807 | -------------------------------------------------------------------------------- /src/easy_sqlite3/logfs.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | 3 | import std/strformat 4 | import ./bindings 5 | 6 | type AppendedInfo = object 7 | name: string 8 | origvt: ptr SqliteIoMethods 9 | 10 | let defaultvfs = sqlite3_vfs_find(nil) 11 | 12 | var logvfs = defaultvfs[] 13 | var logvtb: SqliteIoMethods 14 | logvfs.name = "logfs" 15 | logvfs.osfilesize += AppendedInfo.sizeof.cint 16 | 17 | proc appended(file: ptr SqliteFile): var AppendedInfo = 18 | cast[ptr AppendedInfo](cast[ByteAddress](file) + defaultvfs.osfilesize)[] 19 | 20 | proc name(file: ptr SqliteFile): string = 21 | file.appended.name 22 | proc `name=`(file: ptr SqliteFile, value: string) = 23 | file.appended.name = value 24 | 25 | proc origvt(file: ptr SqliteFile): ptr SqliteIoMethods = 26 | file.appended.origvt 27 | proc `origvt=`(file: ptr SqliteFile, value: ptr SqliteIoMethods) = 28 | file.appended.origvt = value 29 | 30 | {.push warnings:off.} 31 | logvfs.open = proc (vfs: ptr SqliteVFS, name: cstring, file: ptr SqliteFile, flags: OpenFlags, outflags: ptr OpenFlags): ResultCode {.cdecl.} = 32 | echo fmt"open {name} ({flags})" 33 | result = defaultvfs.open(defaultvfs, name, file, flags, outflags) 34 | if result == sr_ok: 35 | if outflags != nil: 36 | echo fmt"open-result {outflags[]}" 37 | file.origvt = file.vtable 38 | file.vtable = addr logvtb 39 | file.name = $name 40 | {.pop.} 41 | 42 | logvfs.delete = proc (vfs: ptr SqliteVFS, name: cstring, syncDir: bool): ResultCode {.cdecl.} = 43 | echo fmt"delete {name}" 44 | defaultvfs.delete(defaultvfs, name, syncDir) 45 | 46 | logvtb.version = 2 47 | 48 | logvtb.close = proc (file: ptr SqliteFile): ResultCode {.cdecl.} = 49 | echo fmt"close {file.name}" 50 | defer: `=destroy`(file.appended) 51 | file.origvt.close(file) 52 | 53 | logvtb.read = proc (file: ptr SqliteFile, buffer: pointer, amt: cint, offset: int64): ResultCode {.cdecl.} = 54 | echo fmt"read {file.name} (offset: {offset}, size: {amt})" 55 | file.origvt.read(file, buffer, amt, offset) 56 | 57 | logvtb.write = proc (file: ptr SqliteFile, buffer: pointer, amt: cint, offset: int64): ResultCode {.cdecl.} = 58 | echo fmt"write {file.name} (offset: {offset}, size: {amt})" 59 | file.origvt.write(file, buffer, amt, offset) 60 | 61 | logvtb.truncate = proc (file: ptr SqliteFile, size: cint): ResultCode {.cdecl.} = 62 | echo fmt"truncate {file.name} {size}" 63 | file.origvt.truncate(file, size) 64 | 65 | logvtb.sync = proc (file: ptr SqliteFile, flags: cint): ResultCode {.cdecl.} = 66 | echo fmt"sync {file.name} ({flags})" 67 | file.origvt.sync(file, flags) 68 | 69 | logvtb.size = proc (file: ptr SqliteFile, size: var int64): ResultCode {.cdecl.} = 70 | result = file.origvt.size(file, size) 71 | echo fmt"size {file.name} = {size}" 72 | 73 | logvtb.lock = proc (file: ptr SqliteFile, level: SqliteLockLevel): ResultCode {.cdecl.} = 74 | echo fmt"lock {file.name} ({level})" 75 | file.origvt.lock(file, level) 76 | 77 | logvtb.unlock = proc (file: ptr SqliteFile, level: SqliteLockLevel): ResultCode {.cdecl.} = 78 | echo fmt"unlock {file.name} ({level})" 79 | file.origvt.unlock(file, level) 80 | 81 | logvtb.checklock = proc (file: ptr SqliteFile, res: var bool): ResultCode {.cdecl.} = 82 | result = file.origvt.checklock(file, res) 83 | echo fmt"checklock {file.name} = {res}" 84 | 85 | logvtb.filectl = proc (file: ptr SqliteFile, op: SqliteFileCtlOp, arg: pointer): ResultCode {.cdecl.} = 86 | echo fmt"filectl {file.name} ({op})" 87 | case op 88 | of sf_vfsname: 89 | let p = sqlite3_mprintf("%s", logvfs.name) 90 | cast[ptr cstring](arg)[] = p 91 | of sf_vfs_pointer: 92 | cast[ptr ptr SqliteVFS](arg)[] = addr logvfs 93 | else: 94 | result = file.origvt.filectl(file, op, arg) 95 | 96 | logvtb.sectorsize = proc (file: ptr SqliteFile): cint {.cdecl.} = 97 | result = file.origvt.sectorsize(file) 98 | echo fmt"sectorsize {file.name} = {result}" 99 | 100 | logvtb.device = proc (file: ptr SqliteFile): SqliteDeviceCharacteristics {.cdecl.} = 101 | result = file.origvt.device(file) 102 | echo fmt"device {file.name} = {result}" 103 | 104 | logvtb.shmmap = proc (file: ptr SqliteFile, pages: cint, pagesize: cint, extend: bool, target: var pointer): ResultCode {.cdecl.} = 105 | echo fmt"shmmap {file.name} (pages: {pages} pagesize: {pagesize} extend: {extend})" 106 | file.origvt.shmmap(file, pages, pagesize, extend, target) 107 | 108 | logvtb.shmlock = proc (file: ptr SqliteFile, offset: cint, n: cint, flags: SqliteShmLockFlags): ResultCode {.cdecl.} = 109 | echo fmt"shmlock {file.name} (offset: {offset} n: {n} flags: {flags})" 110 | file.origvt.shmlock(file, offset, n, flags) 111 | 112 | logvtb.shmbarrier = proc (file: ptr SqliteFile) {.cdecl.} = 113 | echo fmt"shmbarrier {file.name}" 114 | file.origvt.shmbarrier(file) 115 | 116 | logvtb.shmunmap = proc (file: ptr SqliteFile, delete: bool): ResultCode {.cdecl.} = 117 | echo fmt"shmunmap {file.name} ({delete})" 118 | file.origvt.shmunmap(file, delete) 119 | 120 | sqliteCheck sqlite3_vfs_register(logvfs.addr, true) 121 | -------------------------------------------------------------------------------- /src/easy_sqlite3/macros.nim: -------------------------------------------------------------------------------- 1 | import std/[macros, options] 2 | import ./bindings, ./utils 3 | 4 | proc injectDbDecl(result: var NimNode, db_ident: NimNode) = 5 | result[3].insert(1, nnkIdentDefs.newTree( 6 | db_ident, 7 | nnkVarTy.newTree(bindSym "Database"), 8 | newEmptyNode() 9 | )) 10 | 11 | proc injectDbFetch(procbody: var NimNode, sql: string, db_ident, st_ident: NimNode) = 12 | procbody.add nnkVarSection.newTree( 13 | nnkIdentDefs.newTree( 14 | st_ident, 15 | newEmptyNode(), 16 | nnkCall.newTree( 17 | nnkDotExpr.newTree( 18 | db_ident, 19 | bindSym "fetchStatement" 20 | ), 21 | newLit sql 22 | ) 23 | ) 24 | ) 25 | 26 | proc injectDbArguments(procbody: var NimNode, body, st_ident: NimNode): seq[tuple[name: string, idxnode, param: NimNode]] = 27 | result = newSeq[tuple[name: string, idxnode, param: NimNode]]() 28 | procbody.addTree(nnkVarSection, varsec): 29 | for args in body[3][1..^1]: 30 | for arg in args[0..^3]: 31 | let arg_name = $arg 32 | let arg_ident = genSym(nskVar, arg_name & "_idx") 33 | result.add (name: arg_name, idxnode: arg_ident, param: arg) 34 | varsec.add nnkIdentDefs.newTree( 35 | nnkPragmaExpr.newTree( 36 | arg_ident, 37 | nnkPragma.newTree( 38 | ident "threadvar" 39 | ) 40 | ), 41 | ident "int", 42 | newEmptyNode() 43 | ) 44 | for it in result: 45 | procbody.add nnkIfStmt.newTree( 46 | nnkElifBranch.newTree( 47 | nnkInfix.newTree( 48 | ident "==", 49 | it.idxnode, 50 | newLit 0 51 | ), 52 | nnkStmtList.newTree( 53 | nnkAsgn.newTree( 54 | it.idxnode, 55 | nnkCall.newTree( 56 | nnkDotExpr.newTree( 57 | st_ident, 58 | bindSym "getParameterIndex" 59 | ), 60 | newLit "$" & it.name 61 | ) 62 | ) 63 | ) 64 | ) 65 | ) 66 | # procbody.add nnkCall.newTree( 67 | # nnkDotExpr.newTree( 68 | # st_ident, 69 | # bindSym "reset" 70 | # ) 71 | # ) 72 | for it in result: 73 | procbody.add nnkAsgn.newTree( 74 | nnkBracketExpr.newTree( 75 | st_ident, 76 | it.idxnode 77 | ), 78 | it.param 79 | ) 80 | 81 | proc fillPar(ret, st_ident: NimNode): NimNode = 82 | nnkPar.genTree(parbody): 83 | for idx, it in ret: 84 | parbody.add nnkExprColonExpr.newTree( 85 | it[0], 86 | nnkCall.newTree( 87 | nnkDotExpr.newTree( 88 | st_ident, 89 | ident "getColumn" 90 | ), 91 | newLit idx, 92 | nnkBracketExpr.newTree(newIdentNode("typedesc"),it[1]) 93 | ) 94 | ) 95 | 96 | proc genQueryIterator(sql: string, body: NimNode): NimNode = 97 | result = body.copy() 98 | let db_ident = genSym(nskParam, "db") 99 | let st_ident = genSym(nskVar, "st") 100 | 101 | let rettype = block: 102 | case result[3][0].kind 103 | of nnkTupleTy: 104 | result[3][0] 105 | of nnkSym: 106 | let typeDef = result[3][0].getImpl 107 | typeDef.expectKind nnkTypeDef 108 | let tuplDef = typeDef[2] 109 | tuplDef.expectKind nnkTupleTy 110 | tuplDef 111 | else: 112 | error "Expected a tuple type", result[3][0] 113 | nil 114 | 115 | injectDbDecl(result, db_ident) 116 | result[6] = nnkStmtList.genTree(procbody): 117 | injectDbFetch(procbody, sql, db_ident, st_ident) 118 | discard injectDbArguments(procbody, body, st_ident) 119 | procbody.addTree(nnkTryStmt, trybody): 120 | trybody.addTree(nnkWhileStmt, whilebody): 121 | whilebody.add nnkCall.newTree(nnkDotExpr.newTree(st_ident, bindSym "step")) 122 | whilebody.addTree(nnkYieldStmt, yieldbody): 123 | yieldbody.add fillPar(rettype, st_ident) 124 | trybody.addTree(nnkFinally, finallybody): 125 | finallybody.add nnkCall.newTree( 126 | nnkDotExpr.newTree( 127 | st_ident, 128 | bindSym "reset" 129 | ) 130 | ) 131 | 132 | proc genQueryProcedure(sql: string, body, tupdef: NimNode, opt: static bool): NimNode = 133 | result = body.copy() 134 | let db_ident = genSym(nskParam, "db") 135 | let st_ident = genSym(nskVar, "st") 136 | let rettype = when opt: 137 | result[3][0][1] 138 | else: 139 | tupdef 140 | injectDbDecl(result, db_ident) 141 | result[6] = nnkStmtList.genTree(procbody): 142 | injectDbFetch(procbody, sql, db_ident, st_ident) 143 | discard injectDbArguments(procbody, body, st_ident) 144 | procbody.addTree(nnkTryStmt, trybody): 145 | trybody.addTree(nnkIfStmt, ifbody): 146 | ifbody.addTree(nnkElifBranch, branch): 147 | branch.add nnkCall.newTree(nnkDotExpr.newTree(st_ident, bindSym "step")) 148 | branch.addTree(nnkStmtList, resultstmt): 149 | resultstmt.addTree(nnkAsgn, retbody): 150 | retbody.add ident "result" 151 | let tmp = fillPar(rettype, st_ident) 152 | when opt: 153 | retbody.add nnkCommand.newTree(bindSym "some", tmp) 154 | else: 155 | retbody.add tmp 156 | resultstmt.addTree(nnkIfStmt, ifbody2): 157 | ifbody2.addTree(nnkElifBranch, dup_branch): 158 | dup_branch.add nnkCall.newTree(nnkDotExpr.newTree(st_ident, bindSym "step")) 159 | dup_branch.add nnkRaiseStmt.newTree( 160 | nnkCall.newTree(bindSym "newException", ident "SQLiteError", newLit "Too many results") 161 | ) 162 | ifbody.addTree(nnkElse, elsebody): 163 | when opt: 164 | elsebody.add nnkReturnStmt.newTree( 165 | nnkCommand.newTree(bindSym "none", rettype) 166 | ) 167 | else: 168 | elsebody.add nnkRaiseStmt.newTree( 169 | nnkCall.newTree(bindSym "newException", ident "SQLiteError", newLit "No results") 170 | ) 171 | trybody.addTree(nnkFinally, finallybody): 172 | finallybody.add nnkCall.newTree( 173 | nnkDotExpr.newTree( 174 | st_ident, 175 | bindSym "reset" 176 | ) 177 | ) 178 | 179 | proc genUpdateProcedure(sql: string, body: NimNode): NimNode = 180 | result = body.copy() 181 | let db_ident = genSym(nskParam, "db") 182 | let st_ident = genSym(nskVar, "st") 183 | injectDbDecl(result, db_ident) 184 | result[6] = nnkStmtList.genTree(procbody): 185 | injectDbFetch(procbody, sql, db_ident, st_ident) 186 | discard injectDbArguments(procbody, body, st_ident) 187 | procbody.addTree(nnkTryStmt, trybody): 188 | trybody.addTree(nnkIfStmt, ifbody): 189 | ifbody.addTree(nnkElifBranch, branch): 190 | branch.add nnkCall.newTree(nnkDotExpr.newTree(st_ident, bindSym "step")) 191 | branch.add nnkRaiseStmt.newTree( 192 | nnkCall.newTree(bindSym "newException", ident "SQLiteError", newLit "Invalid update") 193 | ) 194 | ifbody.addTree(nnkElse, elsebody): 195 | elsebody.add nnkReturnStmt.newTree( 196 | nnkCall.newTree(nnkDotExpr.newTree(db_ident, bindSym "lastInsertRowid")) 197 | ) 198 | trybody.addTree(nnkFinally, finallybody): 199 | finallybody.add nnkCall.newTree( 200 | nnkDotExpr.newTree( 201 | st_ident, 202 | bindSym "reset" 203 | ) 204 | ) 205 | 206 | proc genCreateProcedure(sql: string, body: NimNode): NimNode = 207 | result = body.copy() 208 | let db_ident = genSym(nskParam, "db") 209 | let st_ident = genSym(nskVar, "st") 210 | injectDbDecl(result, db_ident) 211 | result[6] = nnkStmtList.genTree(procbody): 212 | injectDbFetch(procbody, sql, db_ident, st_ident) 213 | discard injectDbArguments(procbody, body, st_ident) 214 | procbody.addTree(nnkTryStmt, trybody): 215 | trybody.addTree(nnkIfStmt, ifbody): 216 | ifbody.addTree(nnkElifBranch, branch): 217 | branch.add nnkCall.newTree(nnkDotExpr.newTree(st_ident, bindSym "step")) 218 | branch.add nnkRaiseStmt.newTree( 219 | nnkCall.newTree(bindSym "newException", ident "SQLiteError", newLit "Invalid statement") 220 | ) 221 | trybody.addTree(nnkFinally, finallybody): 222 | finallybody.add nnkCall.newTree( 223 | nnkDotExpr.newTree( 224 | st_ident, 225 | bindSym "reset" 226 | ) 227 | ) 228 | 229 | macro importdb*(sql: static string, body: typed) = 230 | case body.kind: 231 | of nnkProcDef: 232 | let ret = body[3][0] 233 | case ret.kind: 234 | of nnkEmpty: 235 | result = genCreateProcedure(sql, body) 236 | of nnkSym: 237 | case ret.strVal: 238 | of "int": 239 | result = genUpdateProcedure(sql, body) 240 | else: 241 | let typImpl = ret.getImpl 242 | typImpl.expectKind nnkTypeDef 243 | let tuplImpl = typImpl[2] 244 | tuplImpl.expectKind nnkTupleTy 245 | result = genQueryProcedure(sql, body, tuplImpl, false) 246 | of nnkBracketExpr: 247 | ret[0].expectIdent "Option" 248 | ret[1].expectKind nnkTupleTy 249 | result = genQueryProcedure(sql, body, ret[1], true) 250 | of nnkTupleTy: 251 | result = genQueryProcedure(sql, body, ret, false) 252 | else: 253 | error("Expected int, tuple, Option[tuple]") 254 | return 255 | of nnkIteratorDef: 256 | let retType = body[3][0] 257 | case retType.kind 258 | of nnkTupleTy, nnkSym: 259 | result = genQueryIterator(sql, body) 260 | else: 261 | error("Expected a tuple type", retType) 262 | else: 263 | error("Expected proc or iterator, got " & $body.kind, body) 264 | return 265 | 266 | if result[0].isExported: 267 | result[0] = nnkPostfix.newTree( 268 | "*".ident, 269 | result[0].strVal.ident 270 | ) 271 | else: 272 | result[0] = result[0].strVal.ident 273 | 274 | proc db_begin_deferred() {.importdb: "BEGIN DEFERRED".} 275 | proc db_begin_immediate() {.importdb: "BEGIN IMMEDIATE".} 276 | proc db_begin_exclusive() {.importdb: "BEGIN EXCLUSIVE".} 277 | proc db_commit() {.importdb: "COMMIT".} 278 | proc db_rollback() {.importdb: "ROLLBACK".} 279 | 280 | template gen_transaction(db: var Database, begin_stmt, body: untyped): untyped = 281 | begin_stmt db 282 | block outer: 283 | template commit() {.inject, used.} = 284 | db_commit db 285 | break outer 286 | template rollback() {.inject, used.} = 287 | db_rollback db 288 | break outer 289 | block inner: 290 | try: 291 | body 292 | commit() 293 | except CatchableError: 294 | db_rollback db 295 | raise getCurrentException() 296 | 297 | template transaction*(db: var Database, body: untyped): untyped = 298 | gen_transaction db, db_begin_deferred, body 299 | 300 | template transactionImmediate*(db: var Database, body: untyped): untyped = 301 | gen_transaction db, db_begin_immediate, body 302 | 303 | template transactionExclusive*(db: var Database, body: untyped): untyped = 304 | gen_transaction db, db_begin_exclusive, body 305 | -------------------------------------------------------------------------------- /src/easy_sqlite3/memfs.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | 3 | import std/[tables, locks] 4 | import ./bindings 5 | 6 | var glock: Lock 7 | glock.initLock() 8 | 9 | const 10 | PAGE_SIZE = 4096 11 | SHM_PAGE_SIZE = 32768 12 | 13 | type 14 | FileKind* = enum 15 | fk_main 16 | fk_temp 17 | fk_transient 18 | fk_main_journal 19 | fk_temp_journal 20 | fk_sub_journal 21 | fk_super_journal 22 | fk_wal 23 | ShmLockKind = enum 24 | slk_none, 25 | slk_shared, 26 | slk_exclusive 27 | ShmLockState = distinct int 28 | ShmFile = object 29 | chunks: seq[ref array[SHM_PAGE_SIZE, byte]] 30 | shmlocks: array[8, ShmLockState] 31 | lock: Lock 32 | MemoryFile = object 33 | kind: FileKind 34 | pages: seq[ref array[PAGE_SIZE, byte]] 35 | shm: ref ShmFile 36 | size: int 37 | refc: int 38 | fileLock: Lock 39 | state: SqliteLockLevel 40 | shareds: int 41 | MemoryFileStat* = object 42 | name*: string 43 | kind*: FileKind 44 | size*: int 45 | refc*: int 46 | MemoryFileInfo = object 47 | base: SqliteFile 48 | data: ptr MemoryFile 49 | locklevel: SqliteLockLevel 50 | shmlocklevel: array[8, ShmLockKind] 51 | 52 | proc createMemoryFile(kind: FileKind): ptr MemoryFile = 53 | result = create MemoryFile 54 | result[].kind = kind 55 | result[].fileLock.initLock() 56 | result[].pages = newSeqOfCap[ref array[PAGE_SIZE, byte]](1024) 57 | 58 | proc initShmFile(file: ptr MemoryFile) {.inline.} = 59 | if file.shm == nil: 60 | new file.shm 61 | file.shm.lock.initLock() 62 | 63 | proc shm_try_share(state: ShmLockState): bool = state.int >= 0 64 | proc shm_try_unshare(state: ShmLockState): bool = state.int > 0 65 | proc shm_try_exclusive(state: ShmLockState): bool = state.int == 0 66 | proc shm_try_release(state: ShmLockState): bool = state.int == -1 67 | 68 | proc shm_share(state: var ShmLockState) = state.int.inc 69 | proc shm_unshare(state: var ShmLockState) = state.int.dec 70 | proc shm_exclusive(state: var ShmLockState) = state = (-1).ShmLockState 71 | proc shm_release(state: var ShmLockState) = state = 0.ShmLockState 72 | 73 | proc lockShmFile(file: ptr MemoryFile, offset, length: int, flags: SqliteShmLockFlags): bool = 74 | assert offset >= 0 and length > 0 and offset + length <= 8 75 | assert file.shm != nil 76 | let (tfn, afn) = if shm_unlock in flags: 77 | if shm_shared in flags: 78 | (shm_try_unshare, shm_unshare) 79 | else: 80 | (shm_try_release, shm_release) 81 | else: 82 | if shm_shared in flags: 83 | (shm_try_share, shm_share) 84 | else: 85 | (shm_try_exclusive, shm_exclusive) 86 | file.shm.lock.withLock: 87 | for i in offset.. oldpagecount: 115 | for i in oldpagecount.. 0: 141 | result = sr_ioerr_short_read 142 | zeroMem(addr buffer[length - remain], remain) 143 | 144 | proc writeBuffer(self: ptr MemoryFile, buffer: ptr UncheckedArray[byte], length: int, offset: int): ResultCode = 145 | let max = length + offset 146 | if max > self.size: 147 | self.resize(max) 148 | for i, p in self.range(offset, length): 149 | p[] = buffer[i] 150 | 151 | var root = initTable[string, ptr MemoryFile]() 152 | 153 | iterator listMemfs*(): MemoryFileStat = 154 | glock.withLock: 155 | for name, file in root: 156 | yield MemoryFileStat(name: name, kind: file.kind, size: file.size, refc: file.refc) 157 | 158 | proc removeMemoryFile*(name: string): bool = 159 | glock.withLock: 160 | root.withValue(name, file) do: 161 | if file.refc != 0: 162 | return false 163 | `=destroy` file[] 164 | dealloc file 165 | root.del name 166 | return true 167 | 168 | proc getMemoryFile(cname: cstring, filekind: FileKind): ptr MemoryFile = 169 | let name = $cname 170 | glock.withLock: 171 | root.withValue(name, file) do: 172 | assert file.kind == filekind 173 | result = file[] 174 | do: 175 | result = createMemoryFile(filekind) 176 | root[name] = result 177 | result.refc.inc 178 | 179 | let defaultvfs = sqlite3_vfs_find(nil) 180 | var memvfs = defaultvfs[] 181 | var memios: SqliteIoMethods 182 | memvfs.version = 2 183 | memvfs.name = "memvfs" 184 | memvfs.maxpathname = 255 185 | memvfs.osfilesize = MemoryFileInfo.sizeof.cint 186 | memios.version = 2 187 | 188 | memvfs.open = proc (vfs: ptr SqliteVFS, name: cstring, file: ptr SqliteFile, flags: OpenFlags, outflags: ptr OpenFlags): ResultCode {.cdecl.} = 189 | var kind: FileKind 190 | if so_main_db in flags: 191 | kind = fk_main 192 | elif so_temp_db in flags: 193 | kind = fk_temp 194 | elif so_transient_db in flags: 195 | kind = fk_transient 196 | elif so_main_journal in flags: 197 | kind = fk_main_journal 198 | elif so_temp_journal in flags: 199 | kind = fk_temp_journal 200 | elif so_subjournal in flags: 201 | kind = fk_sub_journal 202 | elif so_super_journal in flags: 203 | kind = fk_super_journal 204 | elif so_wal in flags: 205 | kind = fk_wal 206 | else: 207 | return sr_misuse 208 | file.openMemoryFile getMemoryFile(name, kind) 209 | file.vtable = addr memios 210 | if outflags != nil: 211 | if so_readwrite in flags: 212 | outflags[] = {so_readwrite} 213 | else: 214 | outflags[] = {so_readonly} 215 | 216 | memvfs.delete = proc (vfs: ptr SqliteVFS, cname: cstring, syncDir: bool): ResultCode {.cdecl.} = 217 | let name = $cname 218 | glock.withLock: 219 | root.withValue(name, file): 220 | assert file.refc >= 0 221 | if file.refc == 0: 222 | `=destroy` file[] 223 | dealloc file 224 | root.del name 225 | else: 226 | return sr_ioerr 227 | 228 | memvfs.access = proc (vfs: ptr SqliteVFS, cname: cstring, flag: SqliteAccessFlag, res: var bool): ResultCode {.cdecl.} = 229 | let name = $cname 230 | glock.withLock: 231 | res = name in root 232 | 233 | memvfs.fullpathname = proc (vfs: ptr SqliteVFS, name: cstring, nOut: cint, zOut: cstring): ResultCode {.cdecl.} = 234 | let l = name.len + 1 235 | if l >= nOut: 236 | return sr_cantopen 237 | copyMem(zOut, name, l) 238 | 239 | memios.close = proc (file: ptr SqliteFile): ResultCode {.cdecl.} = 240 | file.data.close() 241 | 242 | memios.read = proc (file: ptr SqliteFile, buffer: pointer, amt: cint, offset: int64): ResultCode {.cdecl.} = 243 | result = file.data.readBuffer(cast[ptr UncheckedArray[byte]](buffer), amt.int, offset.int) 244 | 245 | memios.write = proc (file: ptr SqliteFile, buffer: pointer, amt: cint, offset: int64): ResultCode {.cdecl.} = 246 | result = file.data.writeBuffer(cast[ptr UncheckedArray[byte]](buffer), amt.int, offset.int) 247 | 248 | memios.truncate = proc (file: ptr SqliteFile, size: cint): ResultCode {.cdecl.} = 249 | file.data.resize(size.int) 250 | 251 | memios.sync = proc (file: ptr SqliteFile, flags: cint): ResultCode {.cdecl.} = fence() 252 | 253 | memios.size = proc (file: ptr SqliteFile, size: var int64): ResultCode {.cdecl.} = 254 | size = file.data.size.int64 255 | 256 | memios.lock = proc (file: ptr SqliteFile, level: SqliteLockLevel): ResultCode {.cdecl.} = 257 | if level <= file.locklevel: 258 | return 259 | file.data.fileLock.withLock: 260 | assert file.data[].state >= file.locklevel 261 | case file.locklevel: 262 | of sl_none: # from 263 | case level: 264 | of sl_shared: # to 265 | case file.data[].state: 266 | of sl_none: # global 267 | file.data[].shareds = 1 268 | of sl_shared, sl_reserved: # global 269 | file.data[].shareds.inc 270 | else: 271 | return sr_busy 272 | else: 273 | return sr_misuse 274 | of sl_shared: # from 275 | case level: 276 | of sl_reserved: # to 277 | case file.data[].state: 278 | of sl_shared: # global 279 | discard 280 | else: 281 | return sr_busy 282 | of sl_pending: # to 283 | case file.data[].state: 284 | of sl_shared: # global 285 | discard 286 | else: 287 | return sr_busy 288 | of sl_exclusive: # to 289 | case file.data[].state: 290 | of sl_shared: # global 291 | if file.data[].shareds > 1: 292 | return sr_busy 293 | file.data[].shareds = 0 294 | else: 295 | return sr_busy 296 | else: 297 | return sr_misuse 298 | of sl_reserved: # from 299 | case level: 300 | of sl_pending: # to 301 | case file.data[].state: 302 | of sl_reserved: # global 303 | discard 304 | else: 305 | return sr_busy 306 | of sl_exclusive: # to 307 | case file.data[].state: 308 | of sl_reserved: # global 309 | if file.data[].shareds > 1: 310 | return sr_busy 311 | file.data[].shareds = 0 312 | else: 313 | return sr_busy 314 | else: 315 | return sr_misuse 316 | of sl_pending: # from 317 | case level: 318 | of sl_exclusive: # to 319 | if file.data[].shareds > 1: 320 | return sr_busy 321 | file.data[].shareds = 0 322 | else: 323 | return sr_misuse 324 | else: 325 | assert false, "invalid lock state" 326 | return sr_error 327 | file.locklevel = level 328 | file.data[].state = level 329 | 330 | memios.unlock = proc (file: ptr SqliteFile, level: SqliteLockLevel): ResultCode {.cdecl.} = 331 | if level >= file.locklevel: 332 | return 333 | if not (level in {sl_none, sl_shared}): 334 | return sr_misuse 335 | file.data.fileLock.withLock: 336 | assert file.data[].state >= file.locklevel 337 | case file.locklevel: 338 | of sl_exclusive: # from 339 | assert file.data[].shareds == 0 340 | if level == sl_shared: 341 | file.data[].shareds = 1 342 | file.data[].state = level 343 | of sl_pending, sl_reserved: # from 344 | assert file.data[].state == file.locklevel 345 | file.data[].state = sl_shared 346 | if level == sl_none: 347 | file.data[].shareds.dec 348 | if file.data[].shareds == 0: 349 | file.data[].state = sl_none 350 | of sl_shared: # from 351 | assert level == sl_none 352 | assert file.data[].state >= sl_shared 353 | file.data[].shareds.dec 354 | if file.data[].shareds == 0: 355 | assert file.data[].state == sl_shared 356 | file.data[].state = sl_none 357 | else: 358 | assert false, "invalid unlock state" 359 | return sr_error 360 | file.locklevel = level 361 | 362 | memios.checklock = proc (file: ptr SqliteFile, outres: var bool): ResultCode {.cdecl.} = 363 | file.data.fileLock.withLock: 364 | outres = file.data[].state >= sl_reserved 365 | 366 | memios.filectl = proc (file: ptr SqliteFile, op: SqliteFileCtlOp, arg: pointer): ResultCode {.cdecl.} = 367 | case op 368 | of sf_vfsname: 369 | let p = sqlite3_mprintf("%s", memvfs.name) 370 | cast[ptr cstring](arg)[] = p 371 | of sf_vfs_pointer: 372 | cast[ptr ptr SqliteVFS](arg)[] = addr memvfs 373 | else: 374 | return sr_notfound 375 | 376 | memios.sectorsize = proc (file: ptr SqliteFile): cint {.cdecl.} = 377 | return PAGE_SIZE 378 | 379 | memios.device = proc (file: ptr SqliteFile): SqliteDeviceCharacteristics {.cdecl.} = 380 | return { dev_sequential, dev_atomic4k, dev_safe_append, dev_powersafe_overwrite } 381 | 382 | memios.shmmap = proc (file: ptr SqliteFile, page: cint, pagesize: cint, extend: bool, target: var pointer): ResultCode {.cdecl.} = 383 | assert file.data.kind in {fk_main, fk_temp} 384 | assert pagesize <= SHM_PAGE_SIZE 385 | file.data.initShmFile() 386 | if page >= file.data.shm.chunks.len: 387 | if extend: 388 | let oldlen = file.data.shm.chunks.len 389 | file.data.shm.chunks.setLen(page + 1) 390 | for i in oldLen..page: 391 | new file.data.shm.chunks[i] 392 | else: 393 | target = nil 394 | return 395 | target = file.data.shm.chunks[page][0].addr 396 | 397 | memios.shmlock = proc (file: ptr SqliteFile, offset: cint, n: cint, flags: SqliteShmLockFlags): ResultCode {.cdecl.} = 398 | if not lockShmFile(file.data, offset.int, n.int, flags): 399 | return sr_busy 400 | 401 | memios.shmbarrier = proc (file: ptr SqliteFile) {.cdecl.} = fence() 402 | 403 | memios.shmunmap = proc (file: ptr SqliteFile, delete: bool): ResultCode {.cdecl.} = discard 404 | 405 | sqliteCheck sqlite3_vfs_register(addr memvfs, true) -------------------------------------------------------------------------------- /src/easy_sqlite3/nim.cfg: -------------------------------------------------------------------------------- 1 | threads:on -------------------------------------------------------------------------------- /src/easy_sqlite3/utils.nim: -------------------------------------------------------------------------------- 1 | import std/macros 2 | 3 | template preventCopy*(T: untyped): untyped = 4 | proc `=copy`*(l: var T, r: T) {.error.} 5 | 6 | template genTree*(kind: NimNodeKind, local, body: untyped): NimNode = 7 | var local {.gensym.} = kind.newNimNode() 8 | body 9 | local 10 | 11 | template addTree*(src: NimNode, kind: NimNodeKind, local, body: untyped) = 12 | var local {.gensym.} = kind.newNimNode() 13 | body 14 | src.add local 15 | 16 | proc getNimIdent*(src: NimNode): string = 17 | case src.kind: 18 | of nnkIdent: 19 | return src.strVal 20 | of nnkPostfix: 21 | src[0].expectIdent "*" 22 | src[1].expectKind nnkIdent 23 | return src[1].strVal 24 | else: 25 | error("Not an ident node") -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !nim.cfg 3 | !test_*.nim 4 | !.gitignore 5 | !config.nims -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on 2 | --gc:arc -------------------------------------------------------------------------------- /tests/test_basic.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, tables] 2 | 3 | import easy_sqlite3 4 | import easy_sqlite3/[memfs, logfs] 5 | 6 | proc select_1(arg: int): tuple[value: int] {.importdb: "SELECT $arg".} 7 | proc select_2(arg1, arg2: int): tuple[value: int, value2: int] {.importdb: "SELECT $arg1, $arg2".} 8 | 9 | proc insert_data(name: string, value: int) {.importdb: """ 10 | INSERT INTO mydata(name, value) VALUES ($name, $value); 11 | """.} 12 | 13 | iterator iterate_data(): tuple[name: string, value: int] {.importdb: """ 14 | SELECT name, value FROM mydata; 15 | """.} = discard 16 | 17 | proc count_data(): tuple[count: int] {.importdb: "SELECT count(*) FROM mydata".} 18 | 19 | test "simple": 20 | var db = initDatabase(":memory:") 21 | check db.select_1(1) == (value: 1) 22 | 23 | test "multiple": 24 | var db = initDatabase(":memory:") 25 | check db.select_2(1, 2) == (value: 1, value2: 2) 26 | 27 | test "full": 28 | const dataset = { 29 | "A": 0, 30 | "B": 1, 31 | "C": 2, 32 | "D": 3, 33 | }.toTable 34 | var db = initDatabase("test") 35 | db.execM( 36 | "PRAGMA journal_mode=DELETE", 37 | "CREATE TABLE mydata(name TEXT PRIMARY KEY NOT NULL, value INT NOT NULL) WITHOUT ROWID;" 38 | ) 39 | db.transaction: 40 | for name, value in dataset: 41 | db.insert_data name, value 42 | db.exec "VACUUM" 43 | for name, value in db.iterate_data(): 44 | check name in dataset 45 | check dataset[name] == value 46 | check db.count_data() == (count: dataset.len) -------------------------------------------------------------------------------- /tests/test_catch.nim: -------------------------------------------------------------------------------- 1 | import easy_sqlite3 2 | import std/[unittest, tables] 3 | 4 | proc createTable() {.importdb: "CREATE TABLE data(id INTEGER PRIMARY KEY, value INTEGER)".} 5 | proc insertData(id, value: int): int {.importdb: "INSERT INTO data(id, value) VALUES ($id, $value)".} 6 | proc getData(id: int): tuple[value: int] {.importdb: "SELECT value FROM data WHERE id = $id".} 7 | iterator listData(): tuple[id: int, value: int] {.importdb: "SELECT id, value FROM data".} 8 | = discard 9 | 10 | suite "catch": 11 | setup: 12 | var db = initDatabase(":memory:") 13 | db.createTable() 14 | check db.insertData(1, 1) == 1 15 | check db.insertData(2, 4) == 2 16 | test "insert 1, 2 (should failed)": 17 | try: 18 | discard db.insertData(1, 2) 19 | fail() 20 | except CatchableError: 21 | check getCurrentExceptionMsg() == "UNIQUE constraint failed: data.id" 22 | test "get data": 23 | check db.getData(1) == (value: 1) 24 | test "list data": 25 | const dataset = { 26 | 1: 1, 27 | 2: 4 28 | }.toTable 29 | for id, value in db.listData(): 30 | check dataset[id] == value -------------------------------------------------------------------------------- /tests/test_emptystr.nim: -------------------------------------------------------------------------------- 1 | import easy_sqlite3 2 | import std/[unittest, tables] 3 | 4 | proc getEmptyString(): tuple[value: string] {.importdb: "SELECT ''".} 5 | 6 | test "get empty string": 7 | var db = initDatabase(":memory:") 8 | check db.getEmptyString() == (value: "") -------------------------------------------------------------------------------- /tests/test_option.nim: -------------------------------------------------------------------------------- 1 | import std/unittest 2 | import std/options 3 | 4 | import easy_sqlite3 5 | 6 | proc returnOptionNone(): tuple[col: Option[int]] {.importdb: "SELECT NULL;".} 7 | proc returnOptionSome(): tuple[col: Option[int]] {.importdb: "SELECT 1;".} 8 | 9 | proc takeOption(col: Option[int]): tuple[val: int, is_null: bool] {.importdb: "SELECT COALESCE($col, 0), $col IS NULL".} 10 | 11 | iterator options(a: Option[int], b: Option[int], c: Option[int]): tuple[val: Option[int]] {.importdb: "VALUES ($a+1), ($b+1), ($c+1)".} = discard 12 | 13 | suite "option": 14 | setup: 15 | var db = initDatabase(":memory:") 16 | 17 | test "return option none": 18 | let col = db.returnOptionNone().col 19 | check col.is_none() 20 | 21 | test "return option some": 22 | let col = db.returnOptionSome().col 23 | check col.is_some() 24 | check col.get == 1 25 | 26 | test "take option none": 27 | let res = db.takeOption(none(int)) 28 | check res.is_null 29 | check res.val == 0 30 | 31 | test "take option some": 32 | let res = db.takeOption(some(1)) 33 | check not res.is_null 34 | check res.val == 1 35 | 36 | test "iterator options": 37 | var res: seq[Option[int]] = @[] 38 | for row in db.options(some(1), none(int), some(2)): 39 | res.add(row.val) 40 | check res == @[some(2), none(int), some(3)] 41 | -------------------------------------------------------------------------------- /tests/test_thread.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, tables, random, os, times, strformat] 2 | 3 | import easy_sqlite3 4 | import easy_sqlite3/memfs 5 | 6 | const useMemFs = not defined(defaultMemoryDB) 7 | 8 | when useMemFs: 9 | template retry(body: untyped) = body 10 | else: 11 | enableSharedCache() 12 | var failedCount = 0 13 | template retry(body: untyped) = 14 | var failed = 0 15 | while true: 16 | try: 17 | body 18 | break 19 | except CatchableError: 20 | failed.inc 21 | if failed > 0: 22 | failedCount.atomicInc(failed) 23 | 24 | proc create_table() {.importdb: """ 25 | CREATE TABLE store(key INTEGER PRIMARY KEY, value INT NOT NULL); 26 | """.} 27 | 28 | proc insert_data(value: int) {.importdb: """ 29 | INSERT INTO store(value) VALUES ($value) 30 | """.} 31 | 32 | proc count_items(): tuple[count: int] {.importdb: "SELECT count(*) FROM store".} 33 | 34 | proc connectDatabase(): Database = 35 | when useMemFs: 36 | initDatabase("file:memdb1", {so_readwrite, so_create, so_uri}) 37 | else: 38 | initDatabase("file:memdb1?mode=memory&cache=shared", {so_readwrite, so_create, so_uri}) 39 | 40 | test "threads": 41 | if defined(macosx): 42 | skip() 43 | else: 44 | var gdb = connectDatabase() 45 | gdb.create_table() 46 | gdb.exec "VACUUM" 47 | 48 | const COUNT = 1000000 49 | const GROUP = 100 50 | 51 | proc worker_fn() {.thread.} = 52 | echo "thread start" 53 | var tdb = connectDatabase() 54 | var r = initRand(42) 55 | for _ in 0..<(COUNT div GROUP): 56 | retry: 57 | tdb.transactionImmediate: 58 | for _ in 0.. 0: 79 | when useMemFs: 80 | echo fmt"{curr - init:>6.1f}s: {c:>7}" 81 | else: 82 | echo fmt"{curr - init:>6.1f}s: {c:>7} failures: {failedCount}" 83 | prev = curr - diff 84 | if c == COUNT: 85 | break 86 | 87 | when useMemFs: 88 | echo fmt"time: {cpuTime() - init:>9.4f}s" 89 | else: 90 | echo fmt"time: {cpuTime() - init:>9.4f}s failures: {failedCount}" 91 | 92 | worker.joinThread() -------------------------------------------------------------------------------- /tests/test_type.nim: -------------------------------------------------------------------------------- 1 | import std/unittest 2 | 3 | import easy_sqlite3 4 | 5 | type 6 | User = tuple[id: int, username: string, email: string] 7 | 8 | proc createUsersTable() {.importdb: "create table users(id INTEGER PRIMARY KEY, username TEXT NOT NULL, email TEXT NOT NULL)".} 9 | proc insertUser(username, email: string) {.importdb: "insert into users(username, email) values ($username, $email)".} 10 | proc selectUserById(id: int): User {.importdb: "select * from users where id = $id".} 11 | iterator iterateUsers(): User {.importdb: "select * from users".} = discard 12 | 13 | suite "type decl": 14 | setup: 15 | var db = initDatabase(":memory:") 16 | db.createUsersTable() 17 | db.insertUser("user", "user@example.com") 18 | db.insertUser("abc", "abc@example.com") 19 | test "typed query": 20 | check db.selectUserById(1) == (id: 1, username: "user", email: "user@example.com") 21 | test "typed iterator": 22 | for id, username, email in db.iterateUsers(): 23 | check email == (username & "@example.com") --------------------------------------------------------------------------------