Skip to content

fix: mark record/aggregate encoders STABLE, not IMMUTABLE - #8

Merged
xzilla merged 2 commits into
masterfrom
fix/volatility
Oct 1, 2026
Merged

xzilla merged 2 commits into
masterfrom
fix/volatility

Conversation

@xzilla

@xzilla xzilla commented Jul 13, 2026 •

Copy link
Copy Markdown
Owner

Fixes #2.

to_toon, row_to_toon, and the toon_agg support functions wrap to_json/row_to_json, which PostgreSQL marks STABLE because their output depends on TimeZone, DateStyle, and extra_float_digits. Same input, different output across sessions — verified on PG 16.13. Declaring them IMMUTABLE permits expression indexes that silently return wrong results when read under different GUCs.

The pure string helpers (toon_escape, toon_quote_key, toon_quote_value, toon_encode_field) stay IMMUTABLE. A catalog-based regression test pins the volatility of every function (by explicit name list, so unrelated schema objects can't break it). AGENTS.md volatility guidance updated.

Release note for users: CREATE INDEX ... (to_toon(col)) and similar expression indexes / generated columns will now fail with functions in index expression must be marked IMMUTABLE. That's intentional — such indexes were wrong-results hazards.

Merge order: if #11 (perf) merges first, the volatility test's function list needs its new function names added — happy to rebase either way. Suite: 68/68.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEYjgkWDYiRMYwwRS985g5

to_toon, row_to_toon, and the toon_agg support functions are built on
to_json/row_to_json, which PostgreSQL itself marks STABLE because their
output depends on session GUCs (TimeZone, DateStyle, extra_float_digits).
The same timestamptz encodes differently across sessions:

    SET TIME ZONE 'UTC';
    SELECT row_to_toon(q) FROM (SELECT '2024-01-01 00:00+00'::timestamptz AS t) q;
    -- t: "2024-01-01T00:00:00+00:00"
    SET TIME ZONE 'America/New_York';  -- same input:
    -- t: "2023-12-31T19:00:00-05:00"

Declaring them IMMUTABLE lets the planner constant-fold across GUC
changes and, worse, permits expression indexes that silently return
wrong results when read under a different TimeZone/DateStyle.

The pure string helpers (toon_escape, toon_quote_key, toon_quote_value,
toon_encode_field) are genuinely immutable and keep IMMUTABLE. A
regression test now pins the volatility of every function.

Fixes #2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEYjgkWDYiRMYwwRS985g5
@xzilla
xzilla merged commit c751b79 into master Oct 1, 2026
8 of 10 checks passed
xzilla pushed a commit that referenced this pull request Oct 1, 2026
Resolve conflicts with #8 (volatility), #9 (delimiter validation) and
#10 (NaN docs), which landed on master first:

- toon_agg_sfunc: keep this branch's text[] state, mark it STABLE per
  #8, and run #9's delimiter guard before the NULL-record skip so an
  invalid delimiter raises even when the row is NULL.
- Volatility test: replace toon_agg_sfunc_default with the functions
  this branch adds (rows_to_toon, toon_agg_rows_ffunc, toon_delim_ok,
  toon_rows_ok).
- AGENTS.md: list the new aggregate helpers in place of the removed
  toon_agg_state / sfunc_default entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JAaWhjV4FjNThbcAAWksS
xzilla added a commit that referenced this pull request Oct 1, 2026
toon_agg was O(n²) for two reasons: PostgreSQL flattens and re-expands
SQL/plpgsql transition state on every call, and the finalizer built
its output with a per-row concat loop.

The default toon_agg(anycompatible) now collects rows with the C
transition pg_catalog.array_append and encodes once at finalization
via a new public rows_to_toon(anyarray, delim), a single set-based
pass over json_array_elements(array_to_json(rows)). The 2-arg
toon_agg(q, delim) keeps a plpgsql transition (text[] state) and stays
quadratic; rows_to_toon(array_agg(q), delim) is the documented linear
alternative. The toon_agg_state type is removed.

NULL records are now skipped rather than NULL-poisoning the result,
and [N] counts only encoded rows. Integrates the delimiter guard from
#9 and STABLE volatility from #8 into the new transition function.
Fixes #4.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Functions marked IMMUTABLE but output depends on runtime GUCs (should be STABLE)

2 participants