quma is a small SQL/database library for Nim (target: Nim 2) that maps object-like method calls to SQL script files.
This repository is a port of:
- Python:
ebenefuenf/quma(canonical feature set + docs) - PHP:
duoncode/quma(PDO-based implementation with similar goals)
The upstream repos are vendored (git-ignored) for reference:
data/quma.pydata/quma.php
- Lets you write plain SQL in files (no ORM) and call them like functions.
- Organizes SQL scripts by folders ("namespaces") and supports root-level scripts.
- Supports dynamic SQL via templating (upstream uses Mako in Python; PHP uses
*.sql.phptemplates). - Provides a small query wrapper with convenience helpers like
one,all,first,exists, and lazy execution/caching (conceptually). - Supports multiple database backends: SQLite, PostgreSQL, MySQL/MariaDB.
quma supports multiple database backends with explicit compile-time selection:
| Backend | URI Scheme | Compile Flag |
|---|---|---|
| SQLite | sqlite:///path or sqlite:///:memory: |
-d:qumaSqlite |
| PostgreSQL | postgres://user:pass@host:port/db |
-d:qumaPostgres |
| MySQL/MariaDB | mysql://user:pass@host:port/db or mariadb://... |
-d:qumaMysql |
Backends are opt-in. You must compile with at least one backend flag.
# SQLite only
nim c -d:qumaSqlite myapp.nim
# PostgreSQL only
nim c -d:qumaPostgres myapp.nim
# MySQL only
nim c -d:qumaMysql myapp.nim
# Multiple backends explicitly
nim c -d:qumaSqlite -d:qumaPostgres -d:qumaMysql myapp.nimimport quma
# SQLite
let sqliteDb = initDatabase("sqlite:///:memory:", store)
# PostgreSQL
let pgDb = initDatabase("postgres://user:pass@localhost:5432/mydb", store)
# MySQL
let mysqlDb = initDatabase("mysql://user:pass@localhost:3306/mydb", store)
# MariaDB
let mariadbDb = initDatabase("mariadb://user:pass@localhost:3306/mydb", store)The PostgreSQL and MySQL integration tests use QUMA_-prefixed environment
variables with defaults for connection. When the backend is enabled, tests will
attempt to connect using these defaults; ensure the database services are
running or override the values below.
| Backend | Env Vars (defaults) |
|---|---|
| PostgreSQL | QUMA_PGSQL_HOST=localhost, QUMA_PGSQL_USER=quma, QUMA_PGSQL_PASSWORD=quma, QUMA_PGSQL_DATABASE=quma, QUMA_PGSQL_PORT=5432 |
| MySQL/MariaDB | QUMA_MYSQL_HOST=127.0.0.1, QUMA_MYSQL_USER=quma, QUMA_MYSQL_PASSWORD=quma, QUMA_MYSQL_DATABASE=quma, QUMA_MYSQL_PORT=3306, QUMA_MYSQL_SCHEME=mysql |
Set these variables if your local test database uses different credentials. If you do not want integration tests to run, avoid enabling the corresponding backend flag during compilation.
On macOS, tests/config.nims adds -Wl,-rpath entries for Homebrew installs
so nim r can load libpq and libmysqlclient without manual DYLD_* setup.
If your libraries live elsewhere, set DYLD_LIBRARY_PATH or adjust the paths
in tests/config.nims.
Given a directory with scripts:
sql/
users/
all.sql
remove.sql
get_admin.sql
The upstream libraries expose these as:
cur.users.all()-> runsusers/all.sqlcur.users.remove(id=...)-> runsusers/remove.sqlcur.get_admin()-> runsget_admin.sql
Multiple SQL directories can be registered; later/earlier directories can "shadow" each other to support overrides.
quma supports dynamic SQL generation using a Svelte-like template syntax. Template
files use the .nsql extension, but template syntax is also auto-detected in
regular .sql files.
SELECT * FROM users
WHERE 1=1
{#if filterActive}
AND active = :active
{/if}
{#if role == 'admin'}
AND role = 'admin'
{:else if role != ''}
AND role = :role
{:else}
AND role IS NOT NULL
{/if}
ORDER BY id| Construct | Description |
|---|---|
{#if expr}...{/if} |
Conditional block |
{:else if expr} |
Else-if branch |
{:else} |
Else branch |
{#include "path"} |
Include another file |
Template conditions use a simple expression language:
| Type | Literals | Example |
|---|---|---|
| string | 'single' or "double" |
name == 'Hans' |
| int | digits | count > 10 |
| float | digits with . |
price >= 99.99 |
| bool | true, false |
isActive |
| null | null |
value != null |
Operators (lowest to highest precedence):
or- logical orand- logical andnot- logical negation==,!=- equality (any type, both sides must match)<,<=,>,>=- comparison (numeric only)()- grouping
Template variables come from the same named parameters used for SQL binding:
let q = db.cursor.users.search(filterActive = true, role = "admin")Unknown variables or type mismatches raise TemplateError with line/column info.
By default, template syntax is auto-detected in both .sql and .nsql files.
To require explicit .nsql extension for templates, enable strict mode:
let db = initDatabase("sqlite:///app.db", store, strictTemplates = true)In strict mode, template syntax in .sql files raises TemplateError.
Use {#include "path"} to insert content from another file:
-- main.nsql
SELECT * FROM users
{#include "filters/active.inc.sql"}
{#if role == 'admin'}
{#include "admin/extraFields.nsql"}
{/if}
ORDER BY id| Extension | Description |
|---|---|
.inc.sql |
Include-only SQL snippet (not callable as script) |
.inc.nsql |
Include-only template snippet |
.sql, .nsql |
Regular scripts (can also be included) |
Files with .inc.sql or .inc.nsql extensions are excluded from script
discovery, so they cannot be called directly via the cursor API. This lets you
organize reusable SQL fragments without polluting the namespace.
Include paths are resolved in this order:
- Relative to the current script's directory
- SQL roots in order (first match wins)
If the path has no extension, quma tries: .inc.nsql, .inc.sql, .nsql, .sql
sql/
users/
all.nsql # callable as db.cursor.users.all()
filters.inc.sql # NOT callable, include-only
includes/
pagination.inc.sql # NOT callable (directory has no scripts)
Circular includes are detected at runtime and raise TemplateError with the
include chain for debugging.
Downstream applications can embed SQL at compile time and overlay local filesystem overrides for development.
import quma
const embedded = embedSqlDir("sql")
let devStore = initFsScriptStore(["sql"])
let store = initOverlayScriptStore(devStore, embedded)
let db = initDatabase("sqlite:///:memory:", store)embedSqlDirexpects a path relative to the calling file.initOverlayScriptStorechecks the primary store first, then falls back to the embedded scripts.
- Install dependencies:
nimble install(oratlas installif you prefer Atlas) - Run tests (sqlite default):
nimble testornim test - Enable more backends in tests:
QUMA_TEST_BACKENDS=all nimble testornim test all - Pass backend args to test task:
nim test mysql,nim test sqlite mysql, ornim test all - Non-mysql/postgres tests require the sqlite flag; use
nim test sqlite mysqlto run core tests plus MySQL. - Integration tests run whenever their backend flag is enabled; ensure Postgres/MySQL are running before
nim test all.
MIT (same as upstream).