Bier.QueryExecutor (bier v0.1.0)

Copy Markdown View Source

Turns a parsed request plan (Bier.QueryParser.parse_request/1) plus a target relation into ONE parameterized SQL statement that returns the result set as JSON text, then executes it through the per-instance Postgrex pool.

The shape mirrors PostgREST:

SELECT coalesce(json_agg(_postgrest_t), '[]')::text AS body,
       count(*) OVER() AS full_count
FROM ( SELECT <select-list> FROM <schema>.<relation>
       WHERE <filters> ORDER BY <order> LIMIT <l> OFFSET <o> ) _postgrest_t;

The window count (count(*) OVER()) is emitted only for Prefer: count=exact|estimated, avoiding a WindowAgg materialization when not needed (:none and :planned modes).

User-supplied values are always passed as bound parameters ($1, $2, …); identifiers (columns) are validated by the parser and quoted here.

Summary

Functions

Wrap a column value expression in its read-representation cast function when the column has one, so its JSON output uses the registered representation. Returns expr unchanged when there is no read cast. The result is a json value (which to_jsonb/json_build_object embed directly).

Build the row source a custom media handler aggregates over.

Build a representation query whose source is a mutation CTE.

Renders the call to computed field col over the row identified by row.

The Postgres base type of a request field — a column's declared type with its DOMAIN chain resolved (cfBaseType), or a computed field's return type. nil when the relation does not know the name.

The read-representation cast function (<domain> AS json) for a column, as a {schema, function} tuple, or nil when the column has no such cast.

Build and run a read query whose source is a set-returning function (/rpc/<fn>), called with the parsed args (a list of {name, type, value}).

Apply Prefer: timezone to an open transaction. Public so the RPC path (Bier.Rpc) applies it through the same statement and the same error shape rather than duplicating it.

The query-string filter parser (text AS <domain>) for a column, as a {schema, function} tuple, or nil. When present, a filter value is parsed through this function before comparison (cases 1808/1810).

The body-value parser (json AS <domain>) for a column, as a {schema, function} tuple, or nil. When present, a JSON body value is parsed through this function on write (cases 1811-1813).

Functions

apply_read_rep(expr, rel, col)

Wrap a column value expression in its read-representation cast function when the column has one, so its JSON output uses the registered representation. Returns expr unchanged when there is no read cast. The result is a json value (which to_jsonb/json_build_object embed directly).

bind(value, type, state)

build_media_source(relation, plan)

@spec build_media_source(Bier.Introspection.Relation.t(), map()) ::
  {:ok, String.t(), [term()]}

Build the row source a custom media handler aggregates over.

A relation-scoped handler is an aggregate over the relation's whole ROW type, so its projection is always the default select list (an explicit ?select= takes the handler out of negotiation entirely — see Bier.CustomMedia). Only the request's filters, order and limit/offset window narrow the set.

The rows are projected as the whole-row reference __t, so the derived table has a single column carrying the relation's composite type and the aggregate's argument type still matches; a bare SELECT * would degrade it to record.

build_representation(relation, plan, relations, arg, opts \\ [])

Build a representation query whose source is a mutation CTE.

source_sql is the INSERT/UPDATE/DELETE ... RETURNING * statement; its bound params are source_params (in $1..$n order). The returned SQL wraps the source in a CTE named pgrst_source and renders plan.select (with embedding) over it.

opts accepts :format:json (default) or :geojson (aggregate the representation into a GeoJSON FeatureCollection via ST_AsGeoJSON), mirroring run/5.

