Reading Data

Includes & Relations

Resolve references with include(), pick columns with select(), inspect permissions, and run recursive queries.

Includes

Use include(...) to load related rows as nested objects in a single query result. Pass true to resolve a relation, or a nested object to follow multi-hop references. In Rust, use join() with on() for equivalent results.

Including a relation adds the resolved object alongside the foreign-key column — it does not replace it. For example, include({ project: true }) gives you both projectId (the FK string) and project (the resolved row).

schema.ts
projects: s.table({
  name: s.string(),
}),
todos: s.table({
  title: s.string(),
  done: s.boolean(),
  description: s.string().optional(),
  owner_id: s.string().optional(),
  parentId: s.ref("todos").optional(),
  projectId: s.ref("projects").optional(),
}),
export async function readTodosWithIncludes(db: Db) {
  return db.all(
    app.todos.where({ done: false }).include({ project: true, parent: { project: true } }),
  );
}
pub async fn read_todos_with_project(client: &JazzClient) -> jazz_tools::Result<usize> {
    let query = QueryBuilder::new("todos")
        .filter_eq("done", Value::Boolean(false))
        .join("projects")
        .on("todos.project_id", "projects._id")
        .build();

    let rows = client.query(query, None).await?;
    Ok(rows.len())
}

Reverse relations

When table A has a ref column pointing to table B, Jazz auto-derives a reverse relation on B that returns all matching A rows as an array. The naming convention is {sourceTable}Via{RelationName} — for example, if todos has projectId: s.ref("projects"), then projects gets a todosViaProject reverse relation.

export async function readProjectsWithTodos(db: Db) {
  return db.all(app.projects.include({ todosViaProject: app.todos.where({ done: false }) }));
}
pub fn build_projects_with_todos_query() -> jazz_tools::Query {
    QueryBuilder::new("projects")
        .with_array("todos_via_project", |sub| {
            sub.from("todos")
                .correlate("project_id", "_id")
                .filter_eq("done", Value::Boolean(false))
        })
        .build()
}

You can chain .where(), .select(), .orderBy() and other query methods on the included reverse relation to filter or shape the nested results.

Select

Use select(...) to narrow a row to id plus the columns you pick. You can combine it with include(...), and select within included rows too.

export async function readTodoTitlesWithSelectedProject(db: Db) {
  return db.all(
    app.todos
      .select("title")
      .where({ done: false })
      .include({ project: app.projects.select("name") }),
  );
}
pub async fn read_todo_titles(client: &JazzClient) -> jazz_tools::Result<usize> {
    let query = QueryBuilder::new("todos")
        .select(&["title", "done"])
        .build();

    let rows = client.query(query, None).await?;
    Ok(rows.len())
}

Missing references

Jazz is distributed and supports offline edits, so a referenced row won't always be available locally — it might not have synced yet, or another peer might have deleted it.

When you don't load a reference, the FK column contains its raw value (the UUID string) and the row always appears in results.

When you do load a reference (TypeScript include, Rust join/on), Jazz uses inner-join semantics. If the FK is set but the target row can't be resolved — not yet synced, deleted, or hidden by permissions — the row is filtered out. If the FK column is null (nullable and unset), the row is still returned with the included field set to null.

Requiring includes

To also filter out rows where the FK is null, use .requireIncludes(). This drops rows where any included forward reference is missing — whether the FK is unset or the target can't be resolved.

export async function readTodosWithRequiredProject(db: Db) {
  return db.all(app.todos.where({ done: false }).include({ project: true }).requireIncludes());
}
pub fn build_todos_with_required_project() -> jazz_tools::Query {
    QueryBuilder::new("todos")
        .filter_eq("done", Value::Boolean(false))
        .with_array("project", |sub| {
            sub.from("projects")
                .correlate("_id", "project_id")
                .require_result()
        })
        .build()
}

requireIncludes() only filters forward references (FK → row). Reverse relations (todosViaOwner, etc.) are unaffected. When used inside a nested include, it applies at that nesting level only.

Magic columns

Jazz exposes computed columns for permission introspection ($canRead, $canEdit, $canDelete) and edit metadata ($createdBy, $createdAt, $updatedBy, $updatedAt). See Magic columns for details and examples.

Recursive queries with gather and hopTo

If your data has self-referencing relations (e.g. a todo with a parent that points to another todo), use gather(...) to walk the graph recursively and collect all reachable rows in a single query.

gather takes three options:

OptionDescription
startA where filter selecting the root rows to begin traversal from
stepA callback that receives { current } (a token for the row being visited) and returns a query with one .hopTo() call specifying which relation to follow
maxDepthMaximum recursion depth (default: 10)

Inside the step callback, call hopTo(relation) to tell Jazz which reference to follow at each level.

export function buildTodoLineageQuery() {
  return app.todos.gather({
    start: { done: false },
    step: ({ current }) => app.todos.where({ parentId: current }).hopTo("parent"),
    maxDepth: 10,
  });
}
pub fn build_todo_lineage_query() -> jazz_tools::Query {
    QueryBuilder::new("todos")
        .filter_eq("done", Value::Boolean(false))
        .with_recursive(|r| {
            r.from("todos")
                .correlate("id", "parent_id")
                .hop("todos", "parent_id")
                .max_depth(10)
        })
        .build()
}

This starts from all incomplete todos, then follows each todo's parentIdparent relation up to 10 levels deep, returning the full lineage.

On this page