Skip to content
Contents

Query Builder

Typed queries in TypeScript, instead of SQL

The builder runs on Rindle’s in-browser engine (@rindle/wasm). createReactiveSqlClient and rindle.addQuery take SQL only for now.

The builder writes the same queries as Rindle SQL, as typed TypeScript. Results are typed from your schema, and a misspelled table or column is a type error.

import { createSchema, createWasmStore, gt, number, string, boolean, table } from "@rindle/wasm";

const issue = table("issue")
  .columns({ id: number(), title: string(), priority: number(), closed: boolean() })
  .primaryKey("id");
const comment = table("comment")
  .columns({ id: number(), issueId: number(), body: string() })
  .primaryKey("id");

const schema = createSchema({ tables: [issue, comment] });
const store = await createWasmStore(schema);

const view = store.query.issue
  .where.closed(false)
  .where.priority(gt(3))
  .orderBy("id", "desc")
  .limit(50)
  .sub("comments", comment, { parent: ["id"], child: ["issueId"] }, (c) =>
    c.orderBy("id", "desc").limit(10),
  )
  .materialize();

view.subscribe((rows) => console.log(rows));

The same query in SQL:

SELECT * FROM issue WHERE closed = 0 AND priority > 3 ORDER BY id DESC LIMIT 50
WITH comments OF comment ON issueId = id (ORDER BY id DESC LIMIT 10)

Schema

table(name).columns({ … }).primaryKey(…cols) A table.
string(), number(), boolean(), int64(), json<T>() Column types. Add .nullable() for a column that can be NULL.
createSchema({ tables }) The schema.
rel(parent, child, { parentCol: "childCol" }) A relationship, to reuse in sub, countAs and exists. Group them with defineRelationships({ … }).

Building a query

store.query.<table> starts a query. Every method returns a new query.

Method SQL
.where(condition) WHERE. Calls are combined with AND.
.where.<column>(value) WHERE column = value, or pass an operator: .where.priority(gt(3))
.orderBy(column, "asc" | "desc") ORDER BY
.limit(n) LIMIT
.start(row, { exclusive }) WHERE (cols) > (…): page from a row
.select(...columns) A column list
.one() LIMIT 1. The view’s data is a row or null.
.sub(name, table, correlation, build?) WITH name OF table ON …
.countAs(name, table, correlation, build?) A live count of related rows on each row
.count(), .groupBy(column), .having(…) Aggregates
.materialize() Starts the live query.
.ast() The query as JSON.

A correlation is { parent: [...], child: [...] }: the parent columns, and the child columns they match. A relationship from rel can stand in for the table and correlation.

Conditions

Function SQL
eq, ne, gt, ge, lt, le =, !=, >, >=, <, <=
is, isNot, isNull, isNotNull IS, IS NOT, IS NULL, IS NOT NULL
like, notLike, ilike, notIlike LIKE, NOT LIKE, ILIKE, NOT ILIKE
inList, notInList IN, NOT IN
and(...), or(...) AND, OR
exists(table, correlation, build?), notExists(…) EXISTS, NOT EXISTS
import { exists, or } from "@rindle/wasm";

store.query.issue.where(
  or(issue.priority(gt(8)), exists(comment, { parent: ["id"], child: ["issueId"] })),
);

A table doubles as a condition factory: issue.priority(gt(8)) is the condition priority > 8.

Views

materialize() returns a view:

data The current rows.
subscribe(listener) Calls listener(rows) now and after every change. Returns an unsubscribe function.
destroy() Stops the query.