Skip to main content

Database API

The db table provides access to the database. Available in queries, procedures, and index hooks.

db.query

local result = db.query({
collection = "xyz.statusphere.status", -- required
did = "did:plc:abc", -- optional: filter by DID
limit = 20, -- optional: max 100, default 20
cursor = params.cursor, -- optional: opaque cursor from a previous response
sort = "name", -- optional: field to sort by, default "indexed_at"
sortDirection = "asc", -- optional: "asc" or "desc", default "desc"
})

-- result.records — array of record tables (each includes a "uri" field)
-- result.cursor — present when more records exist (opaque string, pass back as-is)

The cursor is an opaque string returned in a previous response. Pass it through directly — don't parse or modify it. When no sort field is specified, db.query uses keyset pagination (based on created_at and uri), which is stable even when records are inserted between pages. When a custom sort field is specified, offset-based pagination is used instead.

The sort field can be a top-level column (indexed_at, did, uri) or any field inside the record's value object (e.g. name, createdAt). Field names must contain only alphanumeric characters and underscores.

db.get

local record = db.get("at://did:plc:abc/xyz.statusphere.status/abc123")
-- Returns the record table or nil
-- The returned table includes a "uri" field

db.search

local result = db.search({
collection = "xyz.statusphere.status", -- required
field = "displayName", -- required: record field to search
query = "alice", -- required: search term
limit = 10, -- optional: max 100, default 10
})

-- result.records — array of matching records, ranked by relevance:
-- exact match > prefix match > contains match, then alphabetical

Find records that reference a given AT URI anywhere in their data. Useful for finding likes on a post, replies to a thread, or any record that links to another.

local result = db.backlinks({
collection = "xyz.statusphere.status", -- required
uri = "at://did:plc:abc/xyz.statusphere.status/foo", -- required: the URI to find references to
did = "did:plc:abc", -- optional: filter by DID
limit = 20, -- optional: max 100, default 20
cursor = params.cursor, -- optional: opaque cursor from a previous response
})

-- result.records — array of records whose data contains the given URI
-- result.cursor — present when more records exist (opaque string, pass back as-is)

The search checks the full record data, so it works regardless of which field holds the reference (subject, parent, reply.root, etc.).

db.count

local n = db.count("xyz.statusphere.status")
local n = db.count("xyz.statusphere.status", "did:plc:abc") -- filter by DID

db.raw

Run a raw SQL query against the database. Supports SELECT, INSERT, UPDATE, DELETE, and CREATE TABLE statements.

-- Read query
local rows = db.raw(
"SELECT uri, did, record FROM records WHERE collection = $1 AND did = $2 LIMIT $3",
{ "xyz.statusphere.status", "did:plc:abc", 10 }
)

for _, row in ipairs(rows) do
-- row.uri, row.did, row.record (JSONB is returned as a Lua table)
end

-- Write query (returns affected rows, if any)
db.raw("CREATE TABLE IF NOT EXISTS my_table (id TEXT PRIMARY KEY, value TEXT NOT NULL)")
db.raw("INSERT INTO my_table (id, value) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET value = $2",
{ "key1", "hello" })

Parameters are passed as an array and bound to $1, $2, etc. Supported parameter types: strings, integers, numbers, booleans, and nil.

SQL dialect

Write SQL in SQLite syntax — HappyView translates it to Postgres at runtime if you're using Postgres. See Database Setup for details on what gets translated. If you need database-specific SQL that can't be translated, check db.is_postgres() at runtime.

Column type mapping

SQLite typePostgres typeLua type
TEXTTEXT, VARCHARstring
INTEGERINT4, INT8integer
REALFLOAT4, FLOAT8number
INTEGER (0/1)BOOLboolean
TEXT (JSON)JSON, JSONBtable
TEXT (ISO 8601)TIMESTAMPTZstring (ISO 8601)
OtherOtherstring (fallback)