7.8 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.
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 passingnil) 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
trueis stored as the integer1and reads back as1, nottrue. Compare against1/0, or store a realINTEGERcolumn and interpret it yourself. NULLreads back as an absent key. A column holdingNULLsimply isn't set on the row table, sorow.colisnil— exactly as if the key were present andnil. To bind a SQLNULL, passutils.NULL: a literal Luanilcannot 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, andqueryOnerun 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()