Skip to content

feat(numeric): encrypted-domain scalar type for numeric/Decimal#242

Open
tobyhede wants to merge 22 commits into
eql_v3from
v3-domain-type-numeric
Open

feat(numeric): encrypted-domain scalar type for numeric/Decimal#242
tobyhede wants to merge 22 commits into
eql_v3from
v3-domain-type-numeric

Conversation

@tobyhede
Copy link
Copy Markdown
Contributor

@tobyhede tobyhede commented May 29, 2026

Adds the numeric encrypted-domain scalar (PostgreSQL numeric / rust_decimal::Decimal, EQL cast decimal) as an int4-style transposition. Codegen/matrix harness and hm/ore terms reused unchanged.

Equality, blockers, NULL propagation, CHECK constraints, and eq-index engagement pass.

Known-failing (intentional)

ORE ordering (range, ORDER BY, MIN/MAX) — ~75 matrix tests. Decimal encrypts to a 14-block ORE term, but eql_v2.compare_ore_block_u64_8_256_term hardcodes 8 blocks. Kept red on purpose.

Remaining work

Generalize the comparator to N blocks → #241. That is the only functional gap; it flips the failing tests green with no other numeric changes.

Summary by CodeRabbit

  • New Features

    • Added support for an encrypted numeric scalar domain type with equality and ordering operations (including MIN/MAX support).
  • Chores

    • Updated test dependencies to support decimal type handling.
  • Tests

    • Added comprehensive test suite and fixtures for the new numeric encrypted scalar type.

Review Change Stack

tobyhede added 22 commits May 28, 2026 15:15
DOMAIN_ROLE_PHRASES (templates.py) is the single source of truth for
role → human prose (storage → Storage-only, eq → Equality-only,
ord → Ordered). render_domain_block consulted it; render_functions_file
and render_operators_file interpolated role_for_terms() directly,
surfacing the raw codegen token (ord, eq, storage) in @brief doc-comment
lines that ship through Doxygen and `mise run docs:generate:markdown`.

Adds a role_phrase() helper in templates.py and routes all three
renderers through it. The eight reference files under
tests/codegen/reference/int4/ regenerate to match — the only diff is
prose ("ord domain of..." → "Ordered domain of...").

No behaviour change — doc-comment prose only.
Adds eql_v2.min(eql_v2_<T>_ord) / eql_v2.max(eql_v2_<T>_ord) and the
_ord_ore twin for every ord-capable variant, generated by codegen.
Removes the legacy eql_v2.min(eql_v2_encrypted) / eql_v2.max(eql_v2_encrypted)
aggregates in favour of the type-safe domain surface (U-009 in
docs/upgrading/v2.4.md).

Codegen (tasks/codegen/):
- render_aggregate / render_aggregates_file emit
  src/encrypted_domain/<T>/<T>_<variant>_aggregates.sql for Ord and
  OrdOre only; Storage/Eq skip silently. The ord-capability gate lives
  at the file-level renderer; render_aggregate itself doesn't re-assert
  the precondition.
- State function is LANGUAGE plpgsql IMMUTABLE STRICT — plpgsql so the
  body is opaque to the planner (inlinability rules don't apply to
  aggregate state functions); STRICT so PG seeds state with the first
  non-NULL value and skips subsequent NULLs.
- test_writer.py's misnamed-files guard adds _aggregates.sql to the
  allow-list.

SQL surface: deletes src/encrypted/aggregates.sql. The composite type's
aggregates are removed; callers re-type the column to the matching
_ord variant or cast at the call site.

Matrix coverage (tests/sqlx/src/matrix.rs):
- aggregate_outer: extremum identity (payload-text equality + ord_term
  secondary diagnostic), all-NULL → NULL, mixed NULL/non-NULL spanning
  the fixture extremes (i32::MIN / i32::MAX for int4), GROUP BY across
  two partitions, and a typecheck arm asserting MIN/MAX raise on
  non-ord variants (accepts 42883 undefined_function or 42725
  ambiguous_function).
- count_outer: COUNT(value), COUNT(payload::variant), and per-variant
  COUNT(DISTINCT extractor(value)) where the extractor is read from
  Variant::extractor_fn() — Eq → eql_v2.eq_term, Ord/OrdOre →
  eql_v2.ord_term, Storage skips (no extractor).
