Parser for the PostgREST-style request query string.
parse_request/1 turns a raw query string into a structured query plan
(select tree, filters, order, pagination, write params) that
Bier.QueryExecutor renders into one parameterized SQL statement.
The leaf grammars (json paths, filter expressions, order terms, embed and
aggregate heads, identifiers) are nimble_parsec combinators compiled to
binary-matching clauses (1.6x-5.9x faster than the regex/String.split
parsing they replaced, proven behavior-identical against the conformance
suite -- see bench/REPORT.md). The recursive/orchestration layer (the
select tree, logic groups, embeds, and split_top_commas/1) deliberately
stays on the string path, where nimble_parsec offers no benefit:
split_top_commas/1 is a depth-tracking, quote-aware splitter that must
tolerate arbitrary inner text such as {1,"a,b"}, and the genuinely
recursive grammars recurse back through it and the leaf parsers.
Generated file
The committed lib/bier/query_parser.ex is generated from this
template (lib/bier/query_parser.ex.exs) via mix gen.parsers, which runs
mix nimble_parsec.compile. Only the combinators between the parsec
marker comments are expanded; everything else passes through verbatim. The
generated .ex has no runtime dependency on nimble_parsec (a dev-only
dependency). Edit this template and re-run mix gen.parsers; never edit
the .ex directly.
Summary
Functions
True when a select field is an aggregate: count(), col.sum(),
alias:col.sum()::cast, alias:col->>key::int.sum()::cast. A bare name()
is an aggregate only when name is a known aggregate function; otherwise it
is an empty-projection embed. An <operand>.fn() form is always an aggregate.
True when a select field references an embedding: it has a top-level (
not preceded by a . aggregate marker, i.e. name(...) / alias:name(...)
/ name!hint(...).
Match a logic-group prefix: and(/or(/not.and(/not.or(.
Split an embed term into its name!hint... head and inner sub-select.
Parse a column filter — col_raw (which may carry a json path) plus the
[not.]op[.modifier].value tail — into a filter node.
Parse a json-path column reference (col, col->a->>b, col->0, col->>-3).
Parse one order term: a column order
<col>[->json][.asc|.desc][.nullsfirst|.nullslast] or a related order
<rel>(<col>[->json])[.mods] (ordering by a to-one embedded resource).
Parse a full request query string into a structured query plan.
Parse a scalar select field [alias:]col[::cast][->json] into a :field
node.
Peel a trailing )::cast off an aggregate term
(the regex ^(.*\))::([A-Za-z0-9_ ]+)$).
Split the trailing aggregate call off a select field.
Split a .-delimited query-string key into its segments, treating a quoted
field name as atomic ("a.dotted.column" is one segment, tasks.name is two).
Split a filter's op[.modifier].value tail.
True when col is a valid PostgREST unquoted identifier ([A-Za-z_][A-Za-z0-9_ -]*).
Functions
True when a select field is an aggregate: count(), col.sum(),
alias:col.sum()::cast, alias:col->>key::int.sum()::cast. A bare name()
is an aggregate only when name is a known aggregate function; otherwise it
is an empty-projection embed. An <operand>.fn() form is always an aggregate.
True when a select field references an embedding: it has a top-level (
not preceded by a . aggregate marker, i.e. name(...) / alias:name(...)
/ name!hint(...).
Match a logic-group prefix: and(/or(/not.and(/not.or(.
Returns {negate?, :and | :or, "(...)"} when member begins with
and(/or(/not.and(/not.or( (whitespace allowed before the () and ends
in ); otherwise nil. The returned group keeps its surrounding parens.
Split an embed term into its name!hint... head and inner sub-select.
Returns {:ok, head_string, inner_string} where head is the name!hint...
text and inner is everything between the first ( and the final ) (kept
opaque). Returns :error when the string is not a well-formed embed term.
Parse a column filter — col_raw (which may carry a json path) plus the
[not.]op[.modifier].value tail — into a filter node.
Returns {:ok, %{column:, json_path:, op:, modifier:, negate:, value:}} or
:error.
@spec parse_json_path(String.t()) :: {:ok, {String.t(), [{:arrow | :arrow_text, String.t()}]}} | :error
Parse a json-path column reference (col, col->a->>b, col->0, col->>-3).
Returns {:ok, {base_col, [{:arrow | :arrow_text, key}]}} or :error.
Parse one order term: a column order
<col>[->json][.asc|.desc][.nullsfirst|.nullslast] or a related order
<rel>(<col>[->json])[.mods] (ordering by a to-one embedded resource).
Returns {:ok, term} (column or related order map) or
{:error, {:order_parse, ...}}.
Parse a full request query string into a structured query plan.
Returns {:ok, plan} where plan is a map with keys :select, :filters,
:order, :limit, :offset, or {:error, reason}.
select/order items and column filters are returned as data; the executor
resolves them against the relation's columns and renders SQL.
Parse a scalar select field [alias:]col[::cast][->json] into a :field
node.
Returns {:ok, %{kind: :field, ...}} or {:error, {:select_parse, field}}.
Peel a trailing )::cast off an aggregate term
(the regex ^(.*\))::([A-Za-z0-9_ ]+)$).
Returns {cast, rest} — the trimmed cast and the rest (ending in )) — or
{nil, original} when there is no trailing )::cast.
Split the trailing aggregate call off a select field.
Returns {:ok, operand, fun} where operand is everything before the .
that introduces the call — "" for the field-less count() form — and fun
is the aggregate name. :error when the string does not end in an empty call
<fun>().
The operand is kept opaque here because PostgREST parses it as a full field
reference (pField plus an optional ::cast), so it may carry a json path
and a cast: jsonb_col->>key::integer.sum().
Split a .-delimited query-string key into its segments, treating a quoted
field name as atomic ("a.dotted.column" is one segment, tasks.name is two).
Split a filter's op[.modifier].value tail.
Returns {:ok, op, modifier, value} or :error. modifier is nil unless a
(any)/(all)/(lang) quantifier is present.
True when col is a valid PostgREST unquoted identifier ([A-Za-z_][A-Za-z0-9_ -]*).