Skip to content

Immutable generated queries

Each generated package exports a query facade bound to that package's verified projection. It provides the same supported application outcomes in Python, TypeScript/Node, and Rust without routing generated models through handwritten model descriptors.

Start a direct session

from app_models import Age, Employment, Person

session = Person.query(db)
person = session.exact(Person)
employment = session.exact(Employment)

Person.query(db) and the package's QuerySession constructor produce the same package-scoped direct session. A transaction can be supplied instead of a database.

Use exact(Model) for exactly one schema type and subtypes(Model) for a polymorphic binding. Concrete result evidence determines the generated class used for hydration.

Match fields and roles

adult = person.field(Person.age).gte(Age(18))
employee = employment.role(Employment.employee).connects(person)

query = session.query(person, employment).where(adult, employee)

Bindings, field tokens, role tokens, predicates, selections, and queries are immutable and carry one projection identity. Cross-package composition fails before execution.

Low-level Query V2 plans

The generated facade is the usual application API. The separately retained @type-bridge/node/query-v2 entry point authors a complete prepared plan when an application needs the low-level Query V2 vocabulary without model classes. Rust owns its identities, validation, canonical bytes, and capabilities.

The equivalent Node authoring uses the same operation names and order. Keep the low-level authority outside this builder function so the plan vocabulary cannot silently replace the generated package's authority:

import {
  QueryPlanBuilder,
  type AuthoredQueryInvocation,
  type QueryV2Authority,
} from "@type-bridge/node/query-v2";

function authorInvocation(authority: QueryV2Authority): AuthoredQueryInvocation {
  const builder = new QueryPlanBuilder(authority);
  const person = builder.binding("person");
  const name = builder.binding("name");
  const wanted = builder.input("wanted_name", "string", false);
  builder.match([
    builder.isa(person, "entity", "smoke-person", true),
    builder.has(person, name, "smoke-name"),
    builder.value(
      "equal",
      builder.bindingOperand(name),
      builder.inputOperand(wanted),
    ),
  ]);
  return builder.finalizeRows([person, name]).rows([["Alice"]]);
}

Builder transitions perform no provider or network I/O. Finalization is terminal, and a rejected transition leaves the builder at its preceding valid state. Explicit construction of QueryV2Authority remains a low-level API for integrators that already own canonical declared-schema bytes and their trust policy. schema export-declared is retained only as a debug/compatibility tool; generated sessions do not call it, read its JSON, or use it as an authoring input.

Connectivity and explicit cross joins

Every selected binding must be connected by a predicate, match witness, or bounded reachability path. An intentional Cartesian product must be declared:

left = session.exact(Person)
right = session.exact(Person)

rows = (
    session.query(left, right)
    .allow_cross_join(left, right)
    .rows(limit=10)
)

This prevents a missing join predicate from silently becoming an expensive cross product.

Bounded reachability

source = session.exact(Person)
target = session.exact(Person)

path = session.reachable(
    source,
    target,
    NetworkLink,
    NetworkLink.origin,
    NetworkLink.destination,
    min_depth=1,
    max_depth=3,
)

connected = session.query(source, target).where(path).rows(limit=100)

The relation and both role tokens are generated evidence. Depth bounds are mandatory and validated before execution.

Selection shapes

One binding produces one model; multiple bindings produce a tuple:

one_person = session.query(person).one()
pairs = session.query(person, employment).where(employee).rows(limit=100)

Generated Python and TypeScript support positional selections through 16 slots. Named selections map into an immutable declared row shape. Collection preserves one root with zero or more related values:

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class EmploymentRow:
    person: Person
    employments: tuple[Employment, ...]


page = (
    session.query_as(
        EmploymentRow,
        person=person,
        employments=employment.collect().distinct(),
    )
    .where(employee)
    .page_by(person, limit=25, include_total=True)
)

Collections may have their own generated-field ordering. page_by keeps root pagination distinct from collection membership.

Terminals

query.one()
query.first(order_by=(person.field(Person.person_id).asc(),))
query.rows(limit=100, offset=0)
query.page_by(person, limit=25, include_total=True)
query.count_by(person)
query.exists_by(person)

one requires exactly one result. first returns None when empty. Materialized rows and pages are bounded; unbounded implicit fetches are not part of the generated contract.

Direct reductions and grouping

from app_models import aggregate

score = person.field(Person.score)
count, total, mean = session.query(person).aggregate(
    person,
    aggregate.count(),
    aggregate.sum(score),
    aggregate.mean(score),
)

grouped = (
    session.query(person, employment)
    .where(employee)
    .group_by(person, employment)
    .aggregate(aggregate.count(), aggregate.max(score))
)

Reducer inputs are owner/scalar checked. Grouping returns exact generated group values plus typed reducer tuples.

Remote queries

The generated package also exports RemoteQuerySession. It reconstructs Query V2 authority from private, verified package evidence. The caller supplies exact advertisement bytes, a one-exchange transport callback, and resource limits—no authority file or low-level QueryV2Authority argument:

from app_models import Person, RemoteQueryLimits, RemoteQuerySession

remote = RemoteQuerySession(
    advertisement_bytes,
    exchange,
    RemoteQueryLimits(
        max_items=100,
        max_bytes=8_388_608,
        max_collection_members=1_000,
        max_graph_nodes=1_000,
        max_attribute_values=1_000,
        max_role_players=1_000,
        deadline_ms=30_000,
    ),
)
person = remote.exact(Person)
rows = await remote.query(person).rows(limit=50)

TypeScript uses the equivalent new RemoteQuerySession(advertisementBytes, exchange, limits) constructor. Authenticate the advertisement for the intended server or pin it out of band; the caller owns transport, credentials, and retry policy. Composition remains local; one terminal performs one exchange and materializes through the same package projection as direct execution.

Supported remote terminals are one, first, bounded rows, page_by, count_by, exists_by, and typed aggregate/group_by, including exact/subtype hydration, predicates, roles, reachability, explicit cross joins, selected shapes, all reducers, and binding/field/tuple-field grouping. Reduction replies echo the exact root, grouping, and reducer contract and are authenticated before materialization through the same projection as direct execution.

Remote mutations are not advertised by generated sessions.

Safety contract

  • Schema and projection fingerprints are verified at package installation.
  • Only exact registered generated classes and tokens are accepted.
  • Queries are immutable and package-scoped.
  • Disconnected selections fail unless cross joins are explicit.
  • Result evidence is revalidated before hydration.
  • Remote limits and capability advertisements fail closed.

The normative cross-language behavior is maintained in the unified typed-query contract.