034. Reuse normalized JSON from map callbacks

  • Status: Implemented
  • Date: 2026-10-14

Problem and mechanism

PostgreSQL map() builds JSON values under the callback's inferred stored type. A later operation could traverse and normalize that generated document again, even when the exact SQL expression already had a validated representation. The translator now records a proof for the generated map output using its SQL expression, native JSON format, and captured stored-type key. It reuses the proof only after full schema validation.

The proof survives ordinary alias projections and SQL-NULL-to-JSON-null normalization. It does not pass through guards, type guards, extraction, casts, or other derived expressions. Presence and Float-loss provenance are separate, and final canonical serialization remains on its existing path. This slice applies to map outputs; it does not establish equivalent proof for merged fold state, flatmap(), or arbitrary constructed containers.

Applicability

Value path Status Reason
Fully validated map() output with the exact stored type and native JSON representation Applied Reuse the established representation for that exact SQL value.
Ordinary projection alias or SQL NULL normalized to JSON null Applied These preserve the expression's value and representation proof.
Guard, type guard, extraction, cast, or other derived value Not applied Each derived expression needs its own representation proof.
flatmap(), merged fold state, or arbitrary container output Not applied Their output and merged-state representation require separate proof.
Presence or Float-loss handling Not changed These remain independent of the normalized-JSON proof.

Reproduction

The baseline is commit d8673fa. The temporary SQL-shape fixture used Go 1.27.1 on Darwin/arm64 and PostgreSQL 18.6 on aarch64; shape plans used extra_float_digits=1. It bound nullable JSON items values as lists of nullable callback values. The tested callback result families included numeric unions, Struct values, nested maps, Lists, Bytes, Duration, and Timestamp.

Representative queries were:

items:map(x=>x):json="[]"
[items:map(x=>x)]:size>0
[items:map(x=>items:map(y=>y))]:size>0

The shape fixture covered 13 workloads: two JSON serialization cases, Struct results, nested maps, List results, Bytes, Duration, and Timestamp, plus controls for guarded Map output, flatmap(), folds, literals, and simple map():size. It counted generated WHERE bytes, OFFSET 0 barriers, and json_array_elements expansion sites. The temporary shape generator and raw SQL/plans are outside the repository; the rows below preserve their exact query sources.

The durable unit tests cover exact proof matching, alias/null preservation, fallback for derived values, schema checks, and cancellation. The PostgreSQL/local integration test checks 11,648 PostgreSQL/local matches on both the baseline and implementation across JSON and metadata-backed JSONB, extra_float_digits 1 and 3, and nullable/empty/nonempty List inputs. Its stored-type fixtures cover Int, Float, String, Bool, Bytes, Duration, Timestamp, numeric and object unions, Lists, Maps, Structs, and Any. The matrix combines 14 query contexts and 16 record rows per stored type. Records include SQL NULL and JSON null; queries exercise captures, shadowed callback bindings, guards, nested callbacks, and serialization/access paths. A separate decode-failure test checks 24 PostgreSQL results on both revisions for raw Float spellings 1e-400, -1e-400, and 1e400 across nested-map, literal, serialization, and equality contexts at both Float settings.

Run the durable checks with:

GOTOOLCHAIN=go1.27.1 go test ./backends/sql -run '^(TestNormalizedJSONProof|TestNormalizedMapOutput)' -count=1
HFF_POSTGRES_CONTAINER=<container> GOTOOLCHAIN=go1.27.1 go test -tags=integration ./backends/sql -run '^TestPostgreSQLNormalizedMap' -count=1

Results

Element type before nullable wrapping Query Baseline bytes / barriers / array expansions Candidate bytes / barriers / array expansions
string|int|float items:map(x=>x):json="[]" 6,875 / 10 / 3 4,924 / 10 / 2
int|float items:map(x=>x):json="[]" 6,743 / 10 / 3 4,862 / 10 / 2
Struct {i: int, f: float} [items:map(x=>x)]:size>0 5,980 / 9 / 2 3,531 / 9 / 1
float [items:map(x=>items:map(y=>y))]:size>0 6,369 / 13 / 5 4,376 / 13 / 3
list[float] [items:map(x=>x)]:size>0 5,070 / 9 / 4 3,080 / 9 / 2
bytes [items:map(x=>x)]:size>0 3,380 / 9 / 2 2,378 / 9 / 1
duration [items:map(x=>x)]:size>0 4,926 / 9 / 2 4,476 / 9 / 1
timestamp [items:map(x=>x)]:size>0 3,906 / 9 / 2 3,461 / 9 / 1
int|float (guard control) (items:map(x=>x))!:json="[]" 6,881 / 11 / 3 6,881 / 11 / 3
float (flatmap control) [items:flatmap(x=>[x])]:size>0 5,739 / 10 / 4 5,739 / 10 / 4
float (fold control) [items:foldl([], (a,x)=>a:concat([x]))]:size>0 11,141 / 18 / 8 11,141 / 18 / 8
float (literal control) [items]:size>0 2,245 / 5 / 1 2,245 / 5 / 1
float (size control) items:map(x=>x):size>0 1,723 / 8 / 1 1,723 / 8 / 1

Here, array expansions count json_array_elements sites. The focused shape rows reduce those sites but leave their OFFSET 0 barrier counts unchanged. Plain EXPLAIN on an empty records(items json) fixture showed corresponding reductions in Function Scan on json_array_elements nodes: 3 to 2 for numeric union serialization, 5 to 3 for nested maps, and 4 to 2 for List-valued callbacks. Result and Nested Loop counts stayed unchanged in these cases (11/9, 13/12, and 9/8, respectively). This was plain plan inspection without ANALYZE; execution time was not measured. The 11,648-result parity matrix passed on both baseline and implementation, as did the separate 24-result Float decode-failure check. These results do not measure timing, allocations, retained memory, or throughput.

Correctness and tradeoffs

The proof is keyed to the exact expression, JSON format, and captured stored type, and full stored-schema validation remains in front of reuse. Ordinary alias projection and SQL-null normalization preserve the proof; extraction, casts, and guards do not inherit it. The integration matrix exercises metadata-backed JSONB at both Float settings alongside native JSON. It does not validate caller-supplied metadata or eliminate its requirements.

The optimization avoids selected repeated traversals. Required input scans, JSON construction, callback evaluation, projection barriers, and canonical output serialization remain. Fold state and broader provenance propagation remain separate questions. No runtime-speed claim follows from these SQL-shape changes.

Decision and limits

Reuse normalized representation only for the exact fully validated map output and value-preserving aliases covered here. Keep presence and Float-loss provenance separate. Do not infer normalized state from JSON text, key order, or an unrelated derived expression. Fold/merged state, flatmap(), and wider constructed-container reuse require independent schema and provenance proofs.

Explore HFF
Search documentation

Search the language specification and guides.