Skip to content

SQL Database

Execute SQL queries against PostgreSQL, MySQL, and SQLite databases. Features include parameterized queries, transactions, prepared statements, and a fluent query builder.

For database configuration, see Database.

local sql = require("sql")

Get a database connection from the resource registry:

local db, err = sql.get("app.db:main")
if err then
return nil, err
end
local rows = db:query("SELECT * FROM users WHERE active = ?", {1})
db:release()
ParameterTypeDescription
idstringResource ID (e.g., “app.db:main”)

Returns: DB, error

Connections are automatically returned to the pool when the function exits, but calling `db:release()` explicitly is recommended for long-running operations. Placeholders are passed to the database driver unchanged; the runtime does not rewrite them. SQLite and MySQL use `?`, PostgreSQL uses `$1, $2` — write them in the form your driver expects. The examples below use `?` (SQLite/MySQL). For queries that target more than one engine, build them with the [query builder](#query-builder) and set the dialect's `placeholder_format`.
sql.type.POSTGRES -- "postgres"
sql.type.MYSQL -- "mysql"
sql.type.SQLITE -- "sqlite"
sql.type.UNKNOWN -- "unknown"
sql.isolation.DEFAULT -- "default"
sql.isolation.READ_UNCOMMITTED -- "read_uncommitted"
sql.isolation.READ_COMMITTED -- "read_committed"
sql.isolation.WRITE_COMMITTED -- "write_committed"
sql.isolation.REPEATABLE_READ -- "repeatable_read"
sql.isolation.SERIALIZABLE -- "serializable"
local insert = sql.builder.insert("users")
:columns("name", "email")
:values("alice", sql.NULL)
local value = sql.as.int(42)

Returns: userdata

Coerces value to SQL float type.

local value = sql.as.float(19.99)

Returns: userdata

Coerces value to SQL text type.

local value = sql.as.text("hello")

Returns: userdata

Coerces value to SQL binary type.

local value = sql.as.binary("binary data")

Returns: userdata

Returns SQL NULL marker.

local value = sql.as.null()

Returns: userdata

local query = sql.builder.select("id", "name")
:from("users")
:where({active = 1})
ParameterTypeDescription
columns…stringColumn names (optional)

Returns: SelectBuilder

Creates INSERT query builder.

local query = sql.builder.insert("users")
:columns("name", "email")
:values("alice", "alice@example.com")
ParameterTypeDescription
tablestringTable name (optional)

Returns: InsertBuilder

Creates UPDATE query builder.

local query = sql.builder.update("users")
:set("status", "active")
:where({id = 123})
ParameterTypeDescription
tablestringTable name (optional)

Returns: UpdateBuilder

Creates DELETE query builder.

local query = sql.builder.delete("users")
:where({active = 0})
:limit(100)
ParameterTypeDescription
tablestringTable name (optional)

Returns: DeleteBuilder

Creates raw SQL expression for use in where/having clauses.

local expr = sql.builder.expr("score BETWEEN ? AND ?", 80, 90)
ParameterTypeDescription
sqlstringSQL expression with ? placeholders
args…anyBind arguments (optional)

Returns: Sqlizer

Creates equality condition from table.

local cond = sql.builder.eq({active = 1, status = "open"})
ParameterTypeDescription
maptable{column = value} pairs

Returns: Sqlizer

Creates inequality condition from table.

local cond = sql.builder.not_eq({status = "closed"})
ParameterTypeDescription
maptable{column = value} pairs

Returns: Sqlizer

Creates less-than condition from table.

local cond = sql.builder.lt({age = 18})
ParameterTypeDescription
maptable{column = value} pairs

Returns: Sqlizer

Creates less-than-or-equal condition from table.

local cond = sql.builder.lte({price = 100})
ParameterTypeDescription
maptable{column = value} pairs

Returns: Sqlizer

Creates greater-than condition from table.

local cond = sql.builder.gt({score = 80})
ParameterTypeDescription
maptable{column = value} pairs

Returns: Sqlizer

Creates greater-than-or-equal condition from table.

local cond = sql.builder.gte({age = 21})
ParameterTypeDescription
maptable{column = value} pairs

Returns: Sqlizer

Creates LIKE condition from table.

local cond = sql.builder.like({name = "john%"})
ParameterTypeDescription
maptable{column = value} pairs

Returns: Sqlizer

Creates NOT LIKE condition from table.

local cond = sql.builder.not_like({email = "%@spam.com"})
ParameterTypeDescription
maptable{column = value} pairs

Returns: Sqlizer

Combines multiple conditions with AND.

local cond = sql.builder.and_({
sql.builder.eq({active = 1}),
sql.builder.gt({score = 80})
})
ParameterTypeDescription
conditionstableArray of Sqlizer or table conditions

Returns: Sqlizer

Combines multiple conditions with OR.

local cond = sql.builder.or_({
sql.builder.eq({status = "pending"}),
sql.builder.eq({status = "active"})
})
ParameterTypeDescription
conditionstableArray of Sqlizer or table conditions

Returns: Sqlizer

Placeholder format for ? placeholders (default). Available as sql.builder.default_placeholder alias.

local query = sql.builder.select("*")
:from("users")
:placeholder_format(sql.builder.question)

Placeholder format for $1, $2, … placeholders.

local query = sql.builder.select("*")
:from("users")
:placeholder_format(sql.builder.dollar)

Placeholder format for @p1, @p2, ... placeholders (SQL Server style). Passed to placeholder_format like the formats above.

Placeholder format for :1, :2, ... placeholders. Passed to placeholder_format like the formats above.

Database connection handle returned by sql.get().

Returns database type constant.

local dbtype, err = db:type()

Returns: string, error

Executes SELECT query and returns rows.

local rows, err = db:query("SELECT id, name FROM users WHERE active = ?", {1})
ParameterTypeDescription
sqlstringSQL query with ? placeholders
paramstableArray of bind parameters (optional)

Returns: table[], error

Executes INSERT/UPDATE/DELETE query.

local result, err = db:execute("INSERT INTO users (name) VALUES (?)", {"alice"})
ParameterTypeDescription
sqlstringSQL statement with ? placeholders
paramstableArray of bind parameters (optional)

Returns: table, error

Returns table with fields:

  • last_insert_id - Last inserted ID
  • rows_affected - Number of rows affected

Creates prepared statement for repeated execution.

local stmt, err = db:prepare("SELECT * FROM users WHERE id = ?")
ParameterTypeDescription
sqlstringSQL with ? placeholders

Returns: Statement, error

Begins database transaction.

local tx, err = db:begin({
isolation = sql.isolation.SERIALIZABLE,
read_only = false
})
ParameterTypeDescription
optionstableTransaction options (optional)

Options table fields:

  • isolation - Isolation level from sql.isolation.* (default: DEFAULT)
  • read_only - Read-only transaction flag (default: false)

Returns: Transaction, error

Releases database resource back to pool.

local ok, err = db:release()

Returns: boolean, error

Returns connection pool statistics.

local stats, err = db:stats()

Returns: table, error

Returns table with fields:

  • max_open_connections - Max allowed open connections
  • open_connections - Current open connections
  • in_use - Connections currently in use
  • idle - Idle connections in pool
  • wait_count - Total connection wait count
  • wait_duration - Total wait duration
  • max_idle_closed - Connections closed due to max idle
  • max_idle_time_closed - Connections closed due to idle timeout
  • max_lifetime_closed - Connections closed due to max lifetime

Prepared statement returned by db:prepare().

Executes prepared statement as SELECT.

local rows, err = stmt:query({123})
ParameterTypeDescription
paramstableArray of bind parameters (optional)

Returns: table[], error

Executes prepared statement as INSERT/UPDATE/DELETE.

local result, err = stmt:execute({"alice"})
ParameterTypeDescription
paramstableArray of bind parameters (optional)

Returns: table, error

Returns table with fields:

  • last_insert_id - Last inserted ID
  • rows_affected - Number of rows affected

Closes prepared statement.

local ok, err = stmt:close()

Returns: boolean, error

Database transaction returned by db:begin().

Returns database type constant.

local dbtype, err = tx:db_type()

Returns: string, error

Executes SELECT query within transaction.

local rows, err = tx:query("SELECT id, name FROM users WHERE active = ?", {1})
ParameterTypeDescription
sqlstringSQL query with ? placeholders
paramstableArray of bind parameters (optional)

Returns: table[], error

Executes INSERT/UPDATE/DELETE within transaction.

local result, err = tx:execute("INSERT INTO users (name) VALUES (?)", {"alice"})
ParameterTypeDescription
sqlstringSQL statement with ? placeholders
paramstableArray of bind parameters (optional)

Returns: table, error

Returns table with fields:

  • last_insert_id - Last inserted ID
  • rows_affected - Number of rows affected

Creates prepared statement within transaction.

local stmt, err = tx:prepare("SELECT * FROM users WHERE id = ?")
ParameterTypeDescription
sqlstringSQL with ? placeholders

Returns: Statement, error

Commits transaction.

local ok, err = tx:commit()

Returns: boolean, error

Rolls back transaction.

local ok, err = tx:rollback()

Returns: boolean, error

Creates named savepoint within transaction.

local ok, err = tx:savepoint("sp1")
ParameterTypeDescription
namestringSavepoint name (alphanumeric and underscore only)

Returns: boolean, error

Rolls back to named savepoint.

local ok, err = tx:rollback_to("sp1")
ParameterTypeDescription
namestringSavepoint name

Returns: boolean, error

Releases savepoint.

local ok, err = tx:release("sp1")
ParameterTypeDescription
namestringSavepoint name

Returns: boolean, error

Fluent interface for building SELECT queries.

Sets FROM clause.

local query = sql.builder.select("id", "name"):from("users")
ParameterTypeDescription
tablestringTable name

Returns: SelectBuilder

Adds JOIN clause.

local query = sql.builder.select("*")
:from("users")
:join("orders ON orders.user_id = users.id")
ParameterTypeDescription
joinstringJOIN clause with ? placeholders
args…anyBind arguments (optional)

Returns: SelectBuilder

Adds LEFT JOIN clause.

local query = sql.builder.select("*")
:from("users")
:left_join("orders ON orders.user_id = users.id")
ParameterTypeDescription
joinstringJOIN clause with ? placeholders
args…anyBind arguments (optional)

Returns: SelectBuilder

Adds RIGHT JOIN clause.

local query = sql.builder.select("*")
:from("users")
:right_join("orders ON orders.user_id = users.id")
ParameterTypeDescription
joinstringJOIN clause with ? placeholders
args…anyBind arguments (optional)

Returns: SelectBuilder

Adds INNER JOIN clause.

local query = sql.builder.select("*")
:from("users")
:inner_join("orders ON orders.user_id = users.id")
ParameterTypeDescription
joinstringJOIN clause with ? placeholders
args…anyBind arguments (optional)

Returns: SelectBuilder

Adds WHERE condition.

local query = sql.builder.select("*")
:from("users")
:where({active = 1})
ParameterTypeDescription
conditionstring|table|SqlizerWHERE condition
args…anyBind arguments (optional, when using string)

Supports three formats:

  • String: where("status = ?", "active")
  • Table: where({status = "active"})
  • Sqlizer: where(sql.builder.gt({score = 80}))

Returns: SelectBuilder

Adds ORDER BY clause.

local query = sql.builder.select("*")
:from("users")
:order_by("name ASC", "created_at DESC")
ParameterTypeDescription
columns…stringColumn names with optional ASC/DESC

Returns: SelectBuilder

Adds GROUP BY clause.

local query = sql.builder.select("status", "COUNT(*)")
:from("users")
:group_by("status")
ParameterTypeDescription
columns…stringColumn names

Returns: SelectBuilder

Adds HAVING condition.

local query = sql.builder.select("status", "COUNT(*) as cnt")
:from("users")
:group_by("status")
:having(sql.builder.gt({cnt = 10}))
ParameterTypeDescription
conditionstring|table|SqlizerHAVING condition
args…anyBind arguments (optional, when using string)

Returns: SelectBuilder

Sets LIMIT.

local query = sql.builder.select("*")
:from("users")
:limit(10)
ParameterTypeDescription
nintegerLimit value

Returns: SelectBuilder

Sets OFFSET.

local query = sql.builder.select("*")
:from("users")
:offset(20)
ParameterTypeDescription
nintegerOffset value

Returns: SelectBuilder

Adds columns to SELECT.

local query = sql.builder.select():columns("id", "name", "email")
ParameterTypeDescription
columns…stringColumn names

Returns: SelectBuilder

Adds DISTINCT modifier.

local query = sql.builder.select("status")
:from("users")
:distinct()

Returns: SelectBuilder

Adds SQL suffix.

local query = sql.builder.select("*")
:from("users")
:suffix("FOR UPDATE")
ParameterTypeDescription
sqlstringSQL suffix with ? placeholders
args…anyBind arguments (optional)

Returns: SelectBuilder

Sets placeholder format.

local query = sql.builder.select("*")
:from("users")
:placeholder_format(sql.builder.dollar)
ParameterTypeDescription
formatuserdataPlaceholder format (sql.builder.*)

Returns: SelectBuilder

Generates SQL string and bind arguments.

local sql_str, args = query:to_sql()

Returns: string, table

Creates executor for query.

local executor = query:run_with(db)
local rows, err = executor:query()
ParameterTypeDescription
dbDB|TransactionDatabase or transaction handle

Returns: QueryExecutor

Fluent interface for building INSERT queries.

Sets table name.

local query = sql.builder.insert():into("users")
ParameterTypeDescription
tablestringTable name

Returns: InsertBuilder

Sets column names.

local query = sql.builder.insert("users"):columns("name", "email")
ParameterTypeDescription
columns…stringColumn names

Returns: InsertBuilder

Adds row values.

local query = sql.builder.insert("users")
:columns("name", "email")
:values("alice", "alice@example.com")
ParameterTypeDescription
values…anyRow values

Returns: InsertBuilder

Sets columns and values from table.

local query = sql.builder.insert("users")
:set_map({name = "alice", email = "alice@example.com"})
ParameterTypeDescription
maptable{column = value} pairs

Returns: InsertBuilder

Inserts from SELECT query.

local select_query = sql.builder.select("name", "email"):from("temp_users")
local query = sql.builder.insert("users")
:columns("name", "email")
:select(select_query)
ParameterTypeDescription
querySelectBuilderSELECT query

Returns: InsertBuilder

Adds SQL prefix.

local query = sql.builder.insert("users")
:prefix("INSERT IGNORE INTO")
ParameterTypeDescription
sqlstringSQL prefix with ? placeholders
args…anyBind arguments (optional)

Returns: InsertBuilder

Adds SQL suffix.

local query = sql.builder.insert("users")
:columns("name")
:values("alice")
:suffix("RETURNING id")
ParameterTypeDescription
sqlstringSQL suffix with ? placeholders
args…anyBind arguments (optional)

Returns: InsertBuilder

Adds INSERT options.

local query = sql.builder.insert("users")
:options("DELAYED", "IGNORE")
ParameterTypeDescription
options…stringINSERT options

Returns: InsertBuilder

Sets placeholder format.

local query = sql.builder.insert("users")
:placeholder_format(sql.builder.dollar)
ParameterTypeDescription
formatuserdataPlaceholder format (sql.builder.*)

Returns: InsertBuilder

Generates SQL string and bind arguments.

local sql_str, args = query:to_sql()

Returns: string, table

Creates executor for query.

local executor = query:run_with(db)
local result, err = executor:exec()
ParameterTypeDescription
dbDB|TransactionDatabase or transaction handle

Returns: QueryExecutor

Fluent interface for building UPDATE queries.

Sets table name.

local query = sql.builder.update():table("users")
ParameterTypeDescription
tablestringTable name

Returns: UpdateBuilder

Sets column value.

local query = sql.builder.update("users")
:set("status", "active")
:set("updated_at", sql.builder.expr("NOW()"))
ParameterTypeDescription
columnstringColumn name
valueanyColumn value

Returns: UpdateBuilder

Sets multiple columns from table.

local query = sql.builder.update("users")
:set_map({status = "active", updated_at = sql.builder.expr("NOW()")})
ParameterTypeDescription
maptable{column = value} pairs

Returns: UpdateBuilder

Adds WHERE condition.

local query = sql.builder.update("users")
:set("status", "active")
:where({id = 123})
ParameterTypeDescription
conditionstring|table|SqlizerWHERE condition
args…anyBind arguments (optional, when using string)

Returns: UpdateBuilder

Adds ORDER BY clause.

local query = sql.builder.update("users")
:set("rank", 1)
:order_by("score DESC")
ParameterTypeDescription
columns…stringColumn names with optional ASC/DESC

Returns: UpdateBuilder

Sets LIMIT.

local query = sql.builder.update("users")
:set("status", "active")
:limit(10)
ParameterTypeDescription
nintegerLimit value

Returns: UpdateBuilder

Sets OFFSET.

local query = sql.builder.update("users")
:set("status", "active")
:offset(5)
ParameterTypeDescription
nintegerOffset value

Returns: UpdateBuilder

Adds SQL suffix.

local query = sql.builder.update("users")
:set("status", "active")
:suffix("RETURNING id")
ParameterTypeDescription
sqlstringSQL suffix with ? placeholders
args…anyBind arguments (optional)

Returns: UpdateBuilder

Adds FROM clause.

local query = sql.builder.update("users")
:set("status", "active")
:from("other_table")
ParameterTypeDescription
tablestringTable name

Returns: UpdateBuilder

Updates from SELECT query.

local select_query = sql.builder.select("*"):from("temp_users")
local query = sql.builder.update("users")
:set("status", "active")
:from_select(select_query, "t")
ParameterTypeDescription
querySelectBuilderSELECT query
aliasstringTable alias

Returns: UpdateBuilder

Sets placeholder format.

local query = sql.builder.update("users")
:placeholder_format(sql.builder.dollar)
ParameterTypeDescription
formatuserdataPlaceholder format (sql.builder.*)

Returns: UpdateBuilder

Generates SQL string and bind arguments.

local sql_str, args = query:to_sql()

Returns: string, table

Creates executor for query.

local executor = query:run_with(db)
local result, err = executor:exec()
ParameterTypeDescription
dbDB|TransactionDatabase or transaction handle

Returns: QueryExecutor

Fluent interface for building DELETE queries.

Sets table name.

local query = sql.builder.delete():from("users")
ParameterTypeDescription
tablestringTable name

Returns: DeleteBuilder

Adds WHERE condition.

local query = sql.builder.delete("users")
:where({active = 0})
ParameterTypeDescription
conditionstring|table|SqlizerWHERE condition
args…anyBind arguments (optional, when using string)

Returns: DeleteBuilder

Adds ORDER BY clause.

local query = sql.builder.delete("users")
:where({active = 0})
:order_by("created_at ASC")
ParameterTypeDescription
columns…stringColumn names with optional ASC/DESC

Returns: DeleteBuilder

Sets LIMIT.

local query = sql.builder.delete("users")
:where({active = 0})
:limit(100)
ParameterTypeDescription
nintegerLimit value

Returns: DeleteBuilder

Sets OFFSET.

local query = sql.builder.delete("users")
:where({active = 0})
:offset(10)
ParameterTypeDescription
nintegerOffset value

Returns: DeleteBuilder

Adds SQL suffix.

local query = sql.builder.delete("users")
:where({active = 0})
:suffix("RETURNING id")
ParameterTypeDescription
sqlstringSQL suffix with ? placeholders
args…anyBind arguments (optional)

Returns: DeleteBuilder

Sets placeholder format.

local query = sql.builder.delete("users")
:placeholder_format(sql.builder.dollar)
ParameterTypeDescription
formatuserdataPlaceholder format (sql.builder.*)

Returns: DeleteBuilder

Generates SQL string and bind arguments.

local sql_str, args = query:to_sql()

Returns: string, table

Creates executor for query.

local executor = query:run_with(db)
local result, err = executor:exec()
ParameterTypeDescription
dbDB|TransactionDatabase or transaction handle

Returns: QueryExecutor

The query executor runs builder-generated queries.

Executes query and returns rows (for SELECT).

local rows, err = executor:query()

Returns: table[], error

Executes query and returns result (for INSERT/UPDATE/DELETE).

local result, err = executor:exec()

Returns: table, error

Returns table with fields:

  • last_insert_id - Last inserted ID
  • rows_affected - Number of rows affected

Returns generated SQL and arguments without executing.

local sql_str, args = executor:to_sql()

Returns: string, table

Database access is subject to security policy evaluation.

ActionResourceDescription
db.getDatabase IDAcquire database connection
ConditionKindRetryable
Empty resource IDerrors.INVALIDno
Permission deniederrors.PERMISSION_DENIEDno
Resource not founderrors.NOT_FOUNDno
Resource not databaseerrors.INVALIDno
Invalid parameterserrors.INVALIDno
SQL syntax errorerrors.INVALIDno
Statement closederrors.INVALIDno
Transaction not activeerrors.INVALIDno
Invalid savepoint nameerrors.INVALIDno
Query execution errorvariesvaries

See Error Handling for working with errors.

local sql = require("sql")
-- Get database connection
local db, err = sql.get("app.db:main")
if err then error(err) end
-- Check database type
local dbtype, _ = db:type()
print("Database type:", dbtype)
-- Direct query
local users, err = db:query("SELECT id, name FROM users WHERE active = ?", {1})
if err then error(err) end
for _, user in ipairs(users) do
print(user.id, user.name)
end
-- Builder pattern
local query = sql.builder.select("u.id", "u.name", "COUNT(o.id) as order_count")
:from("users u")
:left_join("orders o ON o.user_id = u.id")
:where(sql.builder.and_({
sql.builder.eq({["u.active"] = 1}),
sql.builder.gte({["u.score"] = 80})
}))
:group_by("u.id", "u.name")
:having(sql.builder.gt({["COUNT(o.id)"] = 0}))
:order_by("order_count DESC")
:limit(10)
local executor = query:run_with(db)
local results, err = executor:query()
if err then error(err) end
-- Transaction with savepoints
local tx, err = db:begin({isolation = sql.isolation.SERIALIZABLE})
if err then error(err) end
local _, err = tx:execute("INSERT INTO users (name) VALUES (?)", {"alice"})
if err then
tx:rollback()
error(err)
end
tx:savepoint("sp1")
local _, err = tx:execute("UPDATE users SET status = ? WHERE id = ?", {"active", 1})
if err then
tx:rollback_to("sp1")
else
tx:release("sp1")
end
local ok, err = tx:commit()
if err then error(err) end
-- Prepared statements
local stmt, err = db:prepare("INSERT INTO logs (message, level) VALUES (?, ?)")
if err then error(err) end
for i = 1, 100 do
local _, err = stmt:execute({"log message " .. i, "info"})
if err then
stmt:close()
error(err)
end
end
stmt:close()
-- NULL and typed values
local insert = sql.builder.insert("products")
:columns("name", "price", "description")
:values("Widget", sql.as.float(19.99), sql.NULL)
local executor = insert:run_with(db)
local result, err = executor:exec()
if err then error(err) end
print("Inserted ID:", result.last_insert_id)
db:release()