Catalogs

A catalog is not just a list of allowed table names. Each public relation is a server-owned relational expression that RAQ substitutes into the client query.

This page follows one relation from its definition to executed SQL.

Define the public relation

The repository’s profile catalog exposes customer order summaries. Its public relation is implemented by joining three physical sources:

from raq_lib.builders import attr, eq, join, project, rename, source
from raq_lib.catalog import Catalog, CatalogSurface

profile_catalog = Catalog({
    "profile": CatalogSurface(
        query=project(
            join(
                join(
                    rename(source("orders"), {
                        "customer_id": "profile_customer_id",
                        "total": "total",
                        "store_id": "store_id",
                        "channel": "channel",
                        "status": "status",
                    }),
                    rename(source("customers"), {
                        "id": "customer_id",
                        "customer_name": "name",
                    }),
                    eq(attr("profile_customer_id"), attr("customer_id")),
                ),
                rename(source("stores"), {
                    "id": "profile_store_id",
                    "name": "store_name",
                }),
                eq(attr("store_id"), attr("profile_store_id")),
            ),
            ["name", "total", "store_id", "channel", "status", "store_name"],
        ),
        keys=[["name", "total", "store_id"]],
    )
})

Server-owned catalog definitions use expression builders for join predicates, so the boundary between identifiers and values remains explicit. String predicates remain available as frontend shorthand.

The client sees one relation named profile. It does not see orders, customers, or stores, and it does not reproduce their joins.

keys describes stable identity for schema generation and catalog-change materialization. It does not grant additional query access.

Query it from the client

The client composes over the public shape:

import { project, relation, runQuery, where } from "@raquery/query";

const openOrders = project(
  where(relation("profile"), "status == 'open'"),
  ["name", "total", "status", "store_name"]
);

const result = await runQuery(
  "http://127.0.0.1:8001/profile",
  openOrders,
  { store_id: 42 }
);

The serialized query contains the public source—not the physical implementation:

{
  "type": "project",
  "input": {
    "type": "select",
    "input": { "type": "source", "name": "profile" },
    "predicate": {
      "type": "eq",
      "left": { "type": "attr", "name": "status" },
      "right": { "type": "value", "value": "open" }
    }
  },
  "fields": {
    "name": { "type": "attr", "name": "name" },
    "total": { "type": "attr", "name": "total" },
    "status": { "type": "attr", "name": "status" },
    "store_name": { "type": "attr", "name": "store_name" }
  }
}

Bind the public source

On the server, Catalog.apply parses the request and replaces every public source with a deep copy of its catalog expression:

query = profile_catalog.apply(request.query)

Conceptually, the bound tree becomes:

project name, total, status, store_name       client
└─ select status == "open"                    client
   └─ project public profile fields           server catalog
      └─ join store_id == profile_store_id    server catalog
         ├─ join profile_customer_id == customer_id
         │  ├─ rename physical orders
         │  └─ rename physical customers
         └─ rename physical stores

The client’s operations remain outside the server definition. There is no endpoint-specific translation into a different query model.

Compose authorization into the same tree

The profile endpoint scopes physical orders rows using trusted request context:

from raq_lib.rewrite import enforce_store_scope

def apply_profile_auth(query, context):
    store_id = context.get("store_id")
    if store_id is None:
        return query

    return enforce_store_scope(
        query,
        store_id,
        source_names={"orders"},
    )

After catalog binding, the rewrite can reach the physical orders source inside the server-owned expression and wrap it with store_id == 42. The client cannot remove that selection because it never controls the bound tree.

Compile and execute

The endpoint pipeline stays small because each concern produces another relational tree:

query = profile_catalog.apply(request.query)
query = apply_profile_auth(query, request.context)
plan = compile_sql(query)
rows = execute_plan(plan)

return {
    "sql": plan.sql,
    "params": plan.params,
    "rows": rows,
}

Values such as 42 and "open" become bound SQL parameters. The response keeps the generated SQL visible during the preview so the combined behavior remains inspectable.

What the boundary rejects

If the client requests a source the catalog does not expose:

relation("orders")

binding fails with unknown source: orders. Physical source names are useful inside the server definition, but they are not automatically public.

The schema endpoint reflects the same boundary:

curl http://127.0.0.1:8001/schema

It describes profile and its public fields. It does not publish the underlying three-table layout.

Next, read Schema code generation to turn that public contract into a typed client.