Synsemadocsv0.6.xENES

Standard library / primitives

SQL, Mongo & Redis

All databases open with db_open and are gated by require db(scope). Three families: SQL (SQLite/Postgres/MySQL), documents (MongoDB, mongo_), key-value (Redis, redis_). All drivers are pure-Rust (single static binary).

sql.syn
-- Doc example: SQL over SQLite in-memory (real, no server). Same API for Postgres/MySQL.
intent: "doc example: SQL"
require db(":memory:")

db_open(":memory:", "memory")
sql_exec("CREATE TABLE demo (n TEXT)")
sql_exec("INSERT INTO demo VALUES (?)", ["it works"])
print("sql demo → " + (sql("SELECT n FROM demo")[0])["n"])
db_close()

-- The message of an error, or nothing when the call did not fail.
task error_of(f)
    try
        f()
    recover e
        give e
    give nothing

test "create, parameterized insert, query"
    db_open(":memory:", "memory")
    sql_exec("CREATE TABLE items (name TEXT, price REAL)")
    sql_exec("INSERT INTO items VALUES (?, ?)", ["Laptop", 999])
    sql_exec("INSERT INTO items VALUES (?, ?)", ["Mouse", 25])
    let rows be sql("SELECT name, price FROM items WHERE price > ? ORDER BY price DESC", [50])
    assert_eq(length(rows), 1)
    assert_eq(name of (rows[0]), "Laptop")
    db_close()

test "sql_exec reports rows_affected and last_id; sql_batch runs one statement per row"
    db_open(":memory:", "memory")
    sql_exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
    assert_eq(sql_exec("INSERT INTO users (name) VALUES (?)", ["Ada"]), {"rows_affected": 1, "last_id": 1})
    assert_eq(sql_batch("INSERT INTO users (name) VALUES (?)", [["Bo"], ["Cy"]]), {"rows_affected": 2})
    assert_eq(sql_tables(), ["users"])
    db_close()

test "RETURNING goes through sql(); on SQLite sql_exec refuses it before running"
    db_open(":memory:", "memory")
    sql_exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
    let e be error_of(() => sql_exec("INSERT INTO users (name) VALUES (?) RETURNING id", ["Ada"]))
    assert(contains(e, "nothing was executed"))
    assert_eq(length(sql("SELECT * FROM users")), 0)
    assert_eq(sql("INSERT INTO users (name) VALUES (?) RETURNING id", ["Ada"]), [{"id": 1}])
    db_close()

test "every ? needs exactly one value"
    db_open(":memory:", "memory")
    let e be error_of(() => sql("SELECT ? AS v"))
    assert(contains(e, "the statement has 1 parameter(s) but 0 value(s) were passed"))
    db_close()

test "maps, lists and integers beyond 64 bits are encoded on purpose"
    db_open(":memory:", "memory")
    sql_exec("CREATE TABLE docs (body TEXT, big TEXT)")
    let doc be {"id": 2 ** 70, "tags": ["a", "b"]}
    sql_exec("INSERT INTO docs VALUES (?, ?)", [json_encode(doc), text(2 ** 70)])
    let row be sql("SELECT body, big FROM docs")[0]
    assert_eq(json_decode(row.body), doc)       -- json_decode keeps the big integer exact
    assert_eq(int(row.big), 2 ** 70)
    db_close()

test "bytes round-trip to a BLOB byte-exactly"
    db_open(":memory:", "memory")
    sql_exec("CREATE TABLE files (data BLOB)")
    sql_exec("INSERT INTO files VALUES (?)", [bytes([0, 255, 7])])
    assert_eq(sql("SELECT data FROM files")[0].data, bytes([0, 255, 7]))
    db_close()

test "readonly refuses writes; db_close(target) closes that connection"
    db_open(":memory:", "readonly")
    assert(contains(error_of(() => sql_exec("CREATE TABLE x (a)")), "readonly"))
    db_close(":memory:")
    assert(contains(error_of(() => sql("SELECT 1")), "No database connection"))

test "SQLite cannot open another file from SQL: ATTACH a file and VACUUM INTO are refused (engine v0.6.45+)"
    db_open(":memory:", "memory")
    assert(contains(error_of(() => sql_exec("ATTACH DATABASE 'other.db' AS o")), "not authorized"))
    assert(contains(error_of(() => sql_exec("ATTACH ? AS o", ["other.db"])), "not authorized"))
    -- v0.6.46+: the message says why and what to use instead
    assert(contains(error_of(() => sql_exec("ATTACH DATABASE 'other.db' AS o")), "open the other database with db_open(<path>), which needs require db(<path>)"))
    assert(contains(error_of(() => sql_exec("VACUUM INTO 'copy.db'")), "authorization denied"))
    sql_exec("ATTACH ':memory:' AS scratch")         -- an in-memory database still attaches
    sql_exec("VACUUM")                               -- a plain VACUUM still runs
    db_close()

test "db_open refuses a file: URI; open the database by its path"
    let e be error_of(() => db_open("file:/etc/x.db"))
    assert(contains(e, "is not a database path"))

SQL (SQLite / Postgres / MySQL)§

require db("./store.db")
db_open("./store.db")                 -- file → SQLite. ":memory:" with mode "memory". postgres:// / mysql://
sql_exec("INSERT INTO users (name) VALUES (?)", ["Ada"])   -- parameterized (safe)
let rows be sql("SELECT * FROM users WHERE id = ?", [1])
give paged("SELECT * FROM users ORDER BY id")              -- pagination for a route handler

Placeholders are always ? (Postgres rewrites to $n internally; MySQL uses ? natively). bytes columns round-trip to BLOB/BYTEA byte-exactly.

All of them (the example's tests run each one on SQLite, except paged, which needs a route):

MongoDB (documents)§

require db("mongodb://localhost/appdb")
db_open("mongodb://host:27017/appdb")
let id be mongo_insert("users", {"name": "Ana", "age": 30})
let adults be mongo_find("users", {"age": {"$gte": 18}}, {"sort": {"age": -1}})

All of them: mongo_find(coll, filter?, opts?) (opts {limit, skip, sort: {f: 1/-1}, fields: {f: 1}}), mongo_find_one(coll, filter?) → doc or nothing, mongo_insert(coll, doc) → _id, mongo_insert_many(coll, docs) → ids, mongo_update(coll, filter, update) → {matched, modified} (update uses operators, {"$set": …}), mongo_delete(coll, filter) → {deleted}, mongo_count(coll, filter?), mongo_aggregate(coll, pipeline), mongo_collections(). Documents/filters are maps ↔ BSON; _id reads as hex text and a 24-hex string under _id in a filter is coerced to an ObjectId.

Redis (key-value / cache / structures)§

require db("redis://localhost")
db_open("redis://localhost:6379")
redis_set("session:42", token, 3600)             -- with TTL
let v be redis_get("session:42")
redis_set("cfg", json_encode({"theme": "dark"})) -- structured data is explicit

Values are byte-strings (text if UTF-8, else bytes; integers → number); arguments accept text/bytes/number (anything else: json_encode it). All of them:

db_close(path?) closes a connection of any family.

Redis db-index gotcha: redis://host:6379 → scope redis://host (no /0); …/0 is a different scope.