028. Prune schema-impossible numeric arithmetic branches

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

Problem and mechanism

Dynamic JSON arithmetic must validate the stored representation and honor the compiled receiver type before evaluating an operation. The general lowering previously built both Int and Float arithmetic branches even when that type excluded one family. The translator now intersects the compiled type with the stored schema's possible numeric families after the existing representation and metadata checks, then emits only reachable arithmetic, negation, or round()/ceil()/floor() branches.

This changes SQL construction, not value semantics. Int overflow, division results, and required Int-to-Float promotion remain intact. Float work still follows PostgreSQL's native Float Profile; this change does not claim that its results match the local backend at underflow boundaries. Null and no-value behavior, callback scope, and typed SQL-null carriers are preserved. Mixed Int/Float operations retain the required numeric paths; mixed comparisons remain exact, while mixed arithmetic follows the PostgreSQL Float Profile.

Applicability

Compiled value type and operation Status Reason
String|Int|Null arithmetic, negation, or numeric rounding Applied Only the Int input family is reachable after stored-value checks; operation-specific results such as Int division to Float remain.
String|Float|Null arithmetic, negation, or numeric rounding Partial The unreachable Int input branch is removed; Float uses its existing general PostgreSQL kernel.
String|Int|Float|Null arithmetic or rounding Partial Branches excluded by the compiled type or operation are removed; reachable numeric families remain.
Unguarded Any arithmetic or rounding Not applied Both numeric input families may occur, so runtime dispatch remains necessary.
Mixed Int/Float operands Applied Required promotion and mixed-family behavior remain; only impossible result branches are removed.
Direct native scalar and nonnumeric controls Not applicable These do not use dynamic JSON numeric dispatch.

The same narrowing applies inside supported callbacks and when a compiled type guard restricts a dynamic value. A guard does not bypass stored-value representability or JSONB metadata checks. Int division still returns Float, even when its input operands are Int.

Reproduction

The baseline is commit 46ddd93. The shape harness used Go 1.27.1 and PostgreSQL 18.6 with extra_float_digits=1, the PostgreSQL native Float profile. Its engine schema declared i as String|Int|Null, f as String|Float|Null, both as their union, data as Any, items as List<String|Int|Null>, guard as nullable Int, and name as String. Paths i, f, both, data, and items were bound to matching native JSON columns on records; guard and name were bound to native columns. All builds used the PostgreSQL dialect. Durable semantic fixtures are numeric_branch_pruning_test.go and numeric_branch_pruning_integration_test.go. The generated SQL-shape table came from a separate temporary local harness; its source and raw SQL/plans are not included in the repository.

Representative filters are:

i+2>0
f+2.0>0
i/2>0
items:any(x=>x+2>0)
(data:[string|int]+2):type="int"
i+f>0

Focused validation included:

GOTOOLCHAIN=go1.27.1 go test ./backends/sql -run '^TestNumericBranch' -count=1
HFF_POSTGRES_CONTAINER=<container> GOTOOLCHAIN=go1.27.1 go test -tags=integration ./backends/sql -run '^TestPostgreSQLNumericBranchPruning' -count=1

The JSON/JSONB parity matrix covered 4,064 local/PostgreSQL comparisons against both the pinned baseline and implementation, including nullable and wrong-category values, type guards, callback scopes, mixed numeric values, signed zero, and Int boundaries. Thirty-six extension observations checked that SQL null carriers retained their expected types. Numeric rounding, native Float, underflow, and schema-dispatch controls also passed. No timing or benchmark was collected.

Results

Filter Baseline WHERE bytes / barriers Candidate bytes / barriers
i+2>0 5,277 / 14 1,065 / 6
(data:[string|int]+2):type="int" 5,981 / 17 1,761 / 9
items:any(x=>x+2>0) 5,797 / 18 1,577 / 10
i/2>0 7,211 / 15 2,811 / 7
f+2.0>0 5,994 / 16 5,454 / 15
i+f>0 6,672 / 18 5,922 / 17
(both+2):type="int" control 5,944 / 15 5,922 / 15
(data+2):type="int" Any receiver with a result type test 5,944 / 15 5,922 / 15
name="Alice" control 55 / 0 55 / 0

The String|Float example removes its unreachable Int input branch but keeps the general Float kernel. The Int/Float and unguarded Any controls retain both numeric families. The type-guarded Any example narrows to Int. Int division still has a Float result. Counts describe generated SQL shape, not runtime cost.

Plain EXPLAIN (COSTS OFF) on generated fixtures showed fewer Result and Subquery Scan nodes for representative Int-only arithmetic and callback filters. The Float-only filter retained its Float subquery structure. These are fixture-specific plan observations; there was no EXPLAIN ANALYZE or execution-time measurement.

Correctness and tradeoffs

Pruning uses compiled possible types only after the existing storage representability and metadata checks. It does not inspect stored rows at Build time or accept invalid stored values. Any keeps both numeric families; an Int/Float union keeps both when both are reachable. Mixed arithmetic still promotes Int as required, and Int division remains Float. Mixed comparisons retain their exact represented-value behavior. The native Float Profile, including its backend-specific underflow behavior, is unchanged.

Decision and limits

Emit only numeric arithmetic and rounding branches permitted by the compiled type and operation. Keep the general lowering where the value remains unconstrained or both families are reachable. These SQL-shape reductions do not establish execution speed or eliminate the general Float kernel.

Explore HFF
Search documentation

Search the language specification and guides.