Resource embedding (PostgREST select=...,rel(...)) for the read pipeline.
Given the parsed select tree, the source Relation, its SQL alias, and the
full introspection map, this module produces:
build_row_select/6— the named select list ({expr, out_name}pairs) for a derived table whose row record the executor aggregates withjson_agg. Scalar fields, json-paths, casts, aggregates, computed columns, embeds (many-to-one / one-to-many / many-to-many / one-to-one / spread / computed relationships) are rendered here as correlated sub-queries or, for spread, pulled up through aLEFT JOIN LATERAL.inner_join_where/6— the extraWHEREpredicate that an!innerembed (or an embedded filter that implies inner) adds to the source query so rows whose embedding is empty are dropped.group_by/3— the implicitGROUP BYclause when plain fields are mixed with aggregates.
Relationship resolution walks the foreign keys discovered by
Bier.Introspection, plus computed relationships (SETOF-returning functions).
Disambiguation errors (PGRST200 / PGRST201) are thrown as
{:embed_error, body} and turned into responses by the controller.
Summary
Functions
Builds an ORDER BY clause for the aliased pipeline, supporting plain columns,
json paths, computed columns (schema.fn(alias)), and related ordering
(order=<rel>(<col>)), which orders by a column of a to-one embedded resource
via a correlated scalar subquery.
Build the named select list for a single row of relation (aliased as al),
given the select nodes. Returns {cols, laterals, state}: cols is a list
of {expr, out_name} pairs (rendered with render_cols/1), laterals is a
list of LEFT JOIN LATERAL (...) ON true clauses contributed by spread
embeds. Aggregating the derived table's row record (instead of a
json_build_object scalar, which spaces "k" : v) matches PostgREST's
wire bytes — see issue #31. embed_filters maps embed paths to filter
nodes. qe is the executor module (passed to avoid a compile cycle).
When the select mixes plain fields with aggregates, returns the implicit
GROUP BY clause; otherwise {"", ""}.
Builds the WHERE predicate added to the source query for !inner embeds.
Render a node produced by rewrite_null_embed_filters/3 against the current
scope (state.relation / state.alias_name).
Resolve the embed target names a request's filters, orders, limits and offsets used, rewriting each path to the embed's canonical name.
Rewrite every filter leaf whose column names one of select's embeds into a
null-filter on that embedded resource (<embed>=is.null /
<embed>=not.is.null).
Functions
Builds an ORDER BY clause for the aliased pipeline, supporting plain columns,
json paths, computed columns (schema.fn(alias)), and related ordering
(order=<rel>(<col>)), which orders by a column of a to-one embedded resource
via a correlated scalar subquery.
Related ordering validates against the request's select tree: the named
relation must be embedded (else PGRST108) and must be to-one (else PGRST118).
Build the named select list for a single row of relation (aliased as al),
given the select nodes. Returns {cols, laterals, state}: cols is a list
of {expr, out_name} pairs (rendered with render_cols/1), laterals is a
list of LEFT JOIN LATERAL (...) ON true clauses contributed by spread
embeds. Aggregating the derived table's row record (instead of a
json_build_object scalar, which spaces "k" : v) matches PostgREST's
wire bytes — see issue #31. embed_filters maps embed paths to filter
nodes. qe is the executor module (passed to avoid a compile cycle).
When the select mixes plain fields with aggregates, returns the implicit
GROUP BY clause; otherwise {"", ""}.
Builds the WHERE predicate added to the source query for !inner embeds.
The propagation is recursive: an !inner embed nested inside another one
contributes its own EXISTS inside the outer one, so a filter N levels down
drops non-matching rows at every level of the chain (case 1192).
Render a node produced by rewrite_null_embed_filters/3 against the current
scope (state.relation / state.alias_name).
A correlated scalar count(*) subquery cannot be pulled up into a semi/anti
join, so the parent scan order is preserved (matching PostgREST's
LATERAL-based null filtering, whose row order the frozen cases pin). > 0
keeps parents that HAVE a surviving related row; = 0 keeps those with none.
@spec resolve_target_names(map(), boolean()) :: {:ok, map()} | {:error, {:embed_not_selected, String.t()} | {:embed_not_selected, String.t(), String.t()}}
Resolve the embed target names a request's filters, orders, limits and offsets used, rewriting each path to the embed's canonical name.
PostgREST resolves <target>.<...> query params against the select tree with
matchTarget, which under url-use-legacy-target-names = true accepts either
the embed's alias or its relation name, and binds the param to the first
matching node (Plan.hs updateNode … find). Both spellings are normalised
here to alias || relation, the key the SQL builder routes on, so that:
- an aliased embed still answers to its relation name (
tasks.nameforthe_tasks:tasks(...)), and - embedding the same relation twice keeps the two filters apart — the plain
node claims
tasks.before the aliased node can (v16.0's "unexpected results when embedding and filtering the same table more than once" fix).
Every legacy match is recorded in the plan's :legacy_target_names as a
{relation_name, alias} pair (relIsLegacyTargetNameMatch), which
Bier.Plugs.Warning turns into the deprecation Warning header.
A target name that resolves to no node is a 400 PGRST108 (updateNode's
NotEmbedded), returned as {:error, {:embed_not_selected, name}} — or, when
the name would have resolved under the legacy rule (i.e. it is the relation
name of an aliased embed and legacy? is false),
{:error, {:embed_not_selected, name, alias}}, which carries the
alias-specific details/hint PostgREST builds from findLegacyUsage.
Rewrite every filter leaf whose column names one of select's embeds into a
null-filter on that embedded resource (<embed>=is.null /
<embed>=not.is.null).
This is PostgREST's addNullEmbedFilters (Plan.hs L959-L972): it recurses
through CoercibleExpr before rewriting leaves, so a null filter may sit
inside an and=()/or=() group and two embeds can be OR-ed against each
other (case 1195); and it recurses into the whole read-plan forest, so the
rewrite applies at every nesting depth (case 1194).
The embed's OWN filters (<embed>.<col>=…, taken from embed_filters) are
carried on the rewritten node because PostgREST evaluates the null test
against the embed's post-filter aggregate — the embedded filter lives
inside the embed's LEFT JOIN LATERAL and shrinks the aggregate first, so an
embed whose rows were all filtered out aggregates to NULL (cases 1198/1199).