# `sqlite` The `sqlite` module provides access to [SQLite](https://sqlite.org/) 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. ```lua 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 ```lua 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](fs.md#permissions): 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()`](#conclose) 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. ```lua 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`). ```lua 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. ```lua 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. ```lua 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. ```lua 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. ```lua 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. ```lua 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. ```lua 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: ```lua 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. ```lua 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: ```lua 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 ```lua 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() ```