[PATCH] Make NumericVar storage semantics explicit

First seen: 2026-05-10 14:12:15+00:00 · Messages: 1 · Participants: 1

Latest Update

2026-05-11 · opus 4.7

Analysis: Making NumericVar Storage Semantics Explicit

Context and Architectural Background

PostgreSQL's numeric type has two distinct representations that the code juggles constantly:

  1. The packed Numeric datum — the on-disk/varlena form with a compact header and digit array, subject to TOAST.
  2. NumericVar — the in-memory working representation used throughout numeric.c for arithmetic, comparison, aggregation, and formatting. It carries ndigits, weight, sign, dscale, and a pointer digits into a NumericDigit buffer, with buf tracking the actual allocation base (because digits may point past buf to leave room for a carry digit).

Over roughly two decades of incremental evolution, numeric.c accumulated several implicit conventions around how the digit storage behind a NumericVar is managed:

None of this is codified in the types. It lives in comments, function naming, and programmer discipline. The result is code that is correct but fragile: any patch touching numeric storage must reconstruct the invariants from scratch, and the rules around when free_var is safe, when a buffer may be written to, and when digits point into foreign memory are easy to get wrong.

What the Patch Proposes

Chenhui Mo proposes a refactor-only patch (explicitly disclaiming algorithmic or performance changes) that reifies these implicit contracts into an explicit state machine on NumericVar.

New fields on NumericVar

New wrapper: BufferedNumericVar

A stack-allocatable composite of a NumericVar plus a fixed-size inline digit array. It serves two roles:

  1. Provide writable inline storage that avoids heap allocation for common-sized operands.
  2. Act as a workspace to hold a detoasted Numeric whose digits are then borrowed by a NumericVar in NUMVAR_BORROW_INLINE state.

The author emphasizes BufferedNumericVar is not a new SQL-visible representation — it is purely an internal helper, analogous to how StringInfoData wraps a growable buffer.

Helper stratification

The patch reorganizes helpers into four layers with disciplined responsibilities:

  1. Raw storage helpers — only allocate/free NumericDigit arrays.
  2. Storage-state helpers — perform legal transitions (e.g., borrowed → owned-heap via copy-on-write, empty → owned-inline).
  3. Variable-level helpersinit, copy, acquire_buffer, release.
  4. Result preparation / finalization — handle alias-safe output placement for arithmetic, so add_var(a, b, a) patterns are expressed through the type system rather than ad-hoc set_var_from_var juggling.

Integration scope

Crucially, the patch does not merely add infrastructure. It rewires the top-level SQL entry points — cmp_numerics, numeric_hash, numeric_add, numeric_sub, numeric_mul, numeric_div — to drive the new model. The inner kernels (add_var, sub_var, mul_var, div_var) are not rewritten; their result-placement preludes and epilogues are migrated to the new helpers, leaving the numeric algorithms themselves untouched. Transcendental functions (sqrt, ln, log, power) are only adjusted minimally for compatibility.

Design Tradeoffs and Likely Points of Contention

This is a submission that will almost certainly draw heavy scrutiny from the numeric-code maintainers (Dean Rasheed, Tom Lane, Jeff Davis, historically Tomas Vondra). The following tensions are immediate:

1. Growth of NumericVar's footprint

Adding capacity, state, borrow_kind, and inline_buf fields enlarges sizeof(NumericVar). NumericVar is allocated extensively on the stack in aggregation and per-row paths. Even modest growth can matter — and the information is arguably redundant with what can be deduced from existing fields (buf == NULL already signals no owned allocation; comparing digits against buf tells you about the carry-digit offset). Reviewers will ask whether the explicitness is worth the bytes.

2. "Borrowed" semantics vs. existing conventions

The current code already has a working convention: if you didn't call alloc_var/set_var_from_* that allocates, you don't own the digits. Making this explicit via state fields is valuable for readers but adds a runtime invariant that every helper must maintain. Getting this wrong (e.g., a helper that forgets to transition state) silently regresses to the old implicit situation while adding overhead.

3. BufferedNumericVar vs. the existing inline-buffer pattern

numeric.c already has patterns like NumericDigit local_digits[NUMERIC_LOCAL_NMAX] plus manual pointer fix-up. Introducing a new wrapper type means churning every call site that used the ad-hoc pattern. Reviewers will weigh whether a typed wrapper is cleaner than the existing idiom, or merely different.

4. Refactor-only patches are a hard sell

PostgreSQL's community has a well-known bias against large refactors that do not fix a bug or enable a concrete follow-up. The author anticipates this by framing the work as enabling "future internal numeric optimizations," but without a concrete follow-up patch demonstrating the payoff, acceptance is historically unlikely. Compare to prior numeric work (e.g., Dean Rasheed's div_var_fast improvements, the numeric sum accumulator rewrite) — those landed because they delivered measurable wins, with cleanup as a side effect.

5. Regression test inflation

The patch adds tests up to NUMERIC(1000, 1000) and uses digest-based verification for large outputs. PostgreSQL regression tests avoid both very long expected output and non-deterministic/opaque verification. Digest-based checks specifically tend to be rejected because they obscure failures. This will likely be trimmed significantly in review.

6. Risk/reward for a hot path

cmp_numerics and numeric_hash are hit on every numeric comparison in sorts, hash joins, and hash aggregation. Any additional branching on state in these paths — even predictable branches — is a regression risk. The author claims no measurable regression, but reviewers will demand their own benchmarks on sort- and hash-heavy workloads.

Key Technical Insights

Likely Trajectory

As a solo first post from an apparently new contributor, proposing a large refactor to extremely mature code with no accompanying optimization payload, this patch faces an uphill path. Probable reviewer responses:

The refactor is internally coherent and the author clearly understands the code. Whether it lands will depend almost entirely on whether a follow-up demonstrates the payoff, and whether the core-team numeric maintainers (particularly Dean Rasheed) see the explicit state model as genuinely easier to maintain or as additional bookkeeping for no net gain.