Skip to content

Query Builder

The std/db/query module provides a fluent, chainable SQL query builder that generates parameterized SQL. It works on top of the Database core and is portable across SQLite, PostgreSQL, and MySQL.

import { db } from "std/db"
import { QueryBuilder } from "std/db/query"

Create a QueryBuilder bound to a database connection:

var conn: any = db.open(DbDriver.Sqlite, ":memory:")
var qb: any = new QueryBuilder(conn)
// Fluent query
var users: any = qb.table("users")
.where("age", ">", 18)
.orderBy("name")
.limit(10)
.all()

Each call to .table() resets the builder, so a single QueryBuilder instance can be reused for multiple queries.

Return all matching rows as an array of maps.

var users: any = qb.table("users").all()
for (var i: int = 0; i < length(users); i++) {
println(users[i]["name"])
}

Return the first matching row, or null if none match.

var user: any = qb.table("users").where("id", "=", 1).first()
if (user != null) {
println(user["name"])
}

Return the count of matching rows.

var total: any = qb.table("users").count()
var activeCount: any = qb.table("users").where("active", "=", 1).count()

Specify which columns to retrieve. Omit for SELECT *.

var names: any = qb.table("users").select(["name", "email"]).all()

Add a condition joined by AND. Supported operators: =, !=, >, <, >=, <=, LIKE.

var adults: any = qb.table("users")
.where("age", ">=", 18)
.where("active", "=", 1)
.all()

Add a condition joined by OR.

var result: any = qb.table("users")
.where("role", "=", "admin")
.orWhere("role", "=", "moderator")
.all()

.whereNull(column) / .whereNotNull(column)

Section titled “.whereNull(column) / .whereNotNull(column)”

Filter for NULL or non-NULL values.

var noEmail: any = qb.table("users").whereNull("email").all()
var hasEmail: any = qb.table("users").whereNotNull("email").all()

Filter where column value is in a list.

var admins: any = qb.table("users")
.whereIn("role", ["admin", "moderator"])
.all()

Filter where column value is not in a list.

var active: any = qb.table("users")
.whereNotIn("status", ["banned", "suspended"])
.all()

Filter where column is between two values (inclusive).

var midRange: any = qb.table("products")
.whereBetween("price", 10, 50)
.all()

Filter where column is not between two values.

var outliers: any = qb.table("orders")
.whereNotBetween("total", 0, 10)
.all()

Add a raw SQL WHERE clause. Use ? for parameter placeholders.

var results: any = qb.table("users")
.whereRaw("age > ? AND name LIKE ?", [18, "%alice%"])
.all()

Sort results. Direction defaults to "ASC".

var newest: any = qb.table("users").orderBy("created_at", "DESC").all()
var alphabetical: any = qb.table("users").orderBy("name").all()

Paginate results.

// Page 2, 10 items per page
var page2: any = qb.table("users")
.orderBy("id")
.limit(10)
.offset(10)
.all()

Eliminate duplicate rows from the result set.

var cities: any = qb.table("users")
.distinct()
.select(["city"])
.all()

Group results for aggregate queries.

var stats: any = qb.table("orders")
.select(["status", "COUNT(*) as total"])
.groupBy(["status"])
.all()

Add a HAVING condition for filtering grouped results. Used with .groupBy().

var bigSpenders: any = qb.table("orders")
.select(["customerId", "SUM(total) as totalSpent"])
.groupBy(["customerId"])
.having("SUM(total)", ">", 500)
.all()

Add a JOIN clause. Type defaults to "INNER".

var result: any = qb.table("users")
.select(["users.name", "orders.amount"])
.join("orders", "users.id = orders.user_id")
.all()

.leftJoin(table, on) / .rightJoin(table, on)

Section titled “.leftJoin(table, on) / .rightJoin(table, on)”

Shorthand for LEFT and RIGHT joins.

var result: any = qb.table("users")
.leftJoin("profiles", "users.id = profiles.user_id")
.all()

Insert a row. Pass a map of column names to values. Returns { rowsAffected, lastInsertId }.

var result: any = qb.table("users").insert({
"name": "Alice",
"email": "alice@example.com",
"age": 30
})
println(result["lastInsertId"]) // 1

Insert multiple rows in a single statement. All maps must have the same keys. Returns { rowsAffected, lastInsertId }.

var result: any = qb.table("users").insertMany([
{"name": "Alice", "email": "alice@example.com", "age": 30},
{"name": "Bob", "email": "bob@example.com", "age": 25},
{"name": "Charlie", "email": "charlie@example.com", "age": 35}
])
println(result["rowsAffected"]) // 3

.upsert(data: map, conflictColumns: []any)

Section titled “.upsert(data: map, conflictColumns: []any)”

Insert a row, or update it if a row with the same conflict columns already exists. The conflict columns specify the unique key that determines a conflict. Returns { rowsAffected, lastInsertId }.

// Insert or update based on "email"
qb.table("users").upsert(
{"name": "Alice", "email": "alice@example.com", "age": 31},
["email"]
)

Update matching rows. Combine with .where(). Returns { rowsAffected, lastInsertId }.

var result: any = qb.table("users")
.where("id", "=", 1)
.update({"age": 31})
println(result["rowsAffected"]) // 1