- 35 generated aggregate+count tests across the int4 family; 304 total
  in the encrypted_domain binary.

Extractor accessor (tests/sqlx/src/scalar_domains.rs): collapses
eq_index_expr() + ord_index_expr() into a single
Variant::extractor_fn() returning the function name without column
naming baked in. Callers compose "{fn}(value)" at the call site,
decoupling the accessor from any specific column-naming convention and
removing the implicit "Eq before Ord" invariant the prior or_else
chain encoded.

twin_sync (tests/sqlx/tests/encrypted_domain/scalars/int4.rs): third
assert_eq! pin keeps int4_ord_aggregates.sql and
int4_ord_ore_aggregates.sql byte-identical modulo type-name swap,
mirroring the existing functions/operators guards.

Reference docs (docs/reference/eql-functions.md,
docs/reference/sql-support.md): updated to advertise the per-domain
surface. CHANGELOG.md gains Added + Removed entries.
docs/upgrading/v2.4.md adds U-009 for the migration.
Rustfmt drift across the SQLx test crate (matrix.rs, scalar_domains.rs,
support.rs, scalars/int4.rs, constraint_tests.rs, eql_v2_int4.rs) that
CI surfaced via `mise run test:lint`. Plus a one-line markdown
linter fix on docs/upgrading/v2.4.md (fenced code block language hint).

