Quarry
Quarry 是 PetraDB 的类型安全查询构建器,它生成 AST 对象而非 SQL 字符串,完全绕过解析器。模式定义作为 DDL、查询和编译时 TypeScript 类型的单一事实来源。
npm install @petradb/quarryimport { Session } from "@petradb/engine";import { quarry } from "@petradb/quarry";
const session = new Session({ storage: "memory" });const db = quarry(session);// 内存模式(默认)new Session({ storage: "memory" });
// 文件持久化存储new Session({ storage: "persistent", path: "./mydb.petra" });使用 Quarry 的列构造函数定义表。模式驱动表创建、查询构建和 TypeScript 类型推断:
import { table, serial, text, integer, boolean } from "@petradb/quarry";
const users = table("users", { id: serial("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), age: integer("age"), active: boolean("active").notNull().default(true),});| 构造函数 | SQL 类型 | TypeScript 类型 |
|---|---|---|
serial(name) | SERIAL | number |
bigserial(name) | BIGSERIAL | number |
text(name) | TEXT | string |
varchar(name, length?) | VARCHAR(n) | string |
char(name, length?) | CHAR(n) | string |
integer(name) | INTEGER | number |
smallint(name) | SMALLINT | number |
bigint(name) | BIGINT | number |
doublePrecision(name) | DOUBLE | number |
real(name) | REAL | number |
numeric(name, precision?, scale?) | NUMERIC(p,s) | number |
boolean(name) | BOOLEAN | boolean |
uuid(name) | UUID | string |
timestamp(name) | TIMESTAMP | string |
timestamptz(name) | TIMESTAMPTZ | string |
date(name) | DATE | string |
time(name) | TIME | string |
timetz(name) | TIMETZ | string |
interval(name) | INTERVAL | string |
json(name) | JSON | unknown |
bytea(name) | BYTEA | number[] |
| 修饰符 | 效果 |
|---|---|
.notNull() | 列不能为 null;InferSelect 类型排除 null |
.default(value) | 列在 InferInsert 中为可选 |
.primaryKey() | 主键;隐含 notNull + hasDefault(serial 自增) |
.unique() | 添加唯一约束 |
.references(table, column) | 添加外键引用 |
Quarry 从每个表定义推断两种类型:
import type { InferSelect, InferInsert } from "@petradb/quarry";
type User = InferSelect<typeof users>;// { id: number, name: string, email: string, age: number | null, active: boolean }
type NewUser = InferInsert<typeof users>;// { name: string, email: string, age?: number | null, active?: boolean, id?: number }InferSelect — 查询返回的行类型:
notNull列 → 非空类型- 可空列 →
type | null
InferInsert — .values() 接受的类型:
- 没有默认值的
notNull列 → 必需 - 有默认值的列(
.default()、.primaryKey()、serial) → 可选 - 可空列 → 可选,接受
null
await db.createTable(users);这会从模式定义生成并执行 CREATE TABLE 命令 — 无需 SQL。
// 单行 — 返回包含所有列的插入行const [user] = await db .insert(users) .values({ name: "Alice", email: "alice@example.com", age: 30 }) .execute();// user.id → 自动生成的序列号// user.active → true(默认值)
// 多行await db .insert(users) .values( { name: "Bob", email: "bob@example.com", age: 25 }, { name: "Charlie", email: "charlie@example.com" }, ) .execute();插入要求所有没有默认值的 notNull 列。可选字段可以省略。TypeScript 在编译时强制执行此规则。
RETURNING
Section titled “RETURNING”默认情况下,插入返回所有列(*)。使用 .returning() 选择特定列:
const [{ id }] = await db .insert(users) .values({ name: "Alice", email: "alice@example.com" }) .returning(users.id) .execute();Upsert(ON CONFLICT)
Section titled “Upsert(ON CONFLICT)”使用 onConflictDoNothing() 或 onConflictDoUpdate() 处理插入冲突:
// 静默跳过冲突行await db .insert(users) .values({ name: "Alice", email: "alice@example.com" }) .onConflictDoNothing() .execute();
// 冲突时更新特定列await db .insert(users) .values({ name: "Alice", email: "alice@example.com", age: 31 }) .onConflictDoUpdate(["email"], { name: "Alice Updated", age: 31 }) .execute();onConflictDoUpdate 的第一个参数指定冲突列,第二个参数指定要更新的列。两者都是类型安全的 — TypeScript 强制只能使用有效的列键。
INSERT…SELECT
Section titled “INSERT…SELECT”从查询结果插入行而非字面值:
// 归档所有活跃用户const query = db .select(users.name, users.email) .from(users) .where(eq(users.active, true));
await db.insertFrom(archive, query, ["name", "email"]).execute();第二个参数是一个 select 构建器。可选的第三个参数指定要填充的目标列 — 如果省略,引擎期望查询为所有列生成值。
// 不指定列列表(查询必须匹配所有目标列)await db.insertFrom(archive, query).execute();
// 配合 onConflictDoNothingawait db.insertFrom(archive, query, ["name", "email"]).onConflictDoNothing().execute();import { eq, gt, asc, desc } from "@petradb/quarry";
// 所有行(SELECT *)const allUsers = await db.from(users).execute();
// WHERE 子句const alice = await db .from(users) .where(eq(users.name, "Alice")) .execute();
// 指定列const names = await db .select(users.name, users.email) .from(users) .execute();
// 排序、限制、偏移const page = await db .from(users) .orderBy(asc(users.name)) .limit(10) .offset(20) .execute();
// 去重const statuses = await db .select(users.active) .from(users) .distinct() .execute();
// Distinct on — 每个不同值返回一行const perCategory = await db .from(products) .distinctOn(products.category) .orderBy(asc(products.category), asc(products.price)) .execute();// 返回每个分类中最便宜的产品列作为表对象上的属性直接访问。TypeScript 会阻止访问模式中不存在的列:
users.name; // ✓ 编译通过users.title; // ✗ 编译错误 — 'title' 不在 users 中import { eq, ne, gt, gte, lt, lte, like, ilike } from "@petradb/quarry";import { notLike, notIlike, isDistinctFrom, isNotDistinctFrom } from "@petradb/quarry";
eq(users.name, "Alice") // name = 'Alice'ne(users.name, "Bob") // name != 'Bob'gt(users.age, 21) // age > 21gte(users.age, 18) // age >= 18lt(users.age, 65) // age < 65lte(users.age, 30) // age <= 30like(users.name, "A%") // name LIKE 'A%'notLike(users.name, "A%") // name NOT LIKE 'A%'ilike(users.email, "%@x%") // email ILIKE '%@x%'notIlike(users.email, "%@x%")
// 空值安全比较isDistinctFrom(users.age, null) // age IS DISTINCT FROM NULLisNotDistinctFrom(users.age, null) // age IS NOT DISTINCT FROM NULLimport { and, or, not } from "@petradb/quarry";
and(eq(users.active, true), gt(users.age, 18))or(eq(users.name, "Alice"), eq(users.name, "Bob"))not(eq(users.active, false))and() 和 or() 接受任意数量的参数:
and(cond1, cond2, cond3) // cond1 AND cond2 AND cond3import { isNull, isNotNull } from "@petradb/quarry";
isNull(users.age) // age IS NULLisNotNull(users.age) // age IS NOT NULLimport { isTrue, isNotTrue, isFalse, isNotFalse, isUnknown, isNotUnknown } from "@petradb/quarry";
isTrue(users.active) // active IS TRUEisNotTrue(users.active) // active IS NOT TRUEisFalse(users.active) // active IS FALSEisNotFalse(users.active) // active IS NOT FALSEisUnknown(users.active) // active IS UNKNOWNisNotUnknown(users.active) // active IS NOT UNKNOWNimport { inList, notInList, between, notBetween, betweenSymmetric } from "@petradb/quarry";
inList(users.name, ["Alice", "Bob", "Charlie"]) // name IN (...)notInList(users.id, [1, 2, 3]) // id NOT IN (...)between(users.age, 18, 65) // age BETWEEN 18 AND 65notBetween(users.age, 18, 65) // age NOT BETWEEN 18 AND 65betweenSymmetric(users.age, 65, 18) // age BETWEEN SYMMETRIC 65 AND 18notBetweenSymmetric(users.age, 65, 18) // age NOT BETWEEN SYMMETRIC 65 AND 18import { add, sub, mul, div, mod, pow, neg } from "@petradb/quarry";
add(users.age, 10) // age + 10sub(users.age, 5) // age - 5mul(users.age, 2) // age * 2div(users.age, 3) // age / 3mod(users.age, 2) // age % 2pow(users.age, 2) // age ^ 2neg(users.age) // -age字符串运算符
Section titled “字符串运算符”import { concat } from "@petradb/quarry";
concat(users.name, " Jr.") // name || ' Jr.'import { bitAnd, bitOr, bitXor, bitNot, leftShift, rightShift } from "@petradb/quarry";
bitAnd(users.flags, 0xFF) // flags & 255bitOr(users.flags, 1) // flags | 1bitXor(users.flags, 0xFF) // flags # 255bitNot(users.flags) // ~flagsleftShift(users.flags, 2) // flags << 2rightShift(users.flags, 1) // flags >> 1JSON 运算符
Section titled “JSON 运算符”import { jsonGet, jsonGetText, jsonPath, jsonPathText } from "@petradb/quarry";import { jsonContains, jsonContainedBy, jsonHasKey, jsonHasAnyKey, jsonHasAllKeys } from "@petradb/quarry";
jsonGet(t.data, "name") // data -> 'name'jsonGetText(t.data, "name") // data ->> 'name'jsonPath(t.data, path) // data #> pathjsonPathText(t.data, path) // data #>> pathjsonContains(t.data, other) // data @> otherjsonContainedBy(t.data, other) // data <@ otherjsonHasKey(t.data, "key") // data ? 'key'jsonHasAnyKey(t.data, keys) // data ?| keysjsonHasAllKeys(t.data, keys) // data ?& keysimport { arrayOverlap } from "@petradb/quarry";
arrayOverlap(t.tags, t.otherTags) // tags && otherTags(数组重叠)对于没有命名辅助函数覆盖的运算符,使用 op() 和 unaryOp():
import { op, unaryOp } from "@petradb/quarry";
op(users.age, ">=", 18) // age >= 18unaryOp("NOT", eq(users.active, true))CASE 表达式
Section titled “CASE 表达式”import { caseWhen, literal } from "@petradb/quarry";
caseWhen( [ { when: gt(users.age, 60), then: literal("senior") }, { when: gt(users.age, 18), then: literal("adult") }, ], "minor", // else)CAST 表达式
Section titled “CAST 表达式”import { cast } from "@petradb/quarry";
cast(users.age, "text") // CAST(age AS TEXT)cast(users.age, "double") // CAST(age AS DOUBLE)别名和字面量
Section titled “别名和字面量”import { alias, literal } from "@petradb/quarry";
alias(add(users.age, 10), "age_plus_10")
literal("hello") // 字符串literal(42) // 数字literal(true) // 布尔literal(null) // nullimport { count, sum, avg, min, max, alias } from "@petradb/quarry";import { stringAgg, arrayAgg, boolAnd, boolOr, jsonAgg, jsonObjectAgg } from "@petradb/quarry";
// 计数所有行const [{ total }] = await db .select(alias(count(), "total")) .from(users) .execute();
// 分组聚合const stats = await db .select(users.active, alias(count(), "cnt")) .from(users) .groupBy(users.active) .execute();
// HAVINGconst popular = await db .select(users.active, alias(count(), "cnt")) .from(users) .groupBy(users.active) .having(gt(alias(count(), "cnt"), 5)) .execute();
// 其他聚合sum(users.age) // SUM(age)avg(users.age) // AVG(age)min(users.age) // MIN(age)max(users.age) // MAX(age)stringAgg(users.name, ", ") // STRING_AGG(name, ', ')arrayAgg(users.name) // ARRAY_AGG(name)boolAnd(users.active) // BOOL_AND(active)boolOr(users.active) // BOOL_OR(active)jsonAgg(users.name) // JSON_AGG(name)jsonObjectAgg(users.name, users.age) // JSON_OBJECT_AGG(name, age)import { variance, varSamp, varPop, stddev, stddevSamp, stddevPop } from "@petradb/quarry";
variance(emp.salary) // VARIANCE(salary) — 样本方差varSamp(emp.salary) // VAR_SAMP(salary) — 同 variancevarPop(emp.salary) // VAR_POP(salary) — 总体方差stddev(emp.salary) // STDDEV(salary) — 样本标准差stddevSamp(emp.salary) // STDDEV_SAMP(salary) — 同 stddevstddevPop(emp.salary) // STDDEV_POP(salary) — 总体标准差import { bitAndAgg, bitOrAgg, bitXorAgg } from "@petradb/quarry";
bitAndAgg(emp.flags) // BIT_AND(flags)bitOrAgg(emp.flags) // BIT_OR(flags)bitXorAgg(emp.flags) // BIT_XOR(flags)import { every } from "@petradb/quarry";
every(emp.active) // EVERY(active) — 当所有行为 true 时返回 true聚合 FILTER
Section titled “聚合 FILTER”使用 filter() 限制聚合处理的行:
import { filter } from "@petradb/quarry";
// COUNT(*) FILTER (WHERE salary > 100)filter(count(), gt(emp.salary, 100))
// SUM(salary) FILTER (WHERE active = true)filter(sum(emp.salary), eq(emp.active, true))多个过滤聚合的示例:
const [row] = await db .select( alias(count(), "total"), alias(filter(count(), gt(employees.salary, 100)), "high_earners"), alias(filter(sum(employees.salary), eq(employees.active, true)), "active_payroll"), ) .from(employees) .execute();使用 fn() 调用任何 SQL 函数:
import { fn } from "@petradb/quarry";
fn("upper", users.name) // UPPER(name)fn("coalesce", users.age, 0) // COALESCE(age, 0)fn("length", users.name) // LENGTH(name)fn("lower", users.email) // LOWER(email)fn("abs", users.age) // ABS(age)fn("round", users.score, 2) // ROUND(score, 2)Quarry 支持 inner、left、right、full 和 cross 连接,带编译时结果类型。
Inner 连接
Section titled “Inner 连接”结果包含两个表的所有列。可空性从原始模式保留:
const posts = table("posts", { id: serial("id").primaryKey(), userId: integer("user_id").notNull(), title: text("title").notNull(), body: text("body"),});
const rows = await db .from(users) .innerJoin(posts, eq(users.id, posts.userId)) .where(eq(users.name, "Alice")) .execute();
// 结果类型:(InferSelect<users> & InferSelect<posts>)[]// rows[0].name → string// rows[0].title → string// rows[0].body → string | null(posts 模式中可空)Left 连接
Section titled “Left 连接”被连接表的所有列变为可空,因为不匹配的行产生 null:
const rows = await db .from(users) .leftJoin(posts, eq(users.id, posts.userId)) .execute();
// 结果类型:(InferSelect<users> & Nullable<InferSelect<posts>>)[]// rows[0].name → string (基表,不受影响)// rows[0].title → string | null(left join 使其可空)// rows[0].userId → number | null(left join 使其可空)Right 连接
Section titled “Right 连接”基表的列变为可空,被连接表的列保留其原始可空性:
const rows = await db .from(users) .rightJoin(posts, eq(users.id, posts.userId)) .execute();
// 结果类型:(Nullable<InferSelect<users>> & InferSelect<posts>)[]// rows[0].name → string | null(right join 使基表可空)// rows[0].title → string (被连接表,不受影响)Full 连接
Section titled “Full 连接”两侧都变为可空:
const rows = await db .from(users) .fullJoin(posts, eq(users.id, posts.userId)) .execute();
// 结果类型:(Nullable<InferSelect<users>> & Nullable<InferSelect<posts>>)[]// rows[0].name → string | null// rows[0].title → string | nullCross 连接
Section titled “Cross 连接”生成两个表的笛卡尔积 — 无 on 条件:
const rows = await db .from(users) .crossJoin(posts) .execute();
// 结果类型:(InferSelect<users> & InferSelect<posts>)[]// user × post 的每种组合多个连接正确累积类型:
const comments = table("comments", { id: serial("id").primaryKey(), postId: integer("post_id").notNull(), content: text("content").notNull(),});
const rows = await db .from(users) .innerJoin(posts, eq(users.id, posts.userId)) .leftJoin(comments, eq(posts.id, comments.postId)) .execute();
// posts 列:非空(inner join)// comments 列:可空(left join)// rows[0].title → string (inner join)// rows[0].content → string | null (left join)连接与列选择
Section titled “连接与列选择”const rows = await db .select(users.name, posts.title) .from(users) .innerJoin(posts, eq(users.id, posts.userId)) .execute();const rows = await db .select(users.name, alias(count(), "post_count")) .from(users) .innerJoin(posts, eq(users.id, posts.userId)) .groupBy(users.name) .orderBy(desc(alias(count(), "post_count"))) .execute();使用 tableAs() 为自连接或同一表多次出现时创建别名表:
import { tableAs } from "@petradb/quarry";
const mgr = tableAs(employees, "mgr");const emp = tableAs(employees, "emp");
const rows = await db .select( alias(emp.name, "employee"), alias(mgr.name, "manager"), ) .from(emp) .leftJoin(mgr, eq(emp.managerId, mgr.id)) .execute();别名是类型安全的 — mgr.name 仍然强制 name 存在于 employees 模式中。
IN 子查询
Section titled “IN 子查询”import { inSubquery, notInSubquery } from "@petradb/quarry";
// 至少有一篇文章的用户const rows = await db .from(users) .where( inSubquery(users.id, db.select(posts.userId).from(posts)), ) .execute();
// 没有文章的用户const rows = await db .from(users) .where( notInSubquery(users.id, db.select(posts.userId).from(posts)), ) .execute();EXISTS 子查询
Section titled “EXISTS 子查询”import { exists } from "@petradb/quarry";
const rows = await db .from(users) .where( exists( db.select(literal(1)).from(posts).where(eq(posts.userId, users.id)), ), ) .execute();使用 subquery() 将 select 包装为标量值:
import { subquery } from "@petradb/quarry";
// 年龄大于平均年龄的用户const rows = await db .from(users) .where( gt(users.age, subquery(db.select(avg(users.age)).from(users))), ) .execute();子查询函数(subquery、exists、inSubquery、notInSubquery)直接接受任何查询构建器 — 无需中间转换。
import { asc, desc } from "@petradb/quarry";
// 基本排序db.from(users).orderBy(asc(users.name))db.from(users).orderBy(desc(users.age))
// 多列db.from(users).orderBy(asc(users.name), desc(users.age))
// NULLS FIRST / NULLS LASTdb.from(users).orderBy(asc(users.age, { nulls: "first" }))db.from(users).orderBy(desc(users.age, { nulls: "last" }))不指定 nulls 时,引擎使用默认行为(升序时空值排在最后,降序时空值排在最前)。
// 带 where 的更新const result = await db .update(users) .set({ age: 31 }) .where(eq(users.name, "Alice")) .execute();// result.rowCount → 1
// 更新多个字段await db .update(users) .set({ name: "Alice Smith", age: 32, active: false }) .where(eq(users.id, 1)) .execute();
// 设置为 nullawait db .update(users) .set({ age: null }) .where(eq(users.name, "Bob")) .execute();.set() 方法接受 Partial<InferSelect<T>> — TypeScript 强制有效的列名和类型。
UPDATE…FROM
Section titled “UPDATE…FROM”连接另一个表来驱动更新:
const priceUpdates = table("price_updates", { id: serial("id").primaryKey(), productName: text("product_name").notNull(), newPrice: integer("new_price").notNull(),});
await db .update(products) .set({ price: 0 }) // 设置值;在 WHERE 中使用列引用进行条件逻辑 .from(priceUpdates) .where(eq(products.name, priceUpdates.productName)) .execute();.from() 接受多个表:
db.update(t1).set({ ... }).from(t2, t3).where(and(...))RETURNING
Section titled “RETURNING”update 和 delete 支持 .returning() 返回受影响的行:
const result = await db .update(users) .set({ active: false }) .where(lt(users.age, 18)) .returning(users.id, users.name) .execute();// result.rows → [{ id: 3, name: "Charlie" }, ...]const result = await db .delete(users) .where(eq(users.name, "Alice")) .execute();// result.rowCount → 1DELETE…USING
Section titled “DELETE…USING”连接另一个表来确定要删除的行:
const deleteList = table("delete_list", { id: serial("id").primaryKey(), userName: text("user_name").notNull(),});
await db .delete(users) .using(deleteList) .where(eq(users.name, deleteList.userName)) .execute();.using() 接受多个表:
db.delete(t1).using(t2, t3).where(and(...))使用自动提交/回滚将多个操作包装在事务中:
const result = await db.transaction(async (tx) => { const [user] = await tx .insert(users) .values({ name: "Alice", email: "alice@example.com" }) .execute();
await tx .insert(posts) .values({ userId: user.id, title: "First Post" }) .execute();
return user;});// 如果任何操作抛出异常,整个事务将回滚回调接收一个作用域为该事务的 QuarryDB 实例。回调的返回值成为 transaction() 的返回值,类型保留。
AST 检查
Section titled “AST 检查”每个构建器都有 .toAST() 方法,返回原始 AST 对象而不执行。这对调试、日志记录或构建更高级抽象很有用:
const ast = db .from(users) .where(eq(users.name, "Alice")) .orderBy(asc(users.id)) .limit(10) .toAST();
console.log(JSON.stringify(ast, null, 2));// {// "kind": "query",// "query": {// "kind": "select",// "exprs": [{ "kind": "star" }],// "from": [{ "kind": "table", "name": "users" }],// "where": { "kind": "binary", "left": ..., "op": "=", "right": ... },// "orderBy": [{ "expr": ..., "direction": "asc" }],// "limit": 10// }// }Quarry 构建普通的 JavaScript 对象(带有 kind 字段的判别联合),表示查询 AST。当你调用 .execute() 时,这些对象被传递给引擎的 executeAST() 方法,该方法将其直接转换为引擎的内部 Scala AST — 完全跳过 SQL 字符串生成和解析。
Schema → Builder API → JS AST objects → Engine AST → Rewrite → Execute ↑ 无 SQL 解析器这使 Quarry 具有与 SQL 相同的查询能力,同时消除了解析开销并实现完整的编译时类型安全。
await session.close();