Bier.Embed (bier v0.1.0)

Copy Markdown View Source

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 with json_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 a LEFT JOIN LATERAL.
  • inner_join_where/6 — the extra WHERE predicate that an !inner embed (or an embedded filter that implies inner) adds to the source query so rows whose embedding is empty are dropped.
  • group_by/3 — the implicit GROUP BY clause 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

build_order_advanced(terms, select, relation, al, state, qe)

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_row_select(nodes, relation, al, embed_filters, state, qe)

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).

group_by(nodes, al, relation)

When the select mixes plain fields with aggregates, returns the implicit GROUP BY clause; otherwise {"", ""}.

inner_join_where(nodes, relation, al, embed_filters, state, qe)

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_null_embed(map, state, qe)

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.

resolve_target_names(plan, legacy? \\ true)

@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.name for the_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_null_embed_filters(filters, select, embed_filters)

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).