tiny_sqlite

    Dark Mode
Search:
Group by:

Opening a database connection.

A database connection is opened by calling the openDatabase procedure with the path to the database file as an argument. If the file doesn't exist, it will be created. An in-memory database can be created by using the special path ":memory:" as an argument. Once the database connection is no longer needed close must be called to prevent memory leaks.

let db = openDatabase("path/to/file.db")
# ... (do something with `db`)
db.close()

Executing SQL

The exec procedure can be used to execute a single SQL statement. The execScript procedure is used to execute several statements, but it doesn't support parameter substitution.

db.execScript("""
    CREATE TABLE Person(
        name TEXT,
        age INTEGER
    );
    
    CREATE TABLE Log(
        message TEXT
    );
""")

db.exec("""
    INSERT INTO Person(name, age)
    VALUES(?, ?);
""", "John Doe", 37)

Reading data

Four different procedures for reading data are available:

  • all: procedure returning all result rows
  • iterate: iterator yielding each result row one by one
  • one: procedure returning the first result row, or none if no result row exists
  • value: procedure returning the first column of the first result row, or none if no result row exists

Note that the procedures one and value returns the result wrapped in an Option. See the standard library options module for documentation on how to deal with Option values. For convenience the tiny_sqlite module exports the options.get, options.isSome, and options.isNone procedures so the options module doesn't need to be explicitly imported for typical usage.

for row in db.iterate("SELECT name, age FROM Person"):
    # The 'row' variable is of type ResultRow.
    # The column values can be accesed by both index and column name:
    echo row[0].strVal      # Prints the name
    echo row["name"].strVal # Prints the name
    echo row[1].intVal      # Prints the age
    # Above we're using the raw DbValue's directly. Instead, we can unpack the
    # DbValue using the fromDbValue procedure:
    echo fromDbValue(row[0], string) # Prints the name
    echo fromDbValue(row[1], int)    # Prints the age
    # Alternatively, the entire row can be unpacked at once:
    let (name, age) = row.unpack((string, int))
    # Unpacking the value is preferable as it makes it possible to handle
    # bools, enums, distinct types, nullable types and more. For example, nullable
    # types are handled using Option[T]:
    echo fromDbValue(row[0], Option[string]) # Will work even if the db value is NULL

# Example of reading a single value. In this case, 'value' will be of type `Option[DbValue]`.
let value = db.one("SELECT age FROM Person WHERE name = ?", "John Doe")
if value.isSome:
    echo fromDbValue(value.get, int) # Prints age of John Doe

Inserting data in bulk

The exec procedure works fine for inserting single rows, but it gets awkward when inserting many rows. For this purpose the execMany procedure can be used instead. It executes the same SQL repeatedly, but with different parameters each time.

let parameters = @[toDbValues("Person 1", 17), toDbValues("Person 2", 55)]
# Will insert two rows
db.execMany("""
    INSERT INTO Person(name, age)
    VALUES(?, ?);
""", parameters)

Transactions

The procedures that can execute multiple SQL statements (execScript and execMany) are wrapped in a transaction by tiny_sqlite. Transactions can also be controlled manually by using one of these two options:

db.transaction:
    # Anything inside here is executed inside a transaction which
    # will be rolled back in case of an error
    db.exec("DELETE FROM Person")
    db.exec("""INSERT INTO Person(name, age) VALUES("Jane Doe", 35)""")
  • Option 2: using the exec procedure manually
db.exec("BEGIN")
try:
    db.exec("DELETE FROM Person")
    db.exec("""INSERT INTO Person(name, age) VALUES("Jane Doe", 35)""")
    db.exec("COMMIT")
except:
    db.exec("ROLLBACK")

Prepared statements

All the procedures for executing SQL described above create and execute prepared statements internally. In addition to those procedures, tiny_sqlite also offers an API for preparing SQL statements explicitly. Prepared statements are created with the stmt procedure, and the same procedures for executing SQL that are available directly on the connection object are also available for the prepared statement:

let stmt = db.stmt("INSERT INTO Person(name, age) VALUES (?, ?)")
stmt.exec("John Doe", 21)
# Once the statement is no longer needed it must be finalized
# to prevent memory leaks.
stmt.finalize()

