Query composition

RAQ treats every query as a relation that can become the input to another relational operation. That is what composition means here: build a larger query by wrapping or combining smaller queries.

Nothing runs while the query is being composed. The result is one serializable tree that can be inspected, rewritten, validated, and finally executed.

Start with one relation

relation creates the smallest useful query:

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

const products = relation("products");

It means “the public relation named products.” It does not fetch that relation.

Every operation below accepts a relation and returns another relation, so each result can be passed directly to the next operation:

const inStock = where(products, "stock > 0");
const affordable = where(inStock, "price < 200");
const visibleFields = project(affordable, ["id", "name", "price"]);
const cheapestFirst = order(visibleFields, "price asc");
const firstPage = limit(cheapestFirst, 20);

The names make the stages easier to discuss, but they are not materialized datasets. firstPage is a single query tree:

limit 20
└─ order price asc
   └─ project id, name, price
      └─ where price < 200
         └─ where stock > 0
            └─ relation products

The same composition can be written inline:

const firstPage = limit(
  order(
    project(
      where(
        where(relation("products"), "stock > 0"),
        "price < 200"
      ),
      ["id", "name", "price"]
    ),
    "price asc"
  ),
  20
);

These forms produce the same tree. Prefer named stages when they make the intent clearer.

Composition can branch

Some operations combine two relations instead of wrapping one. A join, union, difference, or intersection is still a relation, so composition continues after the branches meet:

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

const stockedProducts = where(relation("products"), "stock > 0");
const productCategories = join(
  stockedProducts,
  relation("categories"),
  "products.category_id == categories.category_id"
);

const listing = project(productCategories, ["name", "category_name", "price"]);

The relation-qualified names in the join condition identify which input owns each attribute. The predicate is parsed into expression nodes; it is not pasted into SQL.

Client and server compose the same kind of value

Composition does not stop at the network boundary. Suppose the client sends:

project name, price
└─ where price < 200
   └─ relation products

The server catalog can define public products as a projection over physical tables. Catalog binding replaces the public leaf with that server-owned tree:

project name, price                         client
└─ where price < 200                        client
   └─ project public product fields         server catalog
      └─ join product_id == inventory_id    server catalog
         ├─ physical products
         └─ physical inventory

Authorization can then add another relational operation—for example, a trusted store filter around the physical inventory source. The client query, catalog definition, and policy become one tree before validation and SQL compilation.

This is different from concatenating SQL strings or running several queries and combining their arrays. Each layer contributes structured relational nodes, and the final compiler sees the whole query.

The tree is the contract

Builder calls return plain serializable values. For example:

where(relation("products"), "price > 100")

becomes, in abbreviated form:

{
  "type": "select",
  "input": { "type": "source", "name": "products" },
  "predicate": {
    "type": "gt",
    "left": { "type": "attr", "name": "price" },
    "right": { "type": "value", "value": 100 }
  }
}

Because composition produces data rather than executable code, RAQ can validate source names and attributes, bind values as SQL parameters, apply server-owned rewrites, evaluate supported trees locally, and compare trees for reuse.

Catalogs define the boundary

Composition is deliberately not unrestricted database access. A catalog maps a public relation name to a server-owned relational expression. Clients may compose over the public shape, while physical tables, tenant constraints, and private fields remain under server control.

See Catalogs for the complete binding and authorization path, or Query language for the available relational operations.

Conservative reuse

RAQ may derive one materialized query from another only when it can prove the relationship from their trees. Unsupported or ambiguous cases return unknown and fall back to fetching. Composition makes this analysis possible because the operations and their order remain explicit.