Lua runner with rich builtin stdlib
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
a/docs/sqlite.md

8.1 KiB

sqlite

The sqlite module provides access to SQLite databases. SQLite is compiled into the binary, so no system library is required. The module handles type conversion and parameter binding; it is not an ORM. You open a connection and call methods on it.

local con = sqlite.connect(":memory:")

con:execute("CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Alice", 30})

for _, row in ipairs(con:query("SELECT * FROM people")) do
    print(row.id, row.name, row.age)
end

Connecting

local con = sqlite.connect(path)

path is a filename ("data.sqlite"), created if it does not exist, or the special string ":memory:" for a private in-memory database that vanishes when closed. The returned connection is an object you call methods on with : syntax.

Opening a file-backed database runs the fs permission system: one combined read/write question for the database's directory (SQLite keeps its WAL/journal files next to the file). In-memory databases touch no file and are always allowed, even under --no-fs / --sandbox.

The connection closes automatically when it is garbage-collected. Call con:close() to release it eagerly.

Queries

con:execute(sql [, params])

Run a statement that changes data (INSERT, UPDATE, DELETE, CREATE, …). Returns the number of rows changed as an integer.

local changed = con:execute("UPDATE people SET age = age + 1 WHERE name = ?", {"Alice"})
-- changed == 1

con:query(sql [, params])

Run a SELECT and return all matching rows as an array of tables. Each row is a table keyed by column name. An empty result is an empty array (#rows == 0).

local rows = con:query("SELECT id, name FROM people WHERE age > ?", {18})
for _, row in ipairs(rows) do
    print(row.id, row.name)
end

con:queryOne(sql [, params])

Like query, but returns the first row as a table, or nil if nothing matched. Use it for lookups that return at most one row, such as a fetch by primary key.

local row = con:queryOne("SELECT * FROM people WHERE id = ?", {1})
if row then
    print(row.name)
end

Parameters

Pass values as a params table rather than building SQL by string concatenation; the values are then bound and escaped by SQLite. The table is bound either positionally or by name, decided by its keys.

Positional

A plain array binds to ? placeholders in order. It must be a gapless array starting at index 1.

con:query("SELECT * FROM people WHERE age > ? AND name <> ?", {18, "Bob"})

Named

A table with string keys binds to named placeholders. In the SQL the placeholder carries a sigil (:name, @name, or $name); in the table the key is the bare name without it.

con:query("SELECT * FROM people WHERE age > :min", {min = 18})
con:execute("INSERT INTO people (name, age) VALUES (:name, :age)", {name = "Carol", age = 25})

Rules

  • Omitting params (or passing nil) means the statement takes no parameters.
  • A table may be all positional or all named — mixing the two is an error.
  • Every named placeholder in the SQL must have a matching key, or the call errors.

Type mapping

Values convert automatically in both directions.

Lua → SQLite (binding parameters):

Lua SQLite
nil NULL
utils.NULL NULL
boolean INTEGER (0/1)
integer INTEGER
number REAL
string TEXT

SQLite → Lua (reading rows):

SQLite Lua
NULL nil
INTEGER integer
REAL number
TEXT string
BLOB string (raw bytes)

Two consequences worth knowing:

  • Booleans don't round-trip as booleans. SQLite has no boolean type, so true is stored as the integer 1 and reads back as 1, not true. Compare against 1/0, or store a real INTEGER column and interpret it yourself.
  • NULL reads back as an absent key. A column holding NULL simply isn't set on the row table, so row.col is nil — exactly as if the key were present and nil. To bind a SQL NULL, pass utils.NULL: a literal Lua nil cannot live inside a table, so it can never reach a parameter slot.
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Dave", utils.NULL})
local row = con:queryOne("SELECT age FROM people WHERE name = ?", {"Dave"})
print(row.age) --> nil

Metadata

con:lastInsertRowid()

The rowid of the most recent successful INSERT on this connection, as an integer. For a table with an INTEGER PRIMARY KEY, this is that key.

con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Eve", 40})
local id = con:lastInsertRowid()

con:changes()

The number of rows changed by the most recent INSERT/UPDATE/DELETE. This is the same number execute returns, but you can query it separately at any time.

Transactions

con:transaction(fn)

Run fn inside a transaction. If it returns normally the transaction is committed; if it raises an error the transaction is rolled back and the original error is re-raised unchanged.

con:transaction(function()
    con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Frank", 50})
    con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Grace", 60})
end)
-- Both inserts committed together; if either had thrown, neither would persist.

Manual control

For flows that don't fit a single callback, drive the transaction yourself:

con:begin()
local ok, err = pcall(function()
    con:execute("DELETE FROM people WHERE age > ?", {100})
end)
if ok then con:commit() else con:rollback() end
Method Effect
con:begin() Start a transaction (BEGIN).
con:commit() Commit it (COMMIT).
con:rollback() Discard it (ROLLBACK).

You can mix both styles in the same program.

Closing

con:close()

Release the connection and its underlying database handle. Any further call on the connection raises an error. Closing is optional — a connection also closes when garbage-collected — but it is the clean way to release a file lock promptly.

con:close()

Errors

Failures raise Lua errors rather than returning error codes: a bad SQL statement, a type that can't be bound, a parameter mismatch, or use of a closed connection all error() out. Wrap calls in pcall or utils.try where you want to handle failure rather than abort:

local rows, err = utils.try(function()
    return con:query("SELECT * FROM nonexistent")
end)
if not rows then
    log.error("query failed: " .. tostring(err))
end

Notes

  • Statements are cached. Each query is prepared through SQLite's statement cache, so repeating the same SQL reuses the compiled statement. There is no separate prepared-statement API to manage.
  • I/O runs off the event loop. execute, query, and queryOne run SQLite's blocking work on a background thread, so a slow query does not stall other async work (timers, os.sleep, networking) in the same script.

Full example

local con = sqlite.connect(":memory:")

con:execute([[
    CREATE TABLE tasks (
        id    INTEGER PRIMARY KEY,
        title TEXT NOT NULL,
        done  INTEGER NOT NULL DEFAULT 0
    )
]])

con:transaction(function()
    for _, title in ipairs({"write docs", "review code", "update changelog"}) do
        con:execute("INSERT INTO tasks (title) VALUES (:title)", {title = title})
    end
end)

con:execute("UPDATE tasks SET done = 1 WHERE title = ?", {"write docs"})

local pending = con:query("SELECT id, title FROM tasks WHERE done = 0 ORDER BY id")
for _, task in ipairs(pending) do
    print(task.id, task.title)
end

con:close()