From d04d3a8536201beb614f0d15e5c1fadc34747d8e Mon Sep 17 00:00:00 2001 From: niinivaa Date: Tue, 16 Nov 2021 17:44:23 +0200 Subject: [PATCH] 3.0.0 --- README.md | 33 +- docs/nimdoc.out.css | 120 ++- docs/{index.html => sqliteral.html} | 1130 ++++++++++++++------------- sqliteral.nimble | 4 +- src/sqliteral.nim | 246 ++---- 5 files changed, 779 insertions(+), 754 deletions(-) rename docs/{index.html => sqliteral.html} (60%) diff --git a/README.md b/README.md index 38bf1d2..b105fb1 100644 --- a/README.md +++ b/README.md @@ -13,36 +13,29 @@ http://olliNiinivaara.github.io/SQLiteral/ `nimble install sqliteral` -## 2.0.2 Release notes (2021-09-20) -* logs every statement preparation only once (instead of from every thread) -* more realistic example +## 3.0.0 Release notes (2021-11-16) +* new implementation for Text values (see various toDb -procs) +* nimble file typo fix ## Example ```nim import sqliteral -const Schema = """ - CREATE TABLE IF NOT EXISTS Example(count INTEGER NOT NULL); - INSERT INTO Example(rowid, count) VALUES(1, 1) - ON CONFLICT(rowid) DO UPDATE SET count=count+100 - """ -const CountColumn = 0 +const Schema = "CREATE TABLE IF NOT EXISTS Example(string TEXT NOT NULL)" type SqlStatements = enum - Increment = "UPDATE Example SET count=count+1 WHERE rowid = ?" - Select = "SELECT count FROM Example WHERE rowid = ?" - SelectAll = "SELECT count FROM Example" + Upsert = """INSERT INTO Example(rowid, string) VALUES(1, ?) + ON CONFLICT(rowid) DO UPDATE SET string = ?""" + SelectAll = "SELECT string FROM Example" var db: SQLiteral -proc operate() = - echo "count=",db.getTheInt(Select, 1) - db.transaction: db.exec(Increment, 1) - for row in db.rows(SelectAll): echo row.getInt(CountColumn) - -when not defined(release): - db.setLogger(proc(db: SQLiteral, msg: string, code: int) = echo msg) +proc operate(i: string) = + let view: DbValue = i.toDb(3, 7) # zero-copy view into string + db.transaction: db.exec(Upsert, view, view) + for row in db.rows(SelectAll): echo row.getCString(0) db.openDatabase("example.db", Schema) db.prepareStatements(SqlStatements) -operate() +var input = "012INPUT89" +operate(input) db.close() ``` diff --git a/docs/nimdoc.out.css b/docs/nimdoc.out.css index 2d9533c..4abea9c 100644 --- a/docs/nimdoc.out.css +++ b/docs/nimdoc.out.css @@ -14,6 +14,9 @@ Modified by Boyd Greenfield and narimiran --primary-background: #fff; --secondary-background: ghostwhite; --third-background: #e8e8e8; + --info-background: #50c050; + --warning-background: #c0a000; + --error-background: #e04040; --border: #dde; --text: #222; --anchor: #07b; @@ -32,6 +35,8 @@ Modified by Boyd Greenfield and narimiran --escapeSequence: #c4891b; --number: #252dbe; --literal: #a4255b; + --program: #6060c0; + --option: #508000; --raw-data: #a4255b; } @@ -39,6 +44,9 @@ Modified by Boyd Greenfield and narimiran --primary-background: #171921; --secondary-background: #1e202a; --third-background: #2b2e3b; + --info-background: #008000; + --warning-background: #807000; + --error-background: #c03000; --border: #0e1014; --text: #fff; --anchor: #8be9fd; @@ -57,6 +65,8 @@ Modified by Boyd Greenfield and narimiran --escapeSequence: #bd93f9; --number: #bd93f9; --literal: #f1fa8c; + --program: #9090c0; + --option: #90b010; --raw-data: #8be9fd; } @@ -224,6 +234,12 @@ select:focus { } /* Docgen styles */ + +:target { + border: 2px solid #B5651D; + border-style: dotted; +} + /* Links */ a { color: var(--anchor); @@ -378,6 +394,7 @@ h2 { margin-top: 2em; } h2.subtitle { + margin-top: 0em; text-align: center; } h3 { @@ -491,6 +508,45 @@ hr { border: 0; border-top: 1px solid #aaa; } +hr.footnote { + width: 25%; + border-top: 0.15em solid #999; + margin-bottom: 0.15em; + margin-top: 0.15em; +} +div.footnote-group { + margin-left: 1em; } +div.footnote-label { + display: inline-block; + min-width: 1.7em; +} + +div.option-list { + border: 0.1em solid var(--border); +} +div.option-list-item { + padding-left: 12em; + padding-right: 0; + padding-bottom: 0.3em; + padding-top: 0.3em; +} +div.odd { + background-color: var(--secondary-background); +} +div.option-list-label { + margin-left: -11.5em; + margin-right: 0em; + min-width: 11.5em; + display: inline-block; + vertical-align: top; +} +div.option-list-description { + width: calc(100% - 1em); + padding-left: 1em; + padding-right: 0; + display: inline-block; +} + blockquote { font-size: 0.9em; font-style: italic; @@ -499,7 +555,7 @@ blockquote { border-left: 5px solid #bbc; } -.pre { +.pre, span.tok { font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace; font-weight: 500; font-size: 0.85em; @@ -510,6 +566,12 @@ blockquote { border-radius: 4px; } +span.tok { + border: 1px solid #808080; + padding-bottom: 0.1em; + margin-right: 0.2em; +} + pre { font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace; color: var(--text); @@ -561,6 +623,7 @@ table.line-nums-table { .line-nums-table td.blob-line-nums pre { color: #b0b0b0; -webkit-filter: opacity(75%); + filter: opacity(75%); text-align: right; border-color: transparent; background-color: transparent; @@ -609,6 +672,34 @@ table.borderless td, table.borderless th { The right padding separates the table cells. */ padding: 0 0.5em 0 0 !important; } +.admonition { + padding: 0.3em; + background-color: var(--secondary-background); + border-left: 0.4em solid #7f7f84; + margin-bottom: 0.5em; + -webkit-box-shadow: 0 5px 8px -6px rgba(0,0,0,.2); + -moz-box-shadow: 0 5px 8px -6px rgba(0,0,0,.2); + box-shadow: 0 5px 8px -6px rgba(0,0,0,.2); +} +.admonition-info { + border-color: var(--info-background); +} +.admonition-info-text { + color: var(--info-background); +} +.admonition-warning { + border-color: var(--warning-background); +} +.admonition-warning-text { + color: var(--warning-background); +} +.admonition-error { + border-color: var(--error-background); +} +.admonition-error-text { + color: var(--error-background); +} + .first { /* Override more specific margin styles with "! important". */ margin-top: 0 !important; } @@ -768,9 +859,6 @@ span.classifier { span.classifier-delimiter { font-weight: bold; } -span.option { - white-space: nowrap; } - span.problematic { color: #b30000; } @@ -850,6 +938,29 @@ span.Preprocessor { span.Directive { color: #252dbe; } +span.option { + font-weight: bold; + font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace; + color: var(--option); +} + +span.Prompt { + font-weight: bold; + color: red; } + +span.ProgramOutput { + font-weight: bold; + color: #808080; } + +span.program { + font-weight: bold; + color: var(--program); + text-decoration: underline; + text-decoration-color: var(--hint); + text-decoration-thickness: 0.05em; + text-underline-offset: 0.15em; +} + span.Command, span.Rule, span.Hyperlink, span.Label, span.Reference, span.Other { color: var(--other); } @@ -872,6 +983,7 @@ dt pre > span.Operator ~ span.Identifier, dt pre > span.Other ~ span.Identifier background-position: 0 0; background-size: 51px 14px; -webkit-filter: opacity(50%); + filter: opacity(50%); background-repeat: no-repeat; background-image: var(--nim-sprite-base64); margin-bottom: 5px; } diff --git a/docs/index.html b/docs/sqliteral.html similarity index 60% rename from docs/index.html rename to docs/sqliteral.html index 45afe93..29efa5b 100644 --- a/docs/index.html +++ b/docs/sqliteral.html @@ -34,7 +34,6 @@ } } - const toggleSwitch = document.querySelector('.theme-switch input[type="checkbox"]'); function switchTheme(e) { if (e.target.checked) { document.documentElement.setAttribute('data-theme', 'dark'); @@ -45,21 +44,29 @@ } } - toggleSwitch.addEventListener('change', switchTheme, false); + const toggleSwitch = document.querySelector('.theme-switch input[type="checkbox"]'); + if (toggleSwitch !== null) { + toggleSwitch.addEventListener('change', switchTheme, false); + } - const currentTheme = localStorage.getItem('theme') ? localStorage.getItem('theme') : null; + var currentTheme = localStorage.getItem('theme'); + if (!currentTheme && window.matchMedia('(prefers-color-scheme: dark)').matches) { + currentTheme = 'dark'; + } if (currentTheme) { document.documentElement.setAttribute('data-theme', currentTheme); - if (currentTheme === 'dark') { + if (currentTheme === 'dark' && toggleSwitch !== null) { toggleSwitch.checked = true; } } } + +window.addEventListener('DOMContentLoaded', main); - +