There are performance benefits of reusing prepared statements, since the preparation only needs to be done once. However, tiny_sqlite keeps an internal cache of prepared statements, so it's typically not necesarry to manage prepared statements manually. If you prefer if tiny_sqlite doesn't perform this caching, you can disable it by setting the cacheSize parameter when opening the database:

let db = openDatabase(":memory:", cacheSize = 0)

Supported types

For a type to be supported when using unpacking and parameter substitution the procedures toDbValue and fromDbValue must be implemented for the type. Below is table describing which types are supported by default and to which SQLite type they are mapped to:

Nim typeSQLite type
Ordinal

INTEGER

SomeFloat

REAL

string

TEXT

seq[byte]

BLOB

Option[T]

NULL if value is none(T), otherwise the type that T would use

This can be extended by implementing toDdValue and fromDbValue for other types on your own. Below is an example how support for times.Time can be added:

import times

proc toDbValue(t: Time): DbValue =
    DbValue(kind: sqliteInteger, intVal: toUnix(t))

proc fromDbValue(value: DbValue, T: typedesc[Time]): Time =
    fromUnix(value.intval)

Types

DbConn = distinct DbConnImpl
Encapsulates a database connection.
DbMode = enum
  dbRead, dbReadWrite
DbValue = object
  case kind*: DbValueKind
  of sqliteInteger:
      intVal*: int64

  of sqliteReal:
      floatVal*: float64

  of sqliteText:
      strVal*: string

  of sqliteBlob:
      blobVal*: seq[byte]

  of sqliteNull:
      nil

  
Can represent any value in a SQLite database.
DbValueKind = enum
  sqliteNull, sqliteInteger, sqliteReal, sqliteText, sqliteBlob
Enum of all possible value types in a SQLite database.
ResultRow = object
  values: seq[DbValue]
  columns: seq[string]
SqliteError = object of CatchableError
Raised when whenever a database related error occurs. Errors are typically a result of API misuse, e.g trying to close an already closed database connection.
SqlStatement = distinct SqlStatementImpl
A prepared SQL statement.

Procs

proc `$`(dbVal: DbValue): string {....raises: [], tags: [].}
proc `==`(a, b: DbValue): bool {....raises: [], tags: [].}
Returns true if a and b represents the same value.
proc `[]`(row: ResultRow; column: string): DbValue {....raises: [], tags: [].}
Access a column in the result row based on column name. The column name must be unambiguous.
proc `[]`(row: ResultRow; idx: Natural): DbValue {....raises: [], tags: [].}
Access a column in the result row based on index.
proc all(db: DbConn; sql: string; params: varargs[DbValue, toDbValue]): seq[
    ResultRow] {....raises: [SqliteError], tags: [].}
Executes sql, which must be a single SQL statement, and returns all result rows.
proc all(statement: SqlStatement; params: varargs[DbValue, toDbValue]): seq[
    ResultRow] {....raises: [SqliteError, Exception], tags: [RootEffect].}
Executes statement and returns all result rows.
proc changes(db: DbConn): int32 {....raises: [], tags: [].}

Get the number of changes triggered by the most recent INSERT, UPDATE or DELETE statement.

