055. Prune statically empty PostgreSQL list-concat arms

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

Problem and mechanism

PostgreSQL list concatenation can produce SQL for an arm whose row stream is known to be empty, or retain an append when only one useful stream remains. For supported JSON/JSONB shapes, the builder proves a subset of arms empty and omits their row sources from the generated SQL. Source expressions remain evaluated in the emitted query. The builder also lowers full expressions for Build validation; only discarded normalization and callback validation happen in an isolated pass, so they do not leave unused SQL parameters. When all arms are empty, the result remains the correctly typed empty list.

The initial proof covers literal empty lists, List<Never>, Map<Never> enumeration, zero-input map(), filter(), flatmap(), sort(), or distinct(), literal-false filters, and concat trees whose arms are all explicitly proven empty. It applies to ordinary/dynamic JSON concat and the bounded item-slot map/tree pipeline. The generic deferred-list follow-up applies when a statically empty branch remains available to a compatible materialization, aggregate, or fold consumer; this path does not infer emptiness from computed Never schema flags. It omits proven-empty rows, counts, sentinels, selections, encodings, runtime packet rows, or fold successors while retaining static packet variants and original arm identities. An empty list returned by a callback for a nonempty parent is not statically discarded because its failure sentinel is observable. Runtime-created empty streams without an explicit proof, and unproven generic deferred-list paths, keep their existing lowering.

All operands and callbacks are lowered and validated before output is chosen; receiver-first diagnostic ordering, capability checks, null/no-value precedence, and the original outer presence/null state remain intact. In item pipelines, an earlier map's no-value cannot be erased by an empty later filter; a null concat result still masks failures from maps after that concat. The optimization does not change the HFF concat contract.

Applicability

Shape Status Reason
Statically empty JSON/JSONB concat arm in supported ordinary/dynamic concat Applied The arm is fully validated, then its empty row stream is omitted.
Proven empty arm in the supported item-slot concat map/tree pipeline Applied The item-slot stream's schema, order, outer state, and no-value checkpoints remain intact.
Proven empty arm carried through a supported generic deferred-list consumer Applied Empty rows and payloads are omitted while presence, null/no-value state, and packet arm identities remain.
Concat tree with a sole surviving stream Applied The normalized stream is forwarded in its original order and presence/null context.
Runtime-stage-created empty stream or unproven deferred-list path Not applied The existing path retains runtime validation and any state the static proof cannot establish.
Local backend Not applicable This is a PostgreSQL SQL-lowering choice.

Reproduction

The initial ordinary and item-slot baseline is 68b8821. The generic deferred-list follow-up baseline is 85d1799. The candidates are the source trees containing the corresponding changes. Go 1.27.1 ran on Darwin arm64 with PostgreSQL 18.6.

The focused unit and PostgreSQL parity tests are:

GOTOOLCHAIN=go1.27.1 go test ./backends/sql -run '^TestEmptyConcat' -count=1
HFF_POSTGRES_CONTAINER=<container> GOTOOLCHAIN=go1.27.1 go test -tags=integration ./backends/sql \
  -run '^TestPostgreSQLEmpty.*Concat' -count=1
GOTOOLCHAIN=go1.27.1 go test ./backends/sql \
  -run '^TestEmptyDeferred' -count=1
HFF_POSTGRES_CONTAINER=<container> GOTOOLCHAIN=go1.27.1 go test -tags=integration ./backends/sql \
  -run '^TestPostgreSQLEmptyDeferredConcatMatchLocal$' -count=1

For the initial ordinary and item-slot scope, the final PostgreSQL run recorded 54,392 query/record results across 192 groups, including 7,396 ordinary concat outputs and 128 accepted dynamic concat outputs from the new empty-arm tests. Four unsupported String-overload cases were separately confirmed to fail during Build; omitted-OID parameter checks also passed. The ordinary cases include empty literal arms and a single surviving stream. Item-pipeline cases exercise empty streams in map/filter and concat trees, null/no-value precedence, outer state, and callback validation.

Raw SQL-size and plain-plan outputs are retained outside the repository:

  • /private/tmp/hff-empty-concat-{before,final}.log
  • /private/tmp/hff-empty-concat-pipeline-{before,after}.log
  • /private/tmp/hff-empty-concat-{before,final}-explain.log

Results

Workload Baseline WHERE bytes Candidate WHERE bytes Change Uncertainty
Ordinary integer-list concat, sum() observer 3,412 2,311 −1,101 bytes Generated SQL size only
Ordinary integer-list concat, JSON observer 3,507 2,364 −1,143 bytes Generated SQL size only
Ordinary integer-list concat, any() observer 3,474 2,373 −1,101 bytes Generated SQL size only
Item-slot pipeline map/type observation 4,669 4,010 −659 bytes Generated SQL size only
Item-slot pipeline any() 4,290 3,529 −761 bytes Generated SQL size only
Nested item-slot pipeline with unused callback 6,253 5,048 −1,205 bytes Generated SQL size only

For the first ordinary sum query, plain EXPLAIN changed from 34 to 23 plan lines: Aggregate nodes went from 6 to 3, Function Scan nodes from 7 to 3, and the Append node was removed. This is plan-shape evidence only. No EXPLAIN ANALYZE, execution timing, allocation, or speed measurement was collected. Eleven ordinary SQL and argument controls remained byte-identical.

The generic deferred-list follow-up passed 20,812 outputs for 121 sources across 85 records plus one omitted-OID execution per source in both JSON formats. Its selected regression run logged 211,426 query/record outputs across 598 groups. That earlier run excludes a sort-parity counterexample reproduced on both baseline and candidate. Identity-gated sorting addresses that case; see the PostgreSQL JSON sorting guide. The recorded follow-up counts remain scoped to the selected run. For four representative sources, the generated WHERE sizes were:

Workload Baseline WHERE bytes Candidate WHERE bytes Change Uncertainty
Deferred map, type observer 5,535 5,090 −445 bytes Generated SQL size only
Deferred map, any() 5,053 4,284 −769 bytes Generated SQL size only
Deferred foldl() 6,871 6,642 −229 bytes Generated SQL size only
Deferred projected sum 7,713 7,025 −688 bytes Generated SQL size only

Across those four cases, statically false predicates fell from 2/2/1/1 to zero. UNION ALL counts changed from 2/1/2/1 to 0/0/1/0; the remaining recursive-fold union is required. The additional SQL-shape logs are retained at /private/tmp/hff-empty-deferred-{before,after}.log; the four SQL files on each side are /private/tmp/hff-empty-deferred-{before,after}-{0..3}.sql.

For the generic deferred-list map-type query, plain EXPLAIN changed from 78 to 77 lines, while Aggregate (4), Result (24), and Append (0) counts were unchanged. PostgreSQL already pruned the dead rows in that plan. This is a plan-shape observation, not evidence that the query executes less work.

Correctness and tradeoffs

The validation pass can add Build work, while the emitted query avoids SQL for proven empty arms. The mechanism preserves source/receiver validation and checks that discarded callback expressions do not leave unused query parameters. Tests cover literal and statically derived empty lists, all-empty concat identities, single-stream forwarding, diagnostics order, cancellation, null and no-value behavior, packet variants, fold successors, and JSON/JSONB parity. Runtime-created empty streams and generic deferred-list paths without a proof retain the existing lowering.

Decision and limits

Use this optimization only when the current lowering proves an arm empty without suppressing source evaluation, validation, or state. Preserve the generic path when a deferred-list consumer cannot carry that proof. SQL-size and plain-plan observations do not establish runtime speed, and no claim is made for workloads with different schemas or data.

Explore HFF
Search documentation

Search the language specification and guides.