sqliteral

@@ -96,7 +103,21 @@

sqliteral

  • Types
  • Consts
  • Procs
  • +

    A high level SQLite API with support for multi-threading, prepared statements, proper typing, zero-copy data paths, debugging, JSON, optimizing, backups, and more. -

    Example (using JSON extensions)

    #nim c --threads:on example
    -#(works if sqlite compiled with JSON extensions)
    -
    -# nim c -d:danger --gc:orc -d:staticSqlite --experimental:views --threads:on example
    -# (works if sqlite3.c in path)
    +

    Example (using JSON extensions)

    # nim r --threads:on example
     
     import sqliteral, threadpool
     from strutils import find
    @@ -450,7 +466,6 @@ 

    const Schema = "CREATE TABLE IF NOT EXISTS Example(name TEXT NOT NULL, jsondata TEXT NOT NULL)" - type SqlStatements = enum Insert = """INSERT INTO Example (name, jsondata) VALUES (json_extract(?, '$.name'), json_extract(?, '$.data'))""" @@ -462,7 +477,6 @@

    var db: SQLiteral prepared {.threadvar.}: bool - rowresult {.threadvar.}: string ready: int when not defined(release): db.setLogger(proc(db: SQLiteral, msg: string, code: int) = echo msg) @@ -471,27 +485,22 @@

    {.gcsafe.}: if not prepared: db.prepareStatements(SqlStatements) - rowresult = newstring(1000) prepared = true for row in db.rows(Select): - rowresult.setLen(0) - rowresult.add($getThreadId()) - rowresult.add(": ") - rowresult.add($row.getString(0)) - rowresult.add('\n') - stdout.write(rowresult) + stdout.write(row.getCString(0)) + stdout.write('\n') discard ready.atomicInc proc run() = db.openDatabase("ex.db", Schema) defer: db.close() db.prepareStatements(SqlStatements) - let body = asText(httprequest, httprequest.find("BODY:") + 5, httprequest.len - 1) + let body = httprequest.toDb(httprequest.find("BODY:") + 5, httprequest.len - 1) if not db.json_valid(body): quit(0) echo "inserting 10000 rows..." db.transaction: - for i in 1 .. 10000: discard db.insert(Insert, body, body) + for i in 1 .. 10000: discard db.insert(Insert, body, body) echo "10000 rows inserted. Press <Enter> to select all in 4 threads..." discard stdin.readChar() @@ -501,12 +510,34 @@

    echo "Selected 4 * ", db.getTheInt(Count), " = " & $(4 * db.getTheInt(Count)) & " rows." run()

    -

    Compiling with sqlite3.c

    First, sqlite3.c amalgamation must be on compiler search path.
    You can extract it from a zip available at https://www.sqlite.org/download.html.
    Then, -d:staticSqlitecompiler option must be used.

    For your convenience, -d:staticSqlite triggers some useful SQLite compiler options, consult sqliteral source code or about() proc for details. These can be turned off with -d:disableSqliteoptions option.

    +

    Compiling with sqlite3.c

    First, sqlite3.c amalgamation must be on compiler search path.
    You can extract it from a zip available at https://www.sqlite.org/download.html.
    Then, -d:staticSqlitecompiler option must be used.

    For your convenience, -d:staticSqlite triggers some useful SQLite compiler options, consult sqliteral source code or about() proc for details. These can be turned off with -d:disableSqliteoptions option.

    Types

    - +
    +
    DbValue = object
    +  case kind*: DbValueKind
    +  of sqliteInteger:
    +      intVal*: int64
    +
    +  of sqliteReal:
    +      floatVal*: float64
    +
    +  of sqliteText:
    +      textVal*: tuple[chararray: cstring, len: int32]
    +
    +  of sqliteBlob:
    +      blobVal*: seq[byte]
    +
    +  
    +
    + +

    Represents a value in a SQLite database.
    https://www.sqlite.org/datatype3.html
    NULL values are not possible to avoid the billion-dollar mistake.

    + +
    +
    +
    SQLError = ref object of CatchableError
       rescode*: int
     
    @@ -515,7 +546,8 @@

    Types

    https://www.sqlite.org/rescode.html - +
    +
    SQLiteral = object
       sqlite*: PSqlite3
       dbname*: string
    @@ -530,9 +562,9 @@ 

    Types

    laststatementindex: int internalstatements: array[MaxThreadSize, array[3 + 1, PStmt]] transactionlock: Lock - loggerproc: proc (sqliteral: SQLiteral; statement: string; errorcode: int) {...}{. - gcsafe, raises: [].} - oncommitproc: proc (sqliteral: SQLiteral) {...}{.gcsafe, raises: [].} + loggerproc: proc (sqliteral: SQLiteral; statement: string; errorcode: int) {. + ...gcsafe, raises: [].} + oncommitproc: proc (sqliteral: SQLiteral) {....gcsafe, raises: [].} maxparamloggedlen: int Transaction: PStmt Commit: PStmt @@ -543,624 +575,630 @@

    Types

    - -
    Text = tuple[data: ptr UncheckedArray[char], len: int32]
    -
    - -

    To avoid copying strings, SQLiteral offers Text as a zero-copy view to a slice of existing string

    -

    Compile with --experimental:views to use the new language-supported zero-copy views instead (when they work). Text will be removed when views become officially supported in Nim...

    -

    Example:

    -
    var buffer = """{"sentence": "Call me Ishmael"}"""
    -let value = asText(buffer, buffer.find(" \"")+2, buffer.find("\"}")-1)
    -assert value.equals("Call me Ishmael")
    -db.exec(Update, value, rowid)
    - -
    - -
    DbValue = object
    -  case kind*: DbValueKind
    -  of sqliteInteger:
    -      intVal*: int64
    -
    -  of sqliteReal:
    -      floatVal*: float64
    -
    -  of sqliteTextview:
    -      textVal*: void         ## not yer in use!
    -    
    -  of sqliteTextuncheckedarray:
    -      uncheckedarraytextVal*: Text
    -
    -  of sqliteBlob:
    -      blobVal*: seq[byte]
    -
    -  
    -
    - -

    Represents a value in a SQLite database.
    https://www.sqlite.org/datatype3.html
    NULL values are not possible to avoid the billion-dollar mistake.

    - -
    +

    Consts

    - -
    SQLiteralVersion = "2.0.2"
    +
    +
    MaxStatements = 100
    - +Compile time define pragma that limits amount of prepared statements
    - +
    +
    MaxThreadSize = 32
    - -
    MaxStatements = 100
    +
    +
    +
    SQLiteralVersion = "3.0.0"
    -Compile time define pragma that limits amount of prepared statements +
    +

    Procs

    - -
    proc asText(fromstring: string; first: int; last: int): Text {...}{.raises: [],
    -    tags: [].}
    +
    +
    proc `$`[T: DbValue](val: T): string {.inline.}
    -Creates a zero-copy view to a substring of existing string +
    - -
    proc asText(fromstring: ptr string; first: int; last: int): Text {...}{.raises: [],
    -    tags: [].}
    +
    +
    +
    proc about(db: SQLiteral) {....raises: [SQLError, IOError, OSError],
    +                            tags: [RootEffect, ReadIOEffect].}
    -Creates a zero-copy view to a substring of existing string +Echoes some info about the database.
    - -
    proc equals(text: Text; str: string): bool {...}{.inline, raises: [], tags: [].}
    +
    +
    +
    proc bindParams(sql: PStmt; params: varargs[DbValue]): int {.inline, ...raises: [],
    +    tags: [].}
    - -
    proc `$`(text: Text): string {...}{.raises: [], tags: [].}
    +
    +
    +
    proc cancelBackup(db: var SQLiteral; backupdb: PSqlite3;
    +                  backuphandle: PSqlite3_Backup) {....raises: [SQLError],
    +    tags: [RootEffect].}
    - +Cancels an ongoing backup process.
    - -
    proc len(text: Text): int {...}{.inline, raises: [], tags: [].}
    +
    +
    +
    proc close(db: var SQLiteral) {....raises: [SQLError], tags: [RootEffect].}
    - +Closes the database.
    - -
    proc substr(text: Text; start: int; last: int): string {...}{.raises: [], tags: [].}
    +
    +
    +
    proc columnExists(db: SQLiteral; table: string; column: string): bool {.
    +    ...raises: [Exception, SQLError], tags: [RootEffect].}
    - +Returns true if given column exists in given table
    - -
    proc toDb(val: string): DbValue {...}{.inline, raises: [], tags: [].}
    +
    +
    +
    proc doLog(db: SQLiteral; statement: string; params: varargs[DbValue, toDb]) {.
    +    inline, ...raises: [], tags: [RootEffect].}
    - -
    proc toDb(val: Text): DbValue {...}{.inline, raises: [SQLError], tags: [].}
    +
    +
    +
    proc exec(db: SQLiteral; pstatement: PStmt; params: varargs[DbValue, toDb]) {.
    +    inline, ...raises: [SQLError], tags: [RootEffect].}
    - +Executes given prepared statement
    - -
    proc toDb[T: Ordinal](val: T): DbValue {...}{.inline.}
    +
    +
    +
    proc exec(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]) {.
    +    inline.}
    - +Executes given statement
    - -
    proc toDb[T: SomeFloat](val: T): DbValue {...}{.inline.}
    +
    +
    +
    proc exes(db: SQLiteral; sql: string) {....raises: [SQLError], tags: [RootEffect].}
    - +

    Prepares, executes and finalizes given semicolon-separated sql statements.
    For security and performance reasons, this proc should be used with caution.

    - -
    proc toDb(val: seq[byte]): DbValue {...}{.inline, raises: [], tags: [].}
    +
    +
    +
    proc getAsStrings(prepared: PStmt): seq[string] {....raises: [], tags: [].}
    - +Returns values of all result columns as a sequence of strings. This proc is mainly useful for debugging purposes.
    - -
    proc toDb[T: DbValue](val: T): DbValue {...}{.inline.}
    +
    +
    +
    proc getCString(prepared: PStmt; col: int32 = 0): cstring {.inline, ...raises: [],
    +    tags: [].}
    - +

    Returns value of TEXT -type column at given column index as cstring.
    Zero-copy, but result is not available after cursor movement or statement reset.

    - -
    proc `$`[T: DbValue](val: T): string {...}{.inline.}
    +
    +
    +
    proc getFloat(prepared: PStmt; col: int32 = 0): float64 {.inline, ...raises: [],
    +    tags: [].}
    - +Returns value of REAL -type column at given column index
    - -
    proc bindParams(sql: PStmt; params: varargs[DbValue]): int {...}{.inline, raises: [],
    -    tags: [].}
    +
    +
    +
    proc getInt(prepared: PStmt; col: int32 = 0): int64 {.inline, ...raises: [],
    +    tags: [].}
    - +Returns value of INTEGER -type column at given column index
    - -
    proc doLog(db: SQLiteral; statement: string; params: varargs[DbValue, toDb]) {...}{.
    -    inline, raises: [], tags: [RootEffect].}
    +
    +
    +
    proc getLastInsertRowid(db: SQLiteral): int64 {.inline, ...raises: [], tags: [].}
    - +https://www.sqlite.org/c3ref/last_insert_rowid.html
    - -
    proc getInt(prepared: PStmt; col: int32 = 0): int64 {...}{.inline, raises: [],
    -    tags: [].}
    +
    +
    +
    proc getSeq(prepared: PStmt; col: int32 = 0): seq[byte] {.inline, ...raises: [],
    +    tags: [].}
    -Returns value of INTEGER -type column at given column index +Returns value of BLOB -type column at given column index
    - -
    proc getString(prepared: PStmt; col: int32 = 0): string {...}{.inline, raises: [],
    -    tags: [].}
    +
    +
    +
    proc getStatus(db: SQLiteral; status: int; resethighest = false): (int, int) {.
    +    ...raises: [SQLError], tags: [RootEffect].}
    -Returns value of TEXT -type column at given column index as string +

    Retrieves queried status info. See https://www.sqlite.org/c3ref/c_dbstatus_options.html

    +

    Example:

    +
    const SQLITE_DBSTATUS_CACHE_USED = 1
    +echo "current cache usage: ", db.getStatus(SQLITE_DBSTATUS_CACHE_USED)[0]
    - -
    proc getCString(prepared: PStmt; col: int32 = 0): cstring {...}{.inline, raises: [],
    -    tags: [].}
    +
    +
    +
    proc getString(prepared: PStmt; col: int32 = 0): string {.inline, ...raises: [],
    +    tags: [].}
    -

    Returns value of TEXT -type column at given column index as cstring.
    Zero-copy, but result is not available after cursor movement or statement reset.

    +Returns value of TEXT -type column at given column index as string
    - -
    proc getFloat(prepared: PStmt; col: int32 = 0): float64 {...}{.inline, raises: [],
    -    tags: [].}
    +
    +
    +
    proc getTheInt(db: SQLiteral; s: string): int64 {.inline,
    +    ...raises: [SQLError, SQLError], tags: [RootEffect].}
    -Returns value of REAL -type column at given column index +| Dynamically prepares, executes and finalizes given query and returns value of INTEGER -type column at column index 0 of first result row.

    If query does not return any rows, returns -2147483647 (low(int32) + 1).
    For security and performance reasons, this proc should be used with caution.

    - -
    proc getSeq(prepared: PStmt; col: int32 = 0): seq[byte] {...}{.inline, raises: [],
    -    tags: [].}
    +
    +
    +
    proc getTheInt(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): int64 {.
    +    inline.}
    -Returns value of BLOB -type column at given column index +

    Executes query and returns value of INTEGER -type column at column index 0 of first result row.
    If query does not return any rows, returns -2147483647 (low(int32) + 1).
    Automatically resets the statement.

    - -
    proc getAsStrings(prepared: PStmt): seq[string] {...}{.raises: [], tags: [].}
    +
    +
    +
    proc getTheString(db: SQLiteral; s: string): string {.inline,
    +    ...raises: [SQLError, SQLError], tags: [RootEffect].}
    -Returns values of all result columns as a sequence of strings. This proc is mainly useful for debugging purposes. +| Dynamically prepares, executes and finalizes given query and returns value of TEXT -type column at column index 0 of first result row.

    If query does not return any rows, returns empty string.
    For security and performance reasons, this proc should be used with caution.

    - -
    proc prepareSql(db: SQLiteral; sql: cstring): PStmt {...}{.inline,
    -    raises: [SQLError], tags: [RootEffect].}
    +
    +
    +
    proc getTheString(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): string {.
    +    inline.}
    -Prepares a cstring into an executable statement +

    Executes query and returns value of TEXT -type column at column index 0 of first result row.
    If query does not return any rows, returns empty string.
    Automatically resets the statement.

    - -
    proc getTheInt(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): int64 {...}{.
    -    inline.}
    +
    +
    +
    proc initBackup(db: var SQLiteral; backupfilename: string): tuple[
    +    backupdb: PSqlite3, backuphandle: PSqlite3_Backup] {.
    +    ...raises: [SQLError, SQLError], tags: [RootEffect].}
    -

    Executes query and returns value of INTEGER -type column at column index 0 of first result row.
    If query does not return any rows, returns -2147483647 (low(int32) + 1).
    Automatically resets the statement.

    +

    Initializes backup processing, returning variables to use with stepBackup proc.

    +

    Note that close will fail with SQLITE_BUSY if there's an unfinished backup process going on.

    +
    - -
    proc getTheInt(db: SQLiteral; s: string): int64 {...}{.inline,
    -    raises: [SQLError, SQLError], tags: [RootEffect].}
    +
    +
    +
    proc insert(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): int64 {.
    +    inline.}
    -| Dynamically prepares, executes and finalizes given query and returns value of INTEGER -type column at column index 0 of first result row.

    If query does not return any rows, returns -2147483647 (low(int32) + 1).
    For security and performance reasons, this proc should be used with caution.

    +

    Executes given statement and, if succesful, returns db.getLastinsertRowid().
    If not succesful, returns -2147483647 (low(int32) + 1).

    - -
    proc getTheString(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): string {...}{.
    -    inline.}
    +
    +
    +
    proc isIntransaction(db: SQLiteral): bool {.inline, ...raises: [], tags: [].}
    -

    Executes query and returns value of TEXT -type column at column index 0 of first result row.
    If query does not return any rows, returns empty string.
    Automatically resets the statement.

    +
    - -
    proc getTheString(db: SQLiteral; s: string): string {...}{.inline,
    -    raises: [SQLError, SQLError], tags: [RootEffect].}
    +
    +
    +
    proc json_extract(db: var SQLiteral; path: string;
    +                  jsonstring: varargs[DbValue, toDb]): string {.
    +    ...raises: [SQLError, Exception], tags: [RootEffect].}
    -| Dynamically prepares, executes and finalizes given query and returns value of TEXT -type column at column index 0 of first result row.

    If query does not return any rows, returns empty string.
    For security and performance reasons, this proc should be used with caution.

    +
    - -
    proc getLastInsertRowid(db: SQLiteral): int64 {...}{.inline, raises: [], tags: [].}
    +
    +
    +
    proc json_patch(db: var SQLiteral; patch: string;
    +                jsonstring: varargs[DbValue, toDb]): string {.
    +    ...raises: [SQLError, Exception], tags: [RootEffect].}
    -https://www.sqlite.org/c3ref/last_insert_rowid.html +
    - -
    proc rowExists(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): bool {...}{.
    -    inline.}
    +
    +
    +
    proc json_valid(db: var SQLiteral; jsonstring: varargs[DbValue, toDb]): bool {.
    +    ...raises: [SQLError, Exception], tags: [RootEffect].}
    -Returns true if query returns any rows +
    - -
    proc rowExists(db: SQLiteral; sql: string): bool {...}{.inline, raises: [SQLError],
    -    tags: [RootEffect].}
    +
    +
    +
    proc openDatabase(db: var SQLiteral; dbname: string; schema: string;
    +                  maxKbSize = 0; wal = true) {.inline,
    +    ...raises: [SQLError, Exception], tags: [RootEffect].}
    -

    Returns true if query returns any rows.
    For security reasons, this proc should be used with caution.

    +Open database with a single schema.
    - -
    proc exec(db: SQLiteral; pstatement: PStmt; params: varargs[DbValue, toDb]) {...}{.
    -    inline, raises: [SQLError], tags: [RootEffect].}
    +
    +
    +
    proc openDatabase(db: var SQLiteral; dbname: string; schemas: openArray[string];
    +                  maxKbSize = 0; wal = true; ignorableschemaerrors: openArray[
    +    string] = @["duplicate column name", "no such column"]) {.
    +    ...raises: [SQLError, Exception], tags: [RootEffect].}
    -Executes given prepared statement +

    Opens an exclusive connection, boots up the database, executes given schemas and prepares given statements.

    +

    If dbname is not a path, current working directory will be used.

    +

    If wal = true, database is opened in WAL mode with NORMAL synchronous setting.
    If wal = false, database is opened in PERSIST mode with FULL synchronous setting.
    https://www.sqlite.org/wal.html
    https://www.sqlite.org/pragma.html#pragma_synchronous

    If maxKbSize == 0, database size is limited only by OS or hardware with possibly severe consequences.

    +

    ignorableschemaerrors is a list of error message snippets for sql errors that are to be ignored. If a clause may error, it must be given in a separate schema as its unique clause. If * is given as ignorable error, it means that all errors will be ignored.

    +

    Note that by default, "duplicate column name" (ADD COLUMN) and "no such column" (DROP COLUMN) -errors will be ignored. Example below.

    +
    const
    +  Schema1 = "CREATE TABLE IF NOT EXISTS Example(data TEXT NOT NULL)"
    +  Schema2 = "this is to be ignored"
    +  Schema3 = """ALTER TABLE Example ADD COLUMN newcolumn TEXT NOT NULL DEFAULT """""
     
    -
    - -
    proc exec(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]) {...}{.
    -    inline.}
    -
    +var db1, db2, db3: SQLiteral -Executes given statement +proc logger(db: SQLiteral, msg: string, code: int) = echo msg +db1.setLogger(logger); db2.setLogger(logger); db3.setLogger(logger) + +db1.openDatabase("example1.db", [Schema1, Schema3]); db1.close() +db2.openDatabase("example2.db", [Schema1, Schema2], + ignorableschemaerrors = ["""this": syntax error"""]); db2.close() +db3.openDatabase("example3.db", [Schema1, Schema2, Schema3], + ignorableschemaerrors = ["*"]); db3.close()
    - -
    proc exes(db: SQLiteral; sql: string) {...}{.raises: [SQLError], tags: [RootEffect].}
    +
    +
    +
    proc optimize(db: var SQLiteral; pagesize = -1; walautocheckpoint = -1) {.
    +    ...raises: [], tags: [RootEffect].}
    -

    Prepares, executes and finalizes given semicolon-separated sql statements.
    For security and performance reasons, this proc should be used with caution.

    +

    Vacuums and optimizes the database.

    +

    This proc should be run just before closing, when no other thread accesses the database.

    +

    In addition, database read/write performance ratio may be adjusted with parameters:
    https://sqlite.org/pragma.html#pragma_page_size
    https://www.sqlite.org/pragma.html#pragma_wal_checkpoint

    - -
    proc insert(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): int64 {...}{.
    -    inline.}
    +
    +
    +
    proc prepareSql(db: SQLiteral; sql: cstring): PStmt {.inline,
    +    ...raises: [SQLError], tags: [RootEffect].}
    -

    Executes given statement and, if succesful, returns db.getLastinsertRowid().
    If not succesful, returns -2147483647 (low(int32) + 1).

    +Prepares a cstring into an executable statement
    - -
    proc update(db: SQLiteral; sql: string; column: string; newvalue: DbValue;
    -            where: DbValue) {...}{.raises: [Exception, SQLError, SQLError],
    -                              tags: [RootEffect].}
    +
    +
    +
    proc prepareStatements(db: var SQLiteral; Statements: typedesc[enum])
    -

    Dynamically constructs, prepares, executes and finalizes given update query.
    Update must target one column and WHERE -clause must contain one value.
    For security and performance reasons, this proc should be used with caution.

    +Prepares the statements given as enum parameter. Call this exactly once from every thread that is going to access the database. Main example shows how this "exactly once"-requirement can be achieved with a boolean threadvar.
    - -
    proc columnExists(db: SQLiteral; table: string; column: string): bool {...}{.
    -    raises: [Exception, SQLError], tags: [RootEffect].}
    +
    +
    +
    proc rowExists(db: SQLiteral; sql: string): bool {.inline, ...raises: [SQLError],
    +    tags: [RootEffect].}
    -Returns true if given column exists in given table +

    Returns true if query returns any rows.
    For security reasons, this proc should be used with caution.

    - -
    proc isIntransaction(db: SQLiteral): bool {...}{.inline, raises: [], tags: [].}
    +
    +
    +
    proc rowExists(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): bool {.
    +    inline.}
    - +Returns true if query returns any rows
    - +
    +
    proc setLogger(db: var SQLiteral; logger: proc (sqliteral: SQLiteral;
    -    statement: string; code: int) {...}{.gcsafe, raises: [].}; paramtruncat = 50) {...}{.
    -    raises: [], tags: [].}
    + statement: string; code: int) {....gcsafe, raises: [].}; paramtruncat = 50) {. + ...raises: [], tags: [].}

    Set callback procedure to gather all executed statements with their parameters.

    If code > 0, log concerns sqlite error with error code in question.

    -

    If code == -1, log may be of minor interest (originating from exes or statement preparation).

    +

    If code == -1, log may be of minor interest (originating from exes or statement preparation).

    Paramtruncat parameter limits the maximum log length of parameters so that long inputs won't clutter logs. Value < 1 disables truncation.

    You can use the same logger for multiple sqliterals, the caller is also given as parameter.

    - -
    proc setOnCommitCallback(db: var SQLiteral; oncommit: proc (sqliteral: SQLiteral) {...}{.
    -    gcsafe, raises: [].}) {...}{.raises: [], tags: [].}
    +
    +
    +
    proc setOnCommitCallback(db: var SQLiteral; oncommit: proc (sqliteral: SQLiteral) {.
    +    ...gcsafe, raises: [].}) {....raises: [], tags: [].}
    Set callback procedure that is triggered inside transaction proc, when commit to database has been executed.
    - -
    proc json_extract(db: var SQLiteral; path: string;
    -                  jsonstring: varargs[DbValue, toDb]): string {...}{.
    -    raises: [SQLError, Exception], tags: [RootEffect].}
    +
    +
    +
    proc setReadonly(db: var SQLiteral; readonly: bool) {....raises: [SQLError],
    +    tags: [RootEffect].}
    +When in readonly mode:
    1. All transactions will be silently discarded
    2. +
    3. Journal mode is changed to PERSIST in order to be able to change locking mode
    4. +
    5. Locking mode is changed from EXCLUSIVE to NORMAL, allowing other connections access the database
    6. +
    +

    Setting readonly fails with exception "cannot change into wal mode from within a transaction" when a statement is being executed, for example a result of a select is being iterated.

    +

    inreadonlymode property tells current mode.

    - -
    proc json_patch(db: var SQLiteral; patch: string;
    -                jsonstring: varargs[DbValue, toDb]): string {...}{.
    -    raises: [SQLError, Exception], tags: [RootEffect].}
    +
    +
    +
    proc stepBackup(db: var SQLiteral; backupdb: PSqlite3;
    +                backuphandle: PSqlite3_Backup; pagesperportion = 5.int32): int {.
    +    ...raises: [SQLError], tags: [RootEffect].}
    - +

    Backs up a portion of the database pages (default: 5) to a destination initialized with initBackup.

    +

    Returns percentage of progress; 100% means that backup has been finished.

    +

    The idea (check example 2) is to put the thread to sleep between portions so that other operations can proceed concurrently.

    +

    Example:

    +
    from os import sleep
    +let (backupdb , backuphandle) = db.initBackup("./backup.db")
    +var progress: int
    +while progress < 100:
    +  sleep(250)
    +  progress = db.stepBackup(backupdb, backuphandle)
    - -
    proc json_valid(db: var SQLiteral; jsonstring: varargs[DbValue, toDb]): bool {...}{.
    -    raises: [SQLError, Exception], tags: [RootEffect].}
    +
    +
    +
    proc threadi(db: SQLiteral): int {....raises: [], tags: [].}
    - -
    proc openDatabase(db: var SQLiteral; dbname: string; schemas: openArray[string];
    -                  maxKbSize = 0; wal = true; ignorableschemaerrors: openArray[
    -    string] = @["duplicate column name", "no such column"]) {...}{.
    -    raises: [SQLError, Exception], tags: [RootEffect].}
    +
    +
    +
    proc toDb(val: cstring; first, last: int): DbValue {.inline, ...raises: [],
    +    tags: [].}
    -

    Opens an exclusive connection, boots up the database, executes given schemas and prepares given statements.

    -

    If dbname is not a path, current working directory will be used.

    -

    If wal = true, database is opened in WAL mode with NORMAL synchronous setting.
    If wal = false, database is opened in PERSIST mode with FULL synchronous setting.
    https://www.sqlite.org/wal.html
    https://www.sqlite.org/pragma.html#pragma_synchronous

    If maxKbSize == 0, database size is limited only by OS or hardware with possibly severe consequences.

    -

    ignorableschemaerrors is a list of error message snippets for sql errors that are to be ignored. If a clause may error, it must be given in a separate schema as its unique clause. If * is given as ignorable error, it means that all errors will be ignored.

    -

    Note that by default, "duplicate column name" (ADD COLUMN) and "no such column" (DROP COLUMN) -errors will be ignored. Example below.

    -
    const
    -  Schema1 = "CREATE TABLE IF NOT EXISTS Example(data TEXT NOT NULL)"
    -  Schema2 = "this is to be ignored"
    -  Schema3 = """ALTER TABLE Example ADD COLUMN newcolumn TEXT NOT NULL DEFAULT """""
    -
    -var db1, db2, db3: SQLiteral
     
    -proc logger(db: SQLiteral, msg: string, code: int) = echo msg
    -db1.setLogger(logger); db2.setLogger(logger); db3.setLogger(logger)
    -
    -db1.openDatabase("example1.db", [Schema1, Schema3]); db1.close()
    -db2.openDatabase("example2.db", [Schema1, Schema2],
    - ignorableschemaerrors = ["""this": syntax error"""]); db2.close()
    -db3.openDatabase("example3.db", [Schema1, Schema2, Schema3],
    - ignorableschemaerrors = ["*"]); db3.close()
    - -
    proc openDatabase(db: var SQLiteral; dbname: string; schema: string;
    -                  maxKbSize = 0; wal = true) {...}{.inline,
    -    raises: [SQLError, Exception], tags: [RootEffect].}
    +
    +
    +
    proc toDb(val: cstring; len = -1): DbValue {.inline, ...raises: [], tags: [].}
    -Open database with a single schema. - -
    - -
    proc prepareStatements(db: var SQLiteral; Statements: typedesc[enum])
    -
    -Prepares the statements given as enum parameter. Call this exactly once from every thread that is going to access the database. Main example shows how this "exactly once"-requirement can be achieved with a boolean threadvar.
    - -
    proc setReadonly(db: var SQLiteral; readonly: bool) {...}{.raises: [SQLError],
    -    tags: [RootEffect].}
    +
    +
    +
    proc toDb(val: openArray[char]; len = -1): DbValue {.inline, ...raises: [],
    +    tags: [].}
    -

    When in readonly mode:

    -

    1) All transactions will be silently discarded

    -

    2) Journal mode is changed to PERSIST in order to be able to change locking mode

    -

    3) Locking mode is changed from EXCLUSIVE to NORMAL, allowing other connections access the database

    -

    Setting readonly fails with exception "cannot change into wal mode from within a transaction" when a statement is being executed, for example a result of a select is being iterated.

    -

    inreadonlymode property tells current mode.

    - -
    proc optimize(db: var SQLiteral; pagesize = -1; walautocheckpoint = -1) {...}{.
    -    raises: [], tags: [RootEffect].}
    +
    +
    +
    proc toDb(val: seq[byte]): DbValue {.inline, ...raises: [], tags: [].}
    -

    Vacuums and optimizes the database.

    -

    This proc should be run just before closing, when no other thread accesses the database.

    -

    In addition, database read/write performance ratio may be adjusted with parameters:
    https://sqlite.org/pragma.html#pragma_page_size
    https://www.sqlite.org/pragma.html#pragma_wal_checkpoint

    +
    - -
    proc initBackup(db: var SQLiteral; backupfilename: string): tuple[
    -    backupdb: PSqlite3, backuphandle: PSqlite3_Backup] {...}{.
    -    raises: [SQLError, SQLError], tags: [RootEffect].}
    +
    +
    +
    proc toDb(val: string; first, last: int): DbValue {.inline, ...raises: [], tags: [].}
    -

    Initializes backup processing, returning variables to use with stepBackup proc.

    -

    Note that close will fail with SQLITE_BUSY if there's an unfinished backup process going on.

    - -
    proc stepBackup(db: var SQLiteral; backupdb: PSqlite3;
    -                backuphandle: PSqlite3_Backup; pagesperportion = 5.int32): int {...}{.
    -    raises: [SQLError], tags: [RootEffect].}
    +
    +
    +
    proc toDb(val: string; len = -1): DbValue {.inline, ...raises: [], tags: [].}
    -

    Backs up a portion of the database pages (default: 5) to a destination initialized with initBackup.

    -

    Returns percentage of progress; 100% means that backup has been finished.

    -

    The idea (check example 2) is to put the thread to sleep between portions so that other operations can proceed concurrently.

    -

    Example:

    -
    from os import sleep
    -let (backupdb , backuphandle) = db.initBackup("./backup.db")
    -var progress: int
    -while progress < 100:
    -  sleep(250)
    -  progress = db.stepBackup(backupdb, backuphandle)
    +
    - -
    proc cancelBackup(db: var SQLiteral; backupdb: PSqlite3;
    -                  backuphandle: PSqlite3_Backup) {...}{.raises: [SQLError],
    -    tags: [RootEffect].}
    +
    +
    +
    proc toDb[T: DbValue](val: T): DbValue {.inline.}
    -Cancels an ongoing backup process. +
    - -
    proc about(db: SQLiteral) {...}{.raises: [SQLError, IOError, OSError],
    -                            tags: [RootEffect, ReadIOEffect].}
    +
    +
    +
    proc toDb[T: Ordinal](val: T): DbValue {.inline.}
    -Echoes some info about the database. +
    - -
    proc getStatus(db: SQLiteral; status: int; resethighest = false): (int, int) {...}{.
    -    raises: [SQLError], tags: [RootEffect].}
    +
    +
    +
    proc toDb[T: SomeFloat](val: T): DbValue {.inline.}
    -

    Retrieves queried status info. See https://www.sqlite.org/c3ref/c_dbstatus_options.html

    -

    Example:

    -
    const SQLITE_DBSTATUS_CACHE_USED = 1
    -echo "current cache usage: ", db.getStatus(SQLITE_DBSTATUS_CACHE_USED)[0]
    +
    - -
    proc close(db: var SQLiteral) {...}{.raises: [SQLError], tags: [RootEffect].}
    +
    +
    +
    proc update(db: SQLiteral; sql: string; column: string; newvalue: DbValue;
    +            where: DbValue) {....raises: [Exception, SQLError, SQLError],
    +                              tags: [RootEffect].}
    -Closes the database. +

    Dynamically constructs, prepares, executes and finalizes given update query.
    Update must target one column and WHERE -clause must contain one value.
    For security and performance reasons, this proc should be used with caution.

    +

    Iterators

    - -
    iterator rows(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): PStmt
    +
    +
    iterator json_tree(db: SQLiteral; jsonstring: varargs[DbValue, toDb]): PStmt {.
    +    ...raises: [SQLError], tags: [RootEffect].}
    -Iterates over the query results +
    - -
    iterator rows(db: SQLiteral; pstatement: PStmt; params: varargs[DbValue, toDb]): PStmt {...}{.
    -    raises: [SQLError], tags: [RootEffect].}
    +
    +
    +
    iterator rows(db: SQLiteral; pstatement: PStmt; params: varargs[DbValue, toDb]): PStmt {.
    +    ...raises: [SQLError], tags: [RootEffect].}
    Iterates over the query results
    - -
    iterator json_tree(db: SQLiteral; jsonstring: varargs[DbValue, toDb]): PStmt {...}{.
    -    raises: [SQLError], tags: [RootEffect].}
    +
    +
    +
    iterator rows(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb]): PStmt
    - +Iterates over the query results
    +

    Templates

    - -
    template threadi(db: SQLiteral): int
    -
    - - - -
    - +
    template checkRc(db: SQLiteral; resultcode: int)
    -

    Raises SQLError if resultcode notin [SQLITE_OK, SQLITE_ROW, SQLITE_DONE]
    https://www.sqlite.org/rescode.html

    +

    Raises SQLError if resultcode notin [SQLITE_OK, SQLITE_ROW, SQLITE_DONE]
    https://www.sqlite.org/rescode.html

    - -
    template withRow(db: SQLiteral; sql: string; row, body: untyped)
    +
    +
    +
    template transaction(db: var SQLiteral; body: untyped)
    -

    Dynamically prepares and finalizes an sql query.
    Name for the resulting prepared statement is given with row parameter.
    The code block will be executed only if query returns a row.
    For security and performance reasons, this proc should be used with caution.

    +

    Every write to database must happen inside some transaction.
    Groups of reads must be wrapped in same transaction if mutual consistency required.
    In WAL mode (the default), independent reads must NOT be wrapped in transaction to allow parallel processing.

    - -
    template withRowOr(db: SQLiteral; sql: string; row, body1, body2: untyped)
    +
    +
    +
    template transactionsDisabled(db: var SQLiteral; body: untyped)
    -

    Dynamically prepares and finalizes an sql query.
    Name for the resulting prepared statement is given with row parameter.
    First block will be executed if query returns a row, otherwise the second block.
    For security and performance reasons, this proc should be used with caution.

    Example:

    -
    db.withRowOr("SELECT (1) FROM sqlite_master", rowname):
    -  echo "database has some tables because first column = ", rowname.getInt(0)
    -do:
    -  echo "we have a fresh database"
    +Executes body in between transactions (ie. does not start transaction, but transactions are blocked during this operation).
    - -
    template withRow(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb];
    -                 row, body: untyped) {...}{.dirty.}
    +
    +
    +
    template withRow(db: SQLiteral; sql: string; row, body: untyped)
    -

    Executes given statement.
    Name for the prepared statement is given with row parameter.
    The code block will be executed only if query returns a row.

    +

    Dynamically prepares and finalizes an sql query.
    Name for the resulting prepared statement is given with row parameter.
    The code block will be executed only if query returns a row.
    For security and performance reasons, this proc should be used with caution.

    - -
    template withRowOr(db: SQLiteral; statement: enum;
    -                   params: varargs[DbValue, toDb]; row, body1, body2: untyped)
    +
    +
    +
    template withRow(db: SQLiteral; statement: enum; params: varargs[DbValue, toDb];
    +                 row, body: untyped) {.dirty.}
    -

    Executes given statement.
    Name for the prepared statement is given with row parameter.
    First block will be executed if query returns a row, otherwise the second block.

    +

    Executes given statement.
    Name for the prepared statement is given with row parameter.
    The code block will be executed only if query returns a row.

    - -
    template transaction(db: var SQLiteral; body: untyped)
    +
    +
    +
    template withRowOr(db: SQLiteral; sql: string; row, body1, body2: untyped)
    -

    Every write to database must happen inside some transaction.
    Groups of reads must be wrapped in same transaction if mutual consistency required.
    In WAL mode (the default), independent reads must NOT be wrapped in transaction to allow parallel processing.

    +

    Dynamically prepares and finalizes an sql query.
    Name for the resulting prepared statement is given with row parameter.
    First block will be executed if query returns a row, otherwise the second block.
    For security and performance reasons, this proc should be used with caution.

    Example:

    +
    db.withRowOr("SELECT (1) FROM sqlite_master", rowname):
    +  echo "database has some tables because first column = ", rowname.getInt(0)
    +do:
    +  echo "we have a fresh database"
    - -
    template transactionsDisabled(db: var SQLiteral; body: untyped)
    +
    +
    +
    template withRowOr(db: SQLiteral; statement: enum;
    +                   params: varargs[DbValue, toDb]; row, body1, body2: untyped)
    -Executes body in between transactions (ie. does not start transaction, but transactions are blocked during this operation). +

    Executes given statement.
    Name for the prepared statement is given with row parameter.
    First block will be executed if query returns a row, otherwise the second block.

    +
    @@ -1171,7 +1209,7 @@

    Templates

    diff --git a/sqliteral.nimble b/sqliteral.nimble index a947e24..e8e2ecb 100644 --- a/sqliteral.nimble +++ b/sqliteral.nimble @@ -1,6 +1,6 @@ # Package -version = "2.0.2" +version = "3.0.0" author = "Olli" description = "A high level SQLite API for Nim" license = "MIT" @@ -8,4 +8,4 @@ srcDir = "src" # Dependencies -requires "nim >= 1.4.8 \ No newline at end of file +requires "nim >= 1.6" \ No newline at end of file diff --git a/src/sqliteral.nim b/src/sqliteral.nim index 7aa6d05..6571118 100644 --- a/src/sqliteral.nim +++ b/src/sqliteral.nim @@ -1,4 +1,4 @@ -const SQLiteralVersion* = "2.0.2" +const SQLiteralVersion* = "3.0.0" # (C) Olli Niinivaara, 2020-2021 # MIT Licensed @@ -11,60 +11,49 @@ const SQLiteralVersion* = "2.0.2" ## ## .. code-block:: Nim ## -## # nim c --threads:on example -## # (works if sqlite compiled with JSON extensions) -## -## # nim c -d:danger --gc:orc -d:staticSqlite --experimental:views --threads:on example -## # (works if sqlite3.c in path) +## # nim r --threads:on example ## ## import sqliteral, threadpool ## from strutils import find ## from os import sleep -## +## ## const Schema = "CREATE TABLE IF NOT EXISTS Example(name TEXT NOT NULL, jsondata TEXT NOT NULL)" -## ## ## type SqlStatements = enum ## Insert = """INSERT INTO Example (name, jsondata) ## VALUES (json_extract(?, '$.name'), json_extract(?, '$.data'))""" ## Count = "SELECT count(*) FROM Example" ## Select = "SELECT json_extract(jsondata, '$.array') FROM Example" -## +## ## let httprequest = """header BODY:{"name":"Alice", "data":{"info":"xxx", "array":["a","b","c"]}}""" -## -## var +## +## var ## db: SQLiteral ## prepared {.threadvar.}: bool -## rowresult {.threadvar.}: string ## ready: int -## +## ## when not defined(release): db.setLogger(proc(db: SQLiteral, msg: string, code: int) = echo msg) -## +## ## proc select() = ## {.gcsafe.}: ## if not prepared: ## db.prepareStatements(SqlStatements) -## rowresult = newstring(1000) ## prepared = true ## for row in db.rows(Select): -## rowresult.setLen(0) -## rowresult.add($getThreadId()) -## rowresult.add(": ") -## rowresult.add($row.getString(0)) -## rowresult.add('\n') -## stdout.write(rowresult) +## stdout.write(row.getCString(0)) +## stdout.write('\n') ## discard ready.atomicInc -## +## ## proc run() = ## db.openDatabase("ex.db", Schema) ## defer: db.close() ## db.prepareStatements(SqlStatements) -## let body = asText(httprequest, httprequest.find("BODY:") + 5, httprequest.len - 1) +## let body = httprequest.toDb(httprequest.find("BODY:") + 5, httprequest.len - 1) ## if not db.json_valid(body): quit(0) ## ## echo "inserting 10000 rows..." ## db.transaction: -## for i in 1 .. 10000: discard db.insert(Insert, body, body) +## for i in 1 .. 10000: discard db.insert(Insert, body, body) ## ## echo "10000 rows inserted. Press to select all in 4 threads..." ## discard stdin.readChar() @@ -72,10 +61,9 @@ const SQLiteralVersion* = "2.0.2" ## while (ready < 4): sleep(20) ## stdout.flushFile() ## echo "Selected 4 * ", db.getTheInt(Count), " = " & $(4 * db.getTheInt(Count)) & " rows." -## +## ## run() ## -## ## Compiling with sqlite3.c ## ======================== ## @@ -86,15 +74,8 @@ const SQLiteralVersion* = "2.0.2" ## For your convenience, `-d:staticSqlite` triggers some useful SQLite compiler options, ## consult sqliteral source code or `about()` proc for details. ## These can be turned off with `-d:disableSqliteoptions` option. -## ## - -when compiles((var x = 1; var vx: var int = x)): - const ViewsAvailable = true -else: - const ViewsAvailable = false - when defined(staticSqlite): when compileOption("threads"): {.passL: "-lpthread".} else: {.passC: "-DSQLITE_THREADSAFE=0".} @@ -151,93 +132,22 @@ type DbValueKind = enum sqliteInteger, sqliteReal, - sqliteTextview, - sqliteTextuncheckedarray, + sqliteText, sqliteBlob - Text* = tuple[data: ptr UncheckedArray[char], len: int32] - ## To avoid copying strings, SQLiteral offers Text as a zero-copy view to a slice of existing string - ## - ## Compile with --experimental:views to use the new language-supported zero-copy views instead (when they work). - ## Text will be removed when views become officially supported in Nim... - ## - ## **Example:** - ## - ## .. code-block:: Nim - ## - ## var buffer = """{"sentence": "Call me Ishmael"}""" - ## let value = asText(buffer, buffer.find(" \"")+2, buffer.find("\"}")-1) - ## assert value.equals("Call me Ishmael") - ## db.exec(Update, value, rowid) - -when ViewsAvailable: - type - DbValue* = object - ## | Represents a value in a SQLite database. - ## | https://www.sqlite.org/datatype3.html - ## | NULL values are not possible to avoid the billion-dollar mistake. - case kind*: DbValueKind - of sqliteInteger: - intVal*: int64 - of sqliteReal: - floatVal*: float64 - of sqliteTextview: - textVal*: openArray[char] - of sqliteTextuncheckedarray: - uncheckedarraytextVal*: Text - ## --experimental:views also offers openArray[char]. - ## (nimdoc does not support documenting these experimental features) - of sqliteBlob: - blobVal*: seq[byte] # TODO: openArray[byte] -else: - type - DbValue* = object - ## | Represents a value in a SQLite database. - ## | https://www.sqlite.org/datatype3.html - ## | NULL values are not possible to avoid the billion-dollar mistake. - case kind*: DbValueKind - of sqliteInteger: - intVal*: int64 - of sqliteReal: - floatVal*: float64 - of sqliteTextview: - textVal*: void ## not yer in use! - of sqliteTextuncheckedarray: - uncheckedarraytextVal*: Text - of sqliteBlob: - blobVal*: seq[byte] - -#---------------------------------------------------------- - -proc asText*(fromstring: string, first: int, last: int): Text = - ## Creates a zero-copy view to a substring of existing string - doAssert(last < fromstring.len) - doAssert(last < int32.high) - (cast[ptr UncheckedArray[char]](fromstring[first].unsafeAddr), (last - first + 1).int32) - -proc asText*(fromstring: ptr string, first: int, last: int): Text = - ## Creates a zero-copy view to a substring of existing string - doAssert(first < fromstring[].len) - doAssert(last < fromstring[].len) - doAssert(last < int32.high) - (cast[ptr UncheckedArray[char]](fromstring[first].unsafeAddr), (last - first + 1).int32) - -proc equals*(text: Text, str: string): bool {.inline.} = - if text.len != str.len: return false - for i in 0 ..< text.len: - if text.data[i] != str[i]: return false - return true - -proc `$`*(text: Text): string = - for i in 0 ..< text.len: result.add(text.data[i]) - -proc len*(text: Text): int {.inline.} = text.len - -proc substr*(text: Text, start: int, last: int): string = - for i in start ..< last: result.add(text.data[i]) - -var emptytext = "X" -var emptystart = cast[ptr UncheckedArray[char]](addr emptytext[0]) + DbValue* = object + ## | Represents a value in a SQLite database. + ## | https://www.sqlite.org/datatype3.html + ## | NULL values are not possible to avoid the billion-dollar mistake. + case kind*: DbValueKind + of sqliteInteger: + intVal*: int64 + of sqliteReal: + floatVal*: float64 + of sqliteText: + textVal*: tuple[chararray: cstring, len: int32] + of sqliteBlob: + blobVal*: seq[byte] # TODO: openArray[byte] #---------------------------------------------------------- @@ -259,25 +169,24 @@ template checkRc*(db: SQLiteral, resultcode: int) = raise SQLError(msg: db.dbname & " " & errormsg, rescode: resultcode) -when ViewsAvailable: - proc toDb*(val: string): DbValue {.inline.} = - DbValue(kind: sqliteTextview, textVal: toOpenArray(val, 0, val.high)) - - proc toDb*(val: openArray[char]): DbValue {.inline.} = - #DbValue(kind: sqliteText, textVal: val) - # nim error: incompatible types when assigning to type ‘tyOpenArray__g7UvpSI7wiag75QHJKQ1sQ’ {aka ‘struct ’} from type ‘NIM_CHAR *’ {aka ‘char *’} - DbValue(kind: sqliteTextuncheckedarray, uncheckedarraytextVal: (cast[ptr UncheckedArray[char]](val[0].unsafeAddr), val.len.int32)) -else: - proc toDb*(val: string): DbValue {.inline.} = - if val.len == 0: - DbValue(kind: sqliteTextuncheckedarray, uncheckedarraytextVal: (emptystart , 0.int32)) - else: - DbValue(kind: sqliteTextuncheckedarray, uncheckedarraytextVal: (cast[ptr UncheckedArray[char]](val[0].unsafeAddr), val.len.int32)) +proc toDb*(val: cstring, len = -1): DbValue {.inline.} = + if len == -1: DbValue(kind: sqliteText, textVal: (val, int32(val.len()))) + else: DbValue(kind: sqliteText, textVal: (val, int32(len))) + +proc toDb*(val: cstring, first, last: int): DbValue {.inline.} = + DbValue(kind: sqliteText, textVal: (cast[cstring](unsafeAddr(val[first])), int32(1 + last - first))) + +proc toDb*(val: string, len = -1): DbValue {.inline.} = + if len == -1: DbValue(kind: sqliteText, textVal: (cstring(val), int32(val.len()))) + else: DbValue(kind: sqliteText, textVal: (cstring(val), int32(len))) + +proc toDb*(val: string, first, last: int): DbValue {.inline.} = + DbValue(kind: sqliteText, textVal: (cast[cstring](unsafeAddr(val[first])), int32(1 + last - first))) +proc toDb*(val: openArray[char], len = -1): DbValue {.inline.} = + if len == -1: DbValue(kind: sqliteText, textVal: (cstring(unsafeAddr val[0]), int32(val.len()))) + else: DbValue(kind: sqliteText, textVal: (cstring(unsafeAddr val[0]), int32(len))) -proc toDb*(val: Text): DbValue {.inline.} = - if val[1] < 0 or val[1] > high(int32): raise SQLError(msg: "Text weird len: " & $val[1]) - DbValue(kind: sqliteTextuncheckedarray, uncheckedarraytextVal: val) proc toDb*[T: Ordinal](val: T): DbValue {.inline.} = DbValue(kind: sqliteInteger, intVal: val.int64) @@ -288,52 +197,25 @@ proc toDb*(val: seq[byte]): DbValue {.inline.} = DbValue(kind: sqliteBlob, blobV proc toDb*[T: DbValue](val: T): DbValue {.inline.} = val -when ViewsAvailable: - proc `$`*[T: DbValue](val: T): string {.inline.} = - case val.kind - of sqliteInteger: $val.intval - of sqliteReal: $val.floatVal - of sqliteTextview: $val.textVal - of sqliteTextuncheckedarray: $val.uncheckedarraytextVal - of sqliteBlob: cast[string](val.blobVal) -else: - proc `$`*[T: DbValue](val: T): string {.inline.} = - case val.kind - of sqliteInteger: $val.intval - of sqliteReal: $val.floatVal - of sqliteTextview: "{.fatal: not available}" - of sqliteTextuncheckedarray: $val.uncheckedarraytextVal - of sqliteBlob: cast[string](val.blobVal) - - -when ViewsAvailable: - proc bindParams*(sql: PStmt, params: varargs[DbValue]): int {.inline.} = - var idx = 1.int32 - for value in params: - result = - case value.kind - of sqliteInteger: bind_int64(sql, idx, value.intval) - of sqliteReal: bind_double(sql, idx, value.floatVal) - of sqliteTextview: bind_text(sql, idx, cstring(unsafeAddr value.textVal[0]), value.textVal.len().int32, SQLITE_STATIC) - of sqliteTextuncheckedarray: bind_text(sql, idx, value.uncheckedarraytextVal[0], value.uncheckedarraytextVal[1].int32, SQLITE_STATIC) - of sqliteBlob: bind_blob(sql, idx.int32, cast[string](value.blobVal).cstring, - value.blobVal.len.int32 , SQLITE_STATIC) - if result != SQLITE_OK: return - idx.inc -else: - proc bindParams*(sql: PStmt, params: varargs[DbValue]): int {.inline.} = - var idx = 1.int32 - for value in params: - result = - case value.kind - of sqliteInteger: bind_int64(sql, idx, value.intval) - of sqliteReal: bind_double(sql, idx, value.floatVal) - of sqliteTextview: -1 #{.fatal: "not available"} - of sqliteTextuncheckedarray: bind_text(sql, idx, value.uncheckedarraytextVal[0], value.uncheckedarraytextVal[1].int32, SQLITE_STATIC) - of sqliteBlob: bind_blob(sql, idx.int32, cast[string](value.blobVal).cstring, - value.blobVal.len.int32 , SQLITE_STATIC) - if result != SQLITE_OK: return - idx.inc +proc `$`*[T: DbValue](val: T): string {.inline.} = + case val.kind + of sqliteInteger: $val.intval + of sqliteReal: $val.floatVal + of sqliteText: ($val.textVal.chararray)[0 .. val.textVal.len - 1] + of sqliteBlob: cast[string](val.blobVal) + + +proc bindParams*(sql: PStmt, params: varargs[DbValue]): int {.inline.} = + var idx = 1.int32 + for value in params: + result = + case value.kind + of sqliteInteger: bind_int64(sql, idx, value.intval) + of sqliteReal: bind_double(sql, idx, value.floatVal) + of sqliteText: bind_text(sql, idx, value.textVal.chararray, value.textVal.len, SQLITE_STATIC) + of sqliteBlob: bind_blob(sql, idx.int32, cast[string](value.blobVal).cstring, value.blobVal.len.int32, SQLITE_STATIC) + if result != SQLITE_OK: return + idx.inc template log() = @@ -350,7 +232,7 @@ template log() = else: ($params[replacement]).substr(0, db.maxparamloggedlen - 1) logstring = logstring[0 .. position-1] & param & logstring.substr(position+1) replacement += 1 - if (logstring.find('?') != -1): logstring = $params.len & " is not enough params for: " & $statement + if (logstring.find('?') != -1): logstring &= " (some params missing)" db.loggerproc(db, logstring, 0)