Pure formatting — no behaviour change. Multi-line SQL string literals
get their continuation lines flush-left as rustfmt requires; the
trailing `\` in Rust string literals strips leading whitespace on the
next line, so the runtime SQL is byte-identical.
- Add tests/codegen/reference/README.md and per-file `-- REFERENCE:`
  marker identifying these as the hand-written parity baseline for
  tasks/codegen/; test_against_reference.py strips the marker so the
  byte-for-byte parity assertion still holds.
- Delete docs/upgrading/v2.4.md; revert CHANGELOG.md and README.md
  to origin/main — the int4 family is not shipping until the other
  scalar types land, so there is no release entry to make yet.
- Trim verbose "depends on build" exposition in tasks/docs/*.sh
  and tasks/docs/validate/*.sh to a single line.
- Trim Scope comment in tasks/test/splinter.sh.
- Remove tasks/__init__.py — Python 3 namespace packages already
  let `python -m tasks.codegen.generate` and pytest collection
  work without it.
Add workflow-level `permissions: contents: read` and `persist-credentials: false`
on every `actions/checkout` step. Addresses the unambiguous hardening items
from the PR #239 review; SHA-pinning intentionally deferred to keep parity
with the rest of the repo's workflows.
- `Variant::payload_required_keys` returns `impl Iterator` instead of
  allocating a fresh `Vec` per call (single iterating call site).
- `assert_raises` matches on the result so the `sql` format string
  only allocates when the query unexpectedly succeeds.
- `__scalar_matrix_blocker_case!` pins the shapes-array element type
  and uses `.into()` for literal sides so the array is consistent.
…ggregates

7086da1 removed eql_v2.min(eql_v2_encrypted) / eql_v2.max(eql_v2_encrypted)
when adding the per-domain aggregates, breaking every existing caller of
MIN/MAX on an eql_v2_encrypted column (documented public API). The two
surfaces accept disjoint argument types — the composite type vs jsonb-backed
domains — so they coexist without overload ambiguity, and the jsonb ->
eql_v2_encrypted cast is AS ASSIGNMENT (not IMPLICIT), so non-ord domain
variants still raise rather than resolving to the base aggregate.

Restore src/encrypted/aggregates.sql and the aggregate_minmax_data fixture
plus the six min/max tests (byte-identical to main); keep the per-domain
implementation untouched. Correct the reference docs that claimed the base
aggregates were removed and add a CHANGELOG entry noting they are retained.
The eql_v2_int4 generator emits 17 values (i32::MIN, 0, i32::MAX added
for ORE sign-bit boundary coverage), but eql_v2_int4_fixture_tests still
asserted the old 14-value layout — count, id range, plaintext oracle
(42 moved from id 9 to id 11), and distinct hm count. CI regenerates the
fixture every run, so these drifted assertions failed on all PG versions.
Reconcile the four reference docs with the current scalar-domain
generator. Document the per-domain MIN/MAX aggregate code path (missing
from the generator and spec docs): the `<domain>_aggregates.sql` file,
the `1 + 2D + A` file formula (int4 = 11 files), and the deliberate
pinned-search_path/plpgsql/STRICT exception for aggregate state
functions. Fix the operator count (176 = 44 per domain) and stale
templates.py/generate.py/splinter.sh line citations. Correct the
`eql_v2.selector()` signature (no public eql_v2_encrypted overload) and
add the encrypted-domain `eq_term`/`ord_term` extractors. Add a scalar
encrypted-domain capability section to the SQL support matrix.
…ql_type

Simplify the per-type scalar matrix invocation down to three inputs:

- Rename `fixture_script` -> `eql_type`. The value (e.g. "eql_v2_int4")
  is the EQL domain type name, which by convention drives the fixture
  table, the generated script file, and the SQLx `scripts(...)` ref.
  Naming the parameter after the type, not a derived artifact, is more
  correct. It stays an explicit string literal because sqlx parses
  `scripts(...)` as a token-level `LitStr`.
- Fix `fixture_path` as a const inside the convention wrappers instead
  of repeating it per invocation. Every scalar suite lives at
  tests/sqlx/tests/encrypted_domain/scalars/<T>.rs, so "../../../fixtures"
  is fixed there. `scalar_domain_matrix!` keeps the param as an override
  for direct callers at a different depth.
- Derive the comparison pivots (MIN, MAX, zero) from the scalar type via
  a new `Default` bound on `ScalarType`, so `ordered_numeric_matrix!` no
  longer takes a `pivots` list. The fixture must contain those three
  plaintext rows; `fetch_fixture_payload` fails loudly otherwise.

Test-only; no caller-observable behaviour change.
…e teeth

The scalar-domain matrix macro fails silently: a test it never generates
cannot fail, so a green build looks identical whether coverage is complete
or a whole category vanished (e.g. emptying ord_domains drops ~140 tests).
Nothing recorded proof the arms detect regressions.

Add three layers of verification scaffolding (no production SQL, codegen, or
matrix.rs changes):

- Name-inventory snapshot (enforced): test:matrix:inventory regenerates a
  committed list of the int4 matrix test names; the matrix-coverage CI job
  fails on drift. Any coverage change now shows as added/removed names in the
  PR diff. Pinned --no-default-features + LC_ALL=C sort for determinism;
  grep-scoped to scalars::int4 so other family tests never dirty it.
- Mutation negative-controls (enforced): 7 in-suite runtime-DDL tests, each
  applies one surgical mutation to the installed eql_v2 schema and asserts the
  property a matrix arm guards now flips (blocker, planner-metadata, ord/eq
  routing, supported-NULL, ord < correctness, ORDER BY). The #5/#7 split proves
  ORDER BY routes through ord_term, not <.
- cargo-expand snapshot (non-blocking): test:matrix:expand + macro-expand-eql.yml
  capture the expanded matrix bodies on a pinned nightly as a drift detector,
  isolated off the PR path.
…gories

The correctness, cross_shape and supported_null categories each carried
their own outer/mid/inner recursion plumbing — byte-identical between
correctness and cross_shape — whose only job was to fan out over the
domain/op/pivot dimension lists and forward to a per-category leaf macro.

Replace the eight plumbing macros with two generic drivers keyed on a
`case = $case:ident` callback: __scalar_matrix_dxop_* (domain × op × pivot)
and __scalar_matrix_dxo_* (domain × op). macro_rules cannot cross-product
independent lists in a single repetition, so the per-dimension recursion is
retained; only the duplication is removed.

Behaviour-preserving: the inventory snapshot is unchanged and the only
expand-snapshot delta is the start_line/end_line source metadata shifting
with the edited line numbers (zero changes to generated test bodies, names
or SQL).
assert_plan_uses_index had no callers — the three plan checks (scale,
ord-routes, index) each inline their own EXPLAIN against a transaction with
bespoke failure messages, so routing them through a shared &PgPool helper
would cost an executor-generic signature and a message param for ~4 lines of
overlap. Delete it and its re-export.

scalar_plaintexts_matching is only used internally by assert_scalar_plaintexts;
make it private and drop it from the public re-export.

ScalarDomainSpec::supports_eq/supports_ord are kept — family::support pins
them on the spec (not just on Variant), so they are part of the substrate's
tested surface, not dead code.
Sweep the `name = name` named format arguments (84 of them) where the
placeholder already implicitly captures the same in-scope local — keeping
the macro-var (`op = $op`), expression (`d = &spec.sql_domain`) and
differently-named (`fixture = fixture_table`) args. Also `.iter().copied()
.collect()` -> `.to_vec()` and `format!("'{}'", o)` -> `format!("'{o}'")`.

Purely cosmetic: the rendered strings are unchanged. The expand-snapshot
delta is only format_args positional renumbering (the explicit arg now
precedes the captured locals) plus line-number metadata; the inventory
snapshot is unchanged.
The ORDER BY arm sorts the fixture, which has no NULL rows, so NULLS
placement went untested. Since eql_v2.ord_term is STRICT, a NULL domain
value yields a NULL sort key whose position is governed by NULLS
FIRST/LAST — and a regression making ord_term non-STRICT would silently
let NULL rows interleave with real values.

Add a generic order_by_nulls arm: per ord domain × {ASC,DESC} ×
{NULLS FIRST,NULLS LAST}, build an isolated temp table mixing NULL-valued
rows with the fixture rows and assert the NULL rows land at the requested
end while non-NULL rows hold plaintext order (8 tests for int4; eq-only
types emit none).

Add mutation #8 (making_ord_term_non_strict_flips_order_by_nulls_arm) to
prove the arm has teeth on the NULL-placement dimension: dropping STRICT
on ord_term flips the property — a regression #5/#7 do not exercise.

Regenerate the int4 inventory (+8) and cargo-expand snapshots; the expand
diff also absorbs catch-up drift from the prior cartesian-driver refactors.
rustfmt is non-idempotent on indented string-literal continuations inside
the macro body (it oscillates and never reaches a fixpoint), so the new arm
failed `cargo fmt --check`. Match the existing order_by_case convention —
continuation lines flush-left at column 0 — which is rustfmt's stable
fixpoint. The `\` line continuation strips leading whitespace, so the SQL is
byte-identical; tests and the cargo-expand snapshot are unaffected.
Transpose the int4 reference to a numeric scalar (PostgreSQL numeric /
rust_decimal::Decimal, EQL cast "decimal"): manifest, EqlPlaintext +
ScalarType impls, fixture, and ordered_numeric_matrix! coverage. The
codegen/matrix harness and hm/ore terms are reused unchanged.

Equality (hm), blockers, NULL propagation, CHECK constraints, and
eq-index engagement all pass. ORE *ordering* (range, ORDER BY, MIN/MAX)
fails: Decimal encrypts to a 14-block ORE term, but
eql_v2.compare_ore_block_u64_8_256_term hardcodes 8 blocks. These ~75
failing tests are kept intentionally — they reflect the real state and
are the red tests for the comparator generalization tracked in #241.

- tasks/codegen/types/numeric.toml
- rust_decimal 1.42 (pinned to cipherstash-client's resolution) +
  sqlx "rust_decimal" feature
- EqlPlaintext/ScalarType for Decimal; eql_v2_numeric fixture (14 rows
  incl. Decimal::MIN/MAX/0); scalars::numeric matrix + twin_sync
- .gitignore, mise.toml fixture:generate, CHANGELOG
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85072bbe-f061-47db-9292-27813dcd4ac5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v3-domain-type-numeric

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/sqlx/src/scalar_domains.rs (1)

105-133: ⚡ Quick win

Keep fixture values in a single source of truth.

Line 105 notes these values must match another file exactly; maintaining two parallel constants is fragile. Consider exposing one canonical VALUES constant and reusing it here (or adding a direct equality test across both lists).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sqlx/src/scalar_domains.rs` around lines 105 - 133, FIXTURE_VALUES is
duplicated from the canonical list in eql_v2_numeric.rs; to avoid drift, import
or reuse the single source of truth instead of duplicating: locate the canonical
constant (e.g., VALUES or similar in eql_v2_numeric.rs), make it pub if needed,
then replace this FIXTURE_VALUES definition with a reference/alias to that
canonical constant (or alternatively add a unit test that asserts equality
between FIXTURE_VALUES and the canonical VALUES to catch divergence). Ensure you
update visibility (pub) and use the canonical symbol name when replacing
FIXTURE_VALUES so both tests and fixtures read from the same array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 25: Update the CHANGELOG.md entry for the `numeric` encrypted-domain
scalar to include the PR link suffix (append "(`#242`)") and soften the ordering
claim to reflect the outstanding comparator gap referenced by issue `#241`; revise
phrases that assert full ordered-comparison readiness (e.g., "The ORE encoding
is order-preserving across the entire `Decimal` range...") to state that
ordering is provided via the ORE term but a comparator implementation/edge-case
support is still outstanding (see issue `#241`), and ensure the entry follows the
project style: single dense paragraph, leading with the user-visible change, a
short "Why" explanation, then the PR link.

In `@tests/sqlx/Cargo.toml`:
- Around line 15-19: The dependency declaration for rust_decimal currently uses
a caret range, allowing non-exact upgrades; change the requirement for the
rust_decimal dependency (the rust_decimal entry in Cargo.toml) to an exact
version requirement (use the "=" exact-version syntax matching 1.42) so Cargo
will resolve exactly the same rust_decimal type that cipherstash-client expects.

In `@tests/sqlx/src/fixtures/eql_v2_numeric.rs`:
- Around line 46-47: The test enables an ORE index on numeric terms
(.with_index(IndexKind::Ore)) while the comparator still only supports 8 blocks
and the Decimal values are 14 blocks, causing ordered/range assertions to break;
remove or disable the ORE index for these Decimal/numeric fixtures (leave
.with_index(IndexKind::Unique) only) in eql_v2_numeric.rs (the builder chain
that calls .with_index(IndexKind::Ore)) until comparator generalization lands,
or alternatively gate the .with_index(IndexKind::Ore) behind a feature
flag/check that ensures comparator support for 14-block decimals.

In `@tests/sqlx/src/scalar_domains.rs`:
- Around line 103-135: The PR enables ordered numeric support for Decimal by
implementing ScalarType for Decimal and supplying a 14-entry FIXTURE_VALUES
array, but the ORE comparator is still hard-coded for 8 blocks causing
ordering/range failures; either remove/disable Decimal from the ordered scalar
matrix (undo the ordered-path registration for the Decimal ScalarType) or update
the comparator to support the 14-block Decimal layout (generalize the comparator
logic that assumes 8 blocks) so Decimal's FIXTURE_VALUES and ORE block count
match; reference the ScalarType impl for Decimal, the const FIXTURE_VALUES, and
the ORE comparator logic (currently fixed to 8 blocks) and align them (or gate
Decimal behind the existing comparator-generalization from issue `#241`) before
merging.

In `@tests/sqlx/tests/encrypted_domain/scalars/numeric.rs`:
- Around line 13-17: The test matrix invocation ordered_numeric_matrix! (suite =
numeric, scalar = Decimal, eql_type = "eql_v2_numeric") is enabling ORE-order
cases known to fail for Decimal (14-block terms vs 8-block comparator); gate
those failing ORE ordering assertions by either: (A) adding a conditional cfg
feature flag (e.g. behind a new feature like "decimal-ore-order-fix") and
skip/omit the ORE-order tests when the flag is not set, or (B) programmatically
ignore the specific ORE-order assertions when scalar == Decimal (or when
eql_type == "eql_v2_numeric") so the matrix does not register failures; update
the ordered_numeric_matrix! invocation or the macro expansion to detect Decimal
and apply the skip/ignore behavior, and leave a TODO referencing issue `#241` to
re-enable once the comparator supports Decimal block width.

---

Nitpick comments:
In `@tests/sqlx/src/scalar_domains.rs`:
- Around line 105-133: FIXTURE_VALUES is duplicated from the canonical list in
eql_v2_numeric.rs; to avoid drift, import or reuse the single source of truth
instead of duplicating: locate the canonical constant (e.g., VALUES or similar
in eql_v2_numeric.rs), make it pub if needed, then replace this FIXTURE_VALUES
definition with a reference/alias to that canonical constant (or alternatively
add a unit test that asserts equality between FIXTURE_VALUES and the canonical
VALUES to catch divergence). Ensure you update visibility (pub) and use the
canonical symbol name when replacing FIXTURE_VALUES so both tests and fixtures
read from the same array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c571564-007f-449b-a882-63e142dc0794

📥 Commits

Reviewing files that changed from the base of the PR and between b88cb50 and a746f22.

⛔ Files ignored due to path filters (1)
  • tests/sqlx/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .gitignore
  • CHANGELOG.md
  • mise.toml
  • tasks/codegen/types/numeric.toml
  • tests/sqlx/Cargo.toml
  • tests/sqlx/src/fixtures/eql_plaintext.rs
  • tests/sqlx/src/fixtures/eql_v2_numeric.rs
  • tests/sqlx/src/fixtures/mod.rs
  • tests/sqlx/src/scalar_domains.rs
  • tests/sqlx/tests/encrypted_domain/scalars/mod.rs
  • tests/sqlx/tests/encrypted_domain/scalars/numeric.rs

Comment thread CHANGELOG.md

### Added

- **`numeric` encrypted-domain scalar type.** The `eql_v2_numeric`, `eql_v2_numeric_eq`, `eql_v2_numeric_ord`, and `eql_v2_numeric_ord_ore` domains join `int4` as the second ordered scalar generated from the codegen materializer (`tasks/codegen/types/numeric.toml`), mapping PostgreSQL `numeric` (Rust `rust_decimal::Decimal`, EQL cast `decimal`). Equality (`=` / `<>`) routes through the `hm` HMAC term and ordering (`<` `<=` `>` `>=`, plus `MIN` / `MAX`) through the `ob` ORE block term — both index-backed via `eql_v2.eq_term` / `eql_v2.ord_term` with no decryption, exactly like `int4`. Why: callers storing encrypted decimal/monetary values can now use a type-safe domain column with the full searchable-comparison surface instead of the untyped `eql_v2_encrypted` composite. The ORE encoding is order-preserving across the entire `Decimal` range, including `Decimal::MIN` / `Decimal::MAX`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the PR link and avoid overstating ordering readiness.

This entry is missing the required PR link suffix, and it currently reads as fully supporting ordered comparisons even though PR #242 documents an outstanding ordering comparator gap (issue #241). Please append the PR link and tighten wording to reflect the current limitation until #241 lands.

As per coding guidelines, "When making a user-facing change, write the CHANGELOG.md entry by picking the right section (Added / Changed / Deprecated / Removed / Fixed / Security), leading with the user-visible fact, then a short 'Why' explanation, then a PR link in parentheses. Match existing entries' tone and density - a single dense paragraph per entry, not a bullet list".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 25, Update the CHANGELOG.md entry for the `numeric`
encrypted-domain scalar to include the PR link suffix (append "(`#242`)") and
soften the ordering claim to reflect the outstanding comparator gap referenced
by issue `#241`; revise phrases that assert full ordered-comparison readiness
(e.g., "The ORE encoding is order-preserving across the entire `Decimal`
range...") to state that ordering is provided via the ORE term but a comparator
implementation/edge-case support is still outstanding (see issue `#241`), and
ensure the entry follows the project style: single dense paragraph, leading with
the user-visible change, a short "Why" explanation, then the PR link.

Comment thread tests/sqlx/Cargo.toml
Comment on lines +15 to +19
# Pinned to the version cipherstash-client resolves (1.42) so the `Decimal`
# the fixture generator hands to `Plaintext::Decimal` is the *same* type sqlx
# decodes from NUMERIC — a second rust_decimal in the tree would make the two
# `Decimal`s distinct, incompatible types.
rust_decimal = "1.42"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la tests/sqlx/Cargo.toml
sed -n '1,60p' tests/sqlx/Cargo.toml | cat -n

Repository: cipherstash/encrypt-query-language

Length of output: 2384


🌐 Web query:

Cargo package version requirement syntax caret default and meaning of "=1.42.0"

💡 Result:

In Cargo, version requirements define the range of acceptable versions for a dependency [1]. Caret Requirements and Default Syntax Caret requirements (denoted by a leading ^) are the default version requirement strategy in Cargo [1][2]. When you specify a dependency version without any operator—for example, "1.2.3"—Cargo interprets it exactly as if you had written "^1.2.3" [1][3][4]. The caret requirement allows updates to any SemVer-compatible version [1][2]. A version is considered compatible if it does not modify the left-most non-zero digit in the major, minor, or patch grouping [1][3][5]. For instance, "^1.2.3" allows updates to any version greater than or equal to 1.2.3 but less than 2.0.0 (>=1.2.3, <2.0.0) [1][5]. Meaning of "=1.42.0" The "=1.42.0" syntax is an exact version requirement (also known as a requirement with a comparison operator) [1][3]. Using the equals operator (=) tells Cargo to accept only the exact version specified [1]. Unlike the default caret requirement, which allows a range of compatible versions, "=1.42.0" forces the resolver to use exactly version 1.42.0 of that crate [1]. This is typically used when you need to pin a dependency to a specific version to ensure reproducibility or to work around compatibility issues [1][4]. Summary - Default syntax (e.g., "1.2.3"): Equivalent to "^1.2.3"; allows compatible SemVer updates [1][5]. - Caret syntax ("^1.2.3"): The default strategy; allows compatible updates [1][2]. - Exact syntax ("=1.42.0"): Restricts the dependency to that specific version only [1].

Citations:


Pin rust_decimal with an exact Cargo requirement.

In Cargo.toml, rust_decimal = "1.42" defaults to a caret requirement (^1.42), meaning Cargo may resolve to any compatible 1.x version (e.g., 1.43+), not just 1.42.*.

Proposed fix
-rust_decimal = "1.42"
+rust_decimal = "=1.42.0"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Pinned to the version cipherstash-client resolves (1.42) so the `Decimal`
# the fixture generator hands to `Plaintext::Decimal` is the *same* type sqlx
# decodes from NUMERIC — a second rust_decimal in the tree would make the two
# `Decimal`s distinct, incompatible types.
rust_decimal = "1.42"
# Pinned to the version cipherstash-client resolves (1.42) so the `Decimal`
# the fixture generator hands to `Plaintext::Decimal` is the *same* type sqlx
# decodes from NUMERIC — a second rust_decimal in the tree would make the two
# `Decimal`s distinct, incompatible types.
rust_decimal = "=1.42.0"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sqlx/Cargo.toml` around lines 15 - 19, The dependency declaration for
rust_decimal currently uses a caret range, allowing non-exact upgrades; change
the requirement for the rust_decimal dependency (the rust_decimal entry in
Cargo.toml) to an exact version requirement (use the "=" exact-version syntax
matching 1.42) so Cargo will resolve exactly the same rust_decimal type that
cipherstash-client expects.

Comment on lines +46 to +47
.with_index(IndexKind::Unique)
.with_index(IndexKind::Ore)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

ORE index inclusion is ahead of comparator capability.

Line 47 enables ORE for numeric, but current comparator support is still fixed at 8 blocks while these Decimal terms are 14 blocks. Until comparator generalization lands, ordered/range numeric assertions remain broken.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sqlx/src/fixtures/eql_v2_numeric.rs` around lines 46 - 47, The test
enables an ORE index on numeric terms (.with_index(IndexKind::Ore)) while the
comparator still only supports 8 blocks and the Decimal values are 14 blocks,
causing ordered/range assertions to break; remove or disable the ORE index for
these Decimal/numeric fixtures (leave .with_index(IndexKind::Unique) only) in
eql_v2_numeric.rs (the builder chain that calls .with_index(IndexKind::Ore))
until comparator generalization lands, or alternatively gate the
.with_index(IndexKind::Ore) behind a feature flag/check that ensures comparator
support for 14-block decimals.

Comment on lines +103 to +135
impl ScalarType for Decimal {
const PG_TYPE: &'static str = "numeric";
/// Mirrors `eql_v2_numeric.rs` `VALUES` exactly — the two lists must stay
/// in agreement: the matrix asserts against `FIXTURE_VALUES` while the DB
/// rows come from the fixture, and `MIN`/`MAX`/`ZERO` are fetched by
/// ciphertext from the fixture as the comparison pivots.
///
/// Spans the signed extremes (`Decimal::MIN`/`MAX`), zero
/// (`Default::default()`), negatives, fractional/high-scale values, and
/// ordinary magnitudes. Every value is distinct under `Decimal::cmp`, so
/// each produces a distinct order-preserving ORE block (equal bytes occur
/// only for `Decimal`-equal values such as `1` vs `1.0`, none of which
/// appear here). `Decimal::from_parts(lo, mid, hi, negative, scale)` is a
/// `const fn`, so fractional values are buildable in this `const` array
/// without `rust_decimal_macros`.
const FIXTURE_VALUES: &'static [Decimal] = &[
Decimal::MIN, // signed extreme (negative)
Decimal::from_parts(100, 0, 0, true, 0), // -100
Decimal::NEGATIVE_ONE, // -1
Decimal::from_parts(5, 0, 0, true, 1), // -0.5 (negative fractional)
Decimal::ZERO, // 0 (= Default; pivot)
Decimal::from_parts(1, 0, 0, false, 3), // 0.001 (small, high scale)
Decimal::ONE, // 1
Decimal::from_parts(15, 0, 0, false, 1), // 1.5 (fractional)
Decimal::TWO, // 2
Decimal::from_parts(314159, 0, 0, false, 5), // 3.14159 (high scale)
Decimal::TEN, // 10
Decimal::ONE_HUNDRED, // 100
Decimal::ONE_THOUSAND, // 1000
Decimal::MAX, // signed extreme (positive)
];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Ordered numeric path is enabled before comparator support exists.

Line 103 introduces Decimal into the ordered scalar matrix, but current PR context confirms ORE compare remains fixed to 8 blocks while Decimal uses 14. This leaves range/ordering behavior failing and keeps the feature incomplete until the comparator is generalized (issue #241).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sqlx/src/scalar_domains.rs` around lines 103 - 135, The PR enables
ordered numeric support for Decimal by implementing ScalarType for Decimal and
supplying a 14-entry FIXTURE_VALUES array, but the ORE comparator is still
hard-coded for 8 blocks causing ordering/range failures; either remove/disable
Decimal from the ordered scalar matrix (undo the ordered-path registration for
the Decimal ScalarType) or update the comparator to support the 14-block Decimal
layout (generalize the comparator logic that assumes 8 blocks) so Decimal's
FIXTURE_VALUES and ORE block count match; reference the ScalarType impl for
Decimal, the const FIXTURE_VALUES, and the ORE comparator logic (currently fixed
to 8 blocks) and align them (or gate Decimal behind the existing
comparator-generalization from issue `#241`) before merging.

Comment on lines +13 to +17
ordered_numeric_matrix! {
suite = numeric,
scalar = Decimal,
eql_type = "eql_v2_numeric",
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Known failing ORE-order cases should be gated until comparator supports Decimal block width.

This matrix currently enables cases that are already known to fail for Decimal (14-block terms vs 8-block comparator), which keeps the suite red. Please gate/ignore the affected ORE ordering assertions in this PR (or split them behind a feature/expected-fail path) and re-enable once issue #241 lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sqlx/tests/encrypted_domain/scalars/numeric.rs` around lines 13 - 17,
The test matrix invocation ordered_numeric_matrix! (suite = numeric, scalar =
Decimal, eql_type = "eql_v2_numeric") is enabling ORE-order cases known to fail
for Decimal (14-block terms vs 8-block comparator); gate those failing ORE
ordering assertions by either: (A) adding a conditional cfg feature flag (e.g.
behind a new feature like "decimal-ore-order-fix") and skip/omit the ORE-order
tests when the flag is not set, or (B) programmatically ignore the specific
ORE-order assertions when scalar == Decimal (or when eql_type ==
"eql_v2_numeric") so the matrix does not register failures; update the
ordered_numeric_matrix! invocation or the macro expansion to detect Decimal and
apply the skip/ignore behavior, and leave a TODO referencing issue `#241` to
re-enable once the comparator supports Decimal block width.

@tobyhede tobyhede force-pushed the v3-domain-type-int4 branch 2 times, most recently from 2c54c4c to e26e3a5 Compare June 1, 2026 02:47
Base automatically changed from v3-domain-type-int4 to eql_v3 June 1, 2026 04:52
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.

1 participant