Delete matching rows. Combine with .where(). Returns { rowsAffected, lastInsertId }.

var result: any = qb.table("users")
.where("id", "=", 1)
.delete()

Return the sum of a column’s values.

var totalRevenue: any = await qb.table("orders").sum("amount")
println(totalRevenue)

Return the average of a column’s values.

var avgAge: any = await qb.table("users").avg("age")
println(avgAge)

Return the minimum value of a column.

var cheapest: any = await qb.table("products").min("price")
println(cheapest)

Return the maximum value of a column.

var mostExpensive: any = await qb.table("products").max("price")
println(mostExpensive)

Aggregate functions can be combined with WHERE clauses:

var avgSalary: any = await qb.table("employees")
.where("department", "=", "engineering")
.avg("salary")

Return a paginated result set. Returns a map with data, total, page, perPage, and lastPage.

var result: any = await qb.table("products")
.orderBy("name")
.paginate(1, 20)
println(result["data"]) // array of rows for this page
println(result["total"]) // total matching rows
println(result["page"]) // current page number
println(result["perPage"]) // rows per page
println(result["lastPage"]) // last page number

Process rows in batches for memory-efficient iteration over large datasets. The callback receives each batch as an array of rows. Return false from the callback to stop early.

await qb.table("logs").orderBy("id").chunk(100, (batch) => {
for (var i: int = 0; i < length(batch); i++) {
println(batch[i]["message"])
}
return true // continue to next batch
})

Get the generated SQL and parameters without executing the query.

var debug: any = qb.table("users")
.where("age", ">", 18)
.orderBy("name")
.limit(5)
.toSql()
println(debug["sql"]) // SELECT * FROM users WHERE age > ? ORDER BY name ASC LIMIT 5
println(debug["params"]) // [18]
MethodReturnsDescription
.table(name)selfSet table, reset builder
.select(columns)selfColumns to select
.where(col, op, val)selfAND condition
.orWhere(col, op, val)selfOR condition
.whereNull(col)selfIS NULL condition
.whereNotNull(col)selfIS NOT NULL condition
.whereIn(col, values)selfIN (values) condition
.whereNotIn(col, values)selfNOT IN (values) condition
.whereBetween(col, min, max)selfBETWEEN min AND max
.whereNotBetween(col, min, max)selfNOT BETWEEN min AND max
.whereRaw(sql, params?)selfRaw SQL WHERE clause
.having(col, op, val)selfHAVING condition (with groupBy)
.distinct()selfEliminate duplicate rows
.orderBy(col, dir?)selfSort (ASC default)
.limit(n)selfMax rows
.offset(n)selfSkip rows
.groupBy(cols)selfGroup results
.join(table, on, type?)selfJOIN clause
.leftJoin(table, on)selfLEFT JOIN
.rightJoin(table, on)selfRIGHT JOIN
.all()[]anyAll matching rows
.first()anyFirst row or null
.count()intRow count
.sum(col)anySum of column
.avg(col)anyAverage of column
.min(col)anyMinimum of column
.max(col)anyMaximum of column
.paginate(page, perPage)anyPaginated results with metadata
.chunk(size, callback)voidProcess rows in batches
.insert(data)anyInsert a row
.insertMany(rows)anyInsert multiple rows
.upsert(data, conflictCols)anyInsert or update on conflict
.update(data)anyUpdate matching rows
.delete()anyDelete matching rows
.toSql()anyGet SQL + params
import { db, DbDriver } from "std/db"
import { QueryBuilder } from "std/db/query"
var conn: any = db.open(DbDriver.Sqlite, ":memory:")
var qb: any = new QueryBuilder(conn)
// Schema
db.exec(conn, "CREATE TABLE products (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL, category TEXT, stock INTEGER DEFAULT 0)")
// Seed data
qb.table("products").insert({"name": "Widget", "price": 9.99, "category": "tools", "stock": 100})
qb.table("products").insert({"name": "Gadget", "price": 24.99, "category": "electronics", "stock": 50})
qb.table("products").insert({"name": "Doohickey", "price": 4.99, "category": "tools", "stock": 200})
qb.table("products").insert({"name": "Thingamajig", "price": 49.99, "category": "electronics", "stock": 25})
// Expensive electronics
var expensive: any = qb.table("products")
.where("category", "=", "electronics")
.where("price", ">", 20.0)
.orderBy("price", "DESC")
.all()
for (var i: int = 0; i < length(expensive); i++) {
var p: any = expensive[i]
println(p["name"] + " — $" + string(p["price"]))
}
// Category totals
var totals: any = qb.table("products")
.select(["category", "COUNT(*) as count", "SUM(stock) as total_stock"])
.groupBy(["category"])
.all()
for (var i: int = 0; i < length(totals); i++) {
var t: any = totals[i]
println(t["category"] + ": " + string(t["count"]) + " products, " + string(t["total_stock"]) + " in stock")
}
// Restock low items
qb.table("products")
.where("stock", "<", 50)
.update({"stock": 100})
// Verify
var thingamajig: any = qb.table("products")
.where("name", "=", "Thingamajig")
.first()
println("Thingamajig stock: " + string(thingamajig["stock"])) // 100
db.close(conn)