Bier.Preferences (bier v0.1.0)

Copy Markdown View Source

Parsing and validation of the Prefer request header for the read path (the write path's Prefer handling lives in Bier.Mutation).

Mirrors PostgREST v16.0's ApiRequest.Preferences semantics:

  • a preference is recognized by its WHOLE token, not by its key: acceptedPrefs is the list of canonical <key>=<value> strings the ToHeaderValue instances produce, and isUnacceptable tests membership in it (Preferences.hs#L145-L163). Only timezone= and max-affected=, whose values are free-form, are matched by prefix. A token with a known key but an unknown value (count=none, return=bogus) is therefore invalid, not merely ignored;
  • handling=strict rejects the whole request (400 PGRST122) when ANY supplied preference is invalid. The error details lists the offending tokens verbatim, comma-separated;
  • handling=lenient (or no handling) silently ignores invalid preferences;
  • timezone=<value> is never an invalid preference. v16.0 dropped the pg_timezone_names membership test that v14.12 applied (fromHeaders lost its TimezoneNames argument along with isTimezonePrefAccepted, Preferences.hs#L129-L163), so every value is accepted as a preference and handed to PostgreSQL as the session TimeZone. A value PostgreSQL rejects therefore surfaces as an ordinary database error (SQLSTATE 22023 -> 400), not as a PGRST122 — and handling has nothing left to suppress, which is why the docs say "handling=lenient is ignored for timezone. Invalid time zones always return an error";
  • the applied preferences are echoed in Preference-Applied in PostgREST's canonical prefsVals order — resolution, missing, representation, count, transaction, handling, timezone, max-affected (Preferences.hs#L179-L188) — never in request order. On a read only count, handling and timezone can apply, so the echo is count before handling before timezone.

Numeric UTC offsets (+05:30, -4) are consequently valid too: they are not members of pg_timezone_names but PostgreSQL accepts them as a TimeZone, and the echo carries the raw preference token rather than a normalized zone name.

Summary

Types

The Prefer: count= mode a request resolves to.

Functions

The Prefer: count= mode the request asks for, or :none when it asks for none (PostgREST's preferCount = Nothing default).

Parse the connection's Prefer header for a read.

Put the Preference-Applied echo (from parse_read/1's :applied list) on a response, omitting the header entirely when nothing applied.

Types

count_mode()

@type count_mode() :: :none | :exact | :planned | :estimated

The Prefer: count= mode a request resolves to.

Functions

count_mode(conn)

@spec count_mode(Plug.Conn.t()) :: count_mode()

The Prefer: count= mode the request asks for, or :none when it asks for none (PostgREST's preferCount = Nothing default).

parsePrefs walks the REQUEST tokens and returns the first that is a known count token (Preferences.hs#L165-L167), so with several present the earliest in the header wins — request order, not the order of the internal constructor list (Preferences.hs#L98-L101).

parse_read(conn)

Parse the connection's Prefer header for a read.

Returns:

  • {:ok, %{timezone: tz | nil, handling: :strict | :lenient | nil, max_affected: integer | nil, applied: [token]}} — the timezone to hand to PostgreSQL (nil when none was requested), the handling and max-affected preferences the caller may need to enforce, and the tokens to echo in Preference-Applied.

  • {:error, {:invalid_prefs, details}}handling=strict with one or more invalid preferences; details is the "Invalid preferences: a, b" string.

handling/max_affected are reported but not echoed here beyond handling=: max-affected constrains how many rows a statement may affect, which only a plan that affects rows can honor, so it is the caller's job to enforce it (Bier.Rpc rejects it outright on a routine that cannot return a set, Bier.Mutation counts the affected rows).

put_applied(conn, tokens)

@spec put_applied(Plug.Conn.t(), [String.t()]) :: Plug.Conn.t()

Put the Preference-Applied echo (from parse_read/1's :applied list) on a response, omitting the header entirely when nothing applied.

Both the relation-read and the /rpc/ paths call this: responsePreferences masks the mutation-only preferences per plan but passes preferCount (and handling/timezone) through untouched for EVERY plan (Response.hs#L296), and the RPC dispatch builds its prefHeader from that same rewritten set (Response.hs#L184). One rule, one call site.