The result is a single row {body, count, meta} where:

  • body — the JSON-array representation shaped by plan.select,
  • count — the number of mutated rows,
  • meta — a JSON object {"pk": <first row's PK cols>} used to build the Location header.

build_where_aliased(filters, al, state)

column_expr(col, path, relation)

column_expr_aliased(col, path, al, relation)

computed_field_call(rel, col, row)

@spec computed_field_call(Bier.Introspection.Relation.t(), String.t(), String.t()) ::
  String.t()

Renders the call to computed field col over the row identified by row.

Qualified with the schema the function lives in, which is not necessarily rel.schema — PostgREST only requires the function to be in an exposed schema or on the extra search path, so a computed field may extend a relation in a different schema. Qualifying with the relation's schema instead calls the wrong function, or none (#100).

This is the single renderer for all three call sites (filter targets here, select list and ORDER BY in Bier.Embed) so they cannot drift apart.

PostgREST renders this unqualified"alias"."fn", resolved by Postgres functional notation (tbl.f is f(tbl)) plus search_path — and once db_extra_search_path is actually applied (#105) matching it becomes possible. Deliberately not done: search_path is order-dependent, so same-named fields in different schemas would resolve by position and shift with config. That is the context-inference #100 was about, and the explicit form gives identical results. See #106 before changing this.

field_base_type(rel, col)

The Postgres base type of a request field — a column's declared type with its DOMAIN chain resolved (cfBaseType), or a computed field's return type. nil when the relation does not know the name.

json_output_name(col, path)

pg_literal(str)

qrel(relation)

qualified_column_expr(col, path, state)

quote_ident(ident)

quote_type(type)

read_rep_fn(rel, col)

The read-representation cast function (<domain> AS json) for a column, as a {schema, function} tuple, or nil when the column has no such cast.

PostgREST applies this cast in the SELECT list so the column's JSON output is produced by the user-defined representation rather than the base type's default rendering. PostgreSQL strips a domain to its base type for a plain CAST(col AS json), so the cast function is invoked by name instead.

render_node(node, state)

run(conn, relation, plan, relations \\ %{}, opts \\ [])

@spec run(conn :: term(), Bier.Introspection.Relation.t(), map(), map(), keyword()) ::
  {:ok, %{body: String.t(), count: non_neg_integer()}} | {:error, term()}

Build and run the read query.

Options:

  • :count_mode:none (default) / :exact / :planned / :estimated. Controls how the total row count (for Content-Range) is computed.
  • :max_rows — server db-max-rows; used by :estimated to decide when to fall back to an exact count.
  • :format:json (default) or :geojson (aggregate the rows into a GeoJSON FeatureCollection via ST_AsGeoJSON; requires postgis).

Returns {:ok, %{body: json_string, count: non_neg_integer}} or {:error, %Postgrex.Error{}}.

run_function(conn, fn_def, ret_relation, args, plan, opts \\ [])

@spec run_function(
  term(),
  map(),
  Bier.Introspection.Relation.t(),
  [tuple()],
  map(),
  keyword()
) ::
  {:ok, %{body: String.t(), count: non_neg_integer()}} | {:error, term()}

Build and run a read query whose source is a set-returning function (/rpc/<fn>), called with the parsed args (a list of {name, type, value}).

Only the flat read shape is supported (select / filters / order / limit / offset / count) — enough for the GET RPC pagination cases. The function's returned relation supplies the column set for select and column filters.

Options accept :format:json (default) or :geojson (aggregate the rows into a GeoJSON FeatureCollection via ST_AsGeoJSON), mirroring run/5 — and :auth, the {context, config} tuple that makes the call run inside the per-request auth transaction (SET LOCAL ROLE, request.* GUCs, db-pre-request) exactly like a relation read. Without it the statement would execute as the pool's connecting role — issue #108.

set_local_timezone(tx, timezone)

@spec set_local_timezone(term(), String.t() | nil) :: :ok | {:error, term()}

Apply Prefer: timezone to an open transaction. Public so the RPC path (Bier.Rpc) applies it through the same statement and the same error shape rather than duplicating it.

text_rep_fn(rel, col)

The query-string filter parser (text AS <domain>) for a column, as a {schema, function} tuple, or nil. When present, a filter value is parsed through this function before comparison (cases 1808/1810).

write_rep_fn(rel, col)

The body-value parser (json AS <domain>) for a column, as a {schema, function} tuple, or nil. When present, a JSON body value is parsed through this function on write (cases 1811-1813).