For more information, refer to the SQLite documentation (https://www.sqlite.org/c3ref/changes.html).

proc close(db: DbConn) {....raises: [Exception, SqliteError], tags: [RootEffect].}
Closes the database connection. This should be called once the connection will no longer be used to avoid leaking memory. Closing an already closed database is a harmless no-op.
proc columns(row: ResultRow): seq[string] {....raises: [], tags: [].}
Returns all column names in the result row.
proc exec(db: DbConn; sql: string; params: varargs[DbValue, toDbValue]) {.
    ...raises: [SqliteError], tags: [].}
Executes sql, which must be a single SQL statement.

Example:

let db = openDatabase(":memory:")
db.exec("CREATE TABLE Person(name, age)")
db.exec("INSERT INTO Person(name, age) VALUES(?, ?)",
    "John Doe", 23)
proc exec(statement: SqlStatement; params: varargs[DbValue, toDbValue]) {.
    ...raises: [SqliteError], tags: [].}
Executes statement with params as parameters.
proc execMany(db: DbConn; sql: string; params: seq[seq[DbValue]]) {.
    ...raises: [Exception, SqliteError, SqliteError, Exception], tags: [RootEffect].}
Executes sql, which must be a single SQL statement, repeatedly using each element of params as parameters. The statements are executed inside a transaction.
proc execMany(statement: SqlStatement; params: seq[seq[DbValue]]) {.
    ...raises: [SqliteError, SqliteError, Exception], tags: [].}
Executes statement repeatedly using each element of params as parameters. The statements are executed inside a transaction.
proc execScript(db: DbConn; sql: string) {.
    ...raises: [Exception, SqliteError, SqliteError, Exception], tags: [RootEffect].}
Executes sql, which can consist of multiple SQL statements. The statements are executed inside a transaction.
proc finalize(statement: SqlStatement): void {....raises: [], tags: [].}
Finalize the statement. This needs to be called once the statement is no longer used to prevent memory leaks. Finalizing an already finalized statement is a harmless no-op.
proc fromDbValue(value: DbValue; T: typedesc[DbValue]): T:type
Special overload that simply return value. The purpose of this overload is to do partial unpacking. For example, if the type of one column in a result row is unknown, the DbValue type can be kept just for that column.
for row in db.iterate("SELECT name, extra FROM Person"):
    # Type of 'extra' is unknown, so we don't unpack it.
    # The 'extra' variable will be of type 'DbValue'
    let (name, extra) = row.unpack((string, DbValue))
proc fromDbValue(value: DbValue; T: typedesc[Ordinal]): T:type
proc fromDbValue(value: DbValue; T: typedesc[seq[byte]]): seq[byte]
Convert a DbValue to a sequence of bytes.
proc fromDbValue(value: DbValue; T: typedesc[SomeFloat]): float64
Convert a DbValue to a float.
proc fromDbValue(value: DbValue; T: typedesc[string]): string
Convert a DbValue to a string.
proc fromDbValue[T](value: DbValue; _: typedesc[Option[T]]): Option[T]
Convert a DbValue to an optional value.
proc isAlive(statement: SqlStatement): bool {.noSideEffect, ...raises: [], tags: [].}
Returns true if statement has been initialized and not yet finalized.
proc isInTransaction(db: DbConn): bool {.noSideEffect, ...raises: [], tags: [].}
Returns true if a transaction is currently active.

Example:

let db = openDatabase(":memory:")
doAssert not db.isInTransaction
db.transaction:
    doAssert db.isInTransaction
proc isOpen(db: DbConn): bool {.noSideEffect, inline, ...raises: [], tags: [].}
Returns true if db has been opened and not yet closed.

Example:

var db: DbConn
doAssert not db.isOpen
db = openDatabase(":memory:")
doAssert db.isOpen
db.close()
doAssert not db.isOpen
proc isReadonly(db: DbConn): bool {....raises: [], tags: [].}
Returns true if db is in readonly mode.

Example:

let db = openDatabase(":memory:")
doAssert not db.isReadonly
let db2 = openDatabase(":memory:", dbRead)
doAssert db2.isReadonly
proc lastInsertRowId(db: DbConn): int64 {....raises: [], tags: [].}

Get the row id of the last inserted row. For tables with an integer primary key, the row id will be the primary key.

For more information, refer to the SQLite documentation (https://www.sqlite.org/c3ref/last_insert_rowid.html).

proc len(row: ResultRow): int {....raises: [], tags: [].}
Returns the number of columns in the result row.
proc loadExtension(db: DbConn; path: string) {....raises: [SqliteError], tags: [].}
Load an SQLite extension. Will raise a SqliteError exception if loading fails.
proc one(db: DbConn; sql: string; params: varargs[DbValue, toDbValue]): Option[
    ResultRow] {....raises: [SqliteError], tags: [].}
Executes sql, which must be a single SQL statement, and returns the first result row. Returns none(seq[DbValue]) if the result was empty.
proc one(statement: SqlStatement; params: varargs[DbValue, toDbValue]): Option[
    ResultRow] {....raises: [SqliteError, Exception], tags: [RootEffect].}
Executes statement and returns the first row found. Returns none(seq[DbValue]) if no result was found.
proc openDatabase(path: string; mode = dbReadWrite; cacheSize: Natural = 100): DbConn {.
    ...raises: [SqliteError], tags: [].}

Open a new database connection to a database file. To create an in-memory database the special path ":memory:" can be used. If the database doesn't already exist and mode is dbReadWrite, the database will be created. If the database doesn't exist and mode is dbRead, a SqliteError exception will be raised.

NOTE: To avoid memory leaks, db.close must be called when the database connection is no longer needed.

Example:

let memDb = openDatabase(":memory:")
proc rows(db: DbConn; sql: string; params: varargs[DbValue, toDbValue]): seq[
    seq[DbValue]] {....deprecated: "use \'all\' instead", raises: [SqliteError],
                    tags: [].}
Deprecated: use 'all' instead
proc stmt(db: DbConn; sql: string): SqlStatement {....raises: [SqliteError],
    tags: [].}
Constructs a prepared statement from sql.
proc toDbValue[T: Option](val: T): DbValue
Convert an optional value to a DbValue.
proc toDbValue[T: Ordinal](val: T): DbValue
Convert an ordinal value to a Dbvalue.
proc toDbValue[T: seq[byte]](val: T): DbValue
Convert a sequence of bytes to a DbValue.
proc toDbValue[T: SomeFloat](val: T): DbValue
Convert a float to a DbValue.
proc toDbValue[T: string](val: T): DbValue
Convert a string to a DbValue.
proc toDbValue[T: type(nil)](val: T): DbValue
Convert a nil literal to a DbValue.
proc toDbValues(values: varargs[DbValue, toDbValue]): seq[DbValue] {....raises: [],
    tags: [].}
Convert several values to a sequence of DbValue's.

Example:

doAssert toDbValues("string", 23) == @[toDbValue("string"), toDbValue(23)] 
proc unpack[T: tuple](row: ResultRow; _: typedesc[T]): T
Calls fromDbValue on each element of row and returns it as a tuple.
proc unpack[T: tuple](row: seq[DbValue]; _: typedesc[T]): T {....deprecated.}
Deprecated
proc unsafeHandle(db: DbConn): sqlite.Sqlite3 {.inline, ...raises: [], tags: [].}
Returns the raw SQLite3 database handle. This can be used to interact directly with the SQLite C API with the tiny_sqlite/sqlite_wrapper module. Note that the handle should not be used after db.close has been called as doing so would break memory safety.
proc value(db: DbConn; sql: string; params: varargs[DbValue, toDbValue]): Option[
    DbValue] {....raises: [SqliteError], tags: [].}
Executes sql, which must be a single SQL statement, and returns the first column of the first result row. Returns none(DbValue) if the result was empty.
proc value(statement: SqlStatement; params: varargs[DbValue, toDbValue]): Option[
    DbValue] {....raises: [SqliteError, Exception], tags: [RootEffect].}
Executes statement and returns the first column of the first row found. Returns none(DbValue) if no result was found.
proc values(row: ResultRow): seq[DbValue] {....raises: [], tags: [].}
Returns all column values in the result row.

Iterators

iterator iterate(db: DbConn; sql: string; params: varargs[DbValue, toDbValue]): ResultRow {.
    ...raises: [SqliteError, SqliteError], tags: [].}
Executes sql, which must be a single SQL statement, and yields each result row one by one.
iterator iterate(statement: SqlStatement; params: varargs[DbValue, toDbValue]): ResultRow {.
    ...raises: [SqliteError, Exception], tags: [RootEffect].}
Executes statement and yields each result row one by one.
iterator rows(db: DbConn; sql: string; params: varargs[DbValue, toDbValue]): seq[
    DbValue] {....deprecated: "use \'iterate\' instead", raises: [SqliteError],
               tags: [].}
Deprecated: use 'iterate' instead

Templates

template transaction(db: DbConn; body: untyped)
Starts a transaction and runs body within it. At the end the transaction is commited. If an error is raised by body the transaction is rolled back. Nesting transactions is a no-op.