Querying
There are three ways to ask the Data API for data. They are surfaces over one query AST and one compiler, which is the fact that makes the rest of this section short: whichever you use, the same tree reaches the same planner, and the same access rules, row-level security and audit apply.
| Surface | You write | Best for |
|---|---|---|
| Pipe syntax | orders | status = "paid" | -created_at | 10 | A person writing a query |
| JSON syntax | {"from":"orders","where":{"status":"paid"}} | Code, or a model, assembling one |
| GraphQL | query { orders(status: "paid") { id total } } | A client that already speaks GraphQL |
The first two are isomorphic — every form in one has an equivalent in the other, and they decode into the identical tree. GraphQL is a genuinely different surface with a narrower filter vocabulary, and that difference is the main thing to know before choosing it.
Choosing
Section titled “Choosing”Who writes the query decides it, more than what the query does.
Reach for the pipe syntax when a person is typing. It is the shortest of the three, it reads
left to right in the order the work happens, and it is what appears in scripts and examples
throughout these docs. From a script, data.q() takes it directly.
Reach for the JSON syntax when code assembles the query — a filter panel, a saved view, a generated report. Building a pipe string by concatenation invites the injection problem the DSL exists to avoid; building an object is ordinary data manipulation. It is also the better target for a model, because a JSON schema constrains generation far more tightly than a grammar does.
Reach for GraphQL when the client already speaks it, or when the shape of the response — nested relations selected per field — is the point. Know that its filters are narrower than the other two before you commit to it.
What they share
Section titled “What they share”Because all three compile through one engine, these hold everywhere:
- The projection is explicit. Omitting a field list projects the entity’s declared fields in
canonical order. No form compiles to
SELECT *, so a schema change shows up in a diff rather than in a response body. - Access rules and RLS apply to the query, not to the response. Rows you may not see are never fetched; they are not fetched and filtered.
- An include is a separate query per relation, not a join, which keeps the parent row count honest.
- Row caps are enforced by the compiler, so an unbounded query is bounded whether or not you said so.
What HTTP accepts
Section titled “What HTTP accepts”Worth being blunt about, because it is the most common wrong assumption:
| Endpoint | Accepts |
|---|---|
| REST list | Query-string parameters, mapped to the AST. See the HTTP reference |
| GraphQL | GraphQL |
| Scripted endpoints | Whatever your script accepts — and inside it, data.q() takes the pipe syntax |
No HTTP endpoint takes a raw pipe string or a raw JSON DSL body. The pipe and JSON forms are
for in-process callers and for scripts. If you want to expose a rich query surface to a client, the
shape to build is a scripted endpoint that
validates its input and calls data.q() — not a passthrough that hands a caller’s string to the
parser.
In this section
Section titled “In this section”| Page | Covers |
|---|---|
| Pipe syntax | The full grammar: pipes, expressions, values, projection, traversal, CTEs, options |
| JSON syntax | Every key and $-operator, mutations, transactions, and the gaps against the pipe form |
| GraphQL | The generated schema, what it exposes, and what it does not |
| Request flags | Per-request behaviour that applies across surfaces |
| Schema export | Getting the schema out, for clients and codegen |