@@ -219,13 +219,15 @@ const ORDERED_INT_DOMAINS: &[DomainSpec] = &[
219219 } ,
220220] ;
221221
222- /// Equality-only domains: storage (no terms) + `_eq` (hm). Used by scalar types
223- /// that can hash for equality but cannot (yet) be ordered. `timestamptz` is the
224- /// first such type: cipherstash encrypts `Plaintext::Timestamp` at native
225- /// 12-block ORE width, but EQL's only ORE comparator
226- /// (`eql_v2.compare_ore_block_u64_8_256_term`) is hardcoded to 8 blocks, so an
227- /// ordered domain would silently mis-order. Ordering is deferred until a
228- /// wide-ORE (12-block) term exists.
222+ /// Equality-only domains: storage (no terms) + `_eq` (hm). The canonical shape
223+ /// for a scalar type that can hash for equality but is not ORE-orderable.
224+ /// **Currently unused:** `timestamptz` (the previous sole user) was promoted to
225+ /// the ordered shape once `eql_v3.compare_ore_block_256_term` generalized to N
226+ /// blocks and could order its native 12-block ORE width. Retained — and still
227+ /// validated as a known-valid shape by `every_type_uses_a_known_domain_shape` —
228+ /// so a future non-orderable scalar (e.g. a hash-only type) can reuse it without
229+ /// reconstructing the shape.
230+ #[ allow( dead_code) ]
229231const EQ_ONLY_DOMAINS : & [ DomainSpec ] = & [
230232 DomainSpec {
231233 suffix : "" ,
@@ -285,6 +287,17 @@ const TIMESTAMPTZ_FIXTURES: &[Fixture] = fixtures!(timestamptz;
285287 "2012-06-30T11:59:59Z" , "2016-03-15T08:15:30Z" , "2020-10-21T14:45:00Z" ,
286288 "2024-02-29T17:30:45Z" , "2038-01-19T03:14:07Z" , "2099-12-31T23:59:59Z" ) ;
287289
290+ /// `numeric` fixture plaintexts — distinct by `Decimal` value, spanning sign,
291+ /// magnitude, and scale, and including `0` plus the min/max pivots
292+ /// (`-1000000000000` / `1000000000000`). They mirror `ore-rs`'s own
293+ /// order-pinning vectors so the 14-block ORE edges (sign + high/low blocks) are
294+ /// exercised. Each literal is distinct by parsed value (no `"1"`/`"1.0"`
295+ /// aliasing) — the harness `numeric_fixtures_distinct_by_value` guard enforces
296+ /// this, since the zero-dep catalog only dedupes by literal string.
297+ const NUMERIC_FIXTURES : & [ Fixture ] = fixtures ! ( numeric;
298+ "-1000000000000" , "-1000000" , "-1.001" , "-1" , "-0.5" , "-0.001" ,
299+ "0" , "0.001" , "0.5" , "0.999999999" , "1" , "1.001" , "1000000" , "1000000000000" ) ;
300+
288301const INT4 : ScalarSpec = ScalarSpec {
289302 token : "int4" ,
290303 kind : ScalarKind :: I32 ,
@@ -321,27 +334,42 @@ pub const DATE: ScalarSpec = ScalarSpec {
321334 fixtures : DATE_FIXTURES ,
322335} ;
323336
324- /// `timestamptz` — an **equality-only** (UTC-normalized) non-integer scalar.
325- /// Uses `EQ_ONLY_DOMAINS` (storage + `_eq`) rather than the four-domain ordered
326- /// shape: cipherstash encrypts `Plaintext::Timestamp` at native 12-block ORE
327- /// width, but EQL's only ORE comparator
328- /// (`eql_v2.compare_ore_block_u64_8_256_term`) is hardcoded to 8 blocks, so an
329- /// ordered timestamptz domain would silently mis-order. Ordering is deferred to
330- /// a future PR that adds a wide-ORE (12-block) term. The three "pivot" fixture
331- /// values are retained as equality pivots; the kind stays ordered-shaped
332- /// (carries a rust type, no i128 range) so the harness can parse them.
337+ /// `timestamptz` — an **ordered**, UTC-normalized non-integer scalar. Uses the
338+ /// four-domain ordered shape (storage, `_eq`, `_ord`, `_ord_ore`): cipherstash
339+ /// encrypts `Plaintext::Timestamp` at native 12-block ORE width, which the
340+ /// generalized `eql_v3.compare_ore_block_256_term` comparator orders correctly.
341+ /// Values are UTC-normalized (cipherstash has no tz-preserving type) and encrypt
342+ /// under the `timestamp` cast.
333343///
334344/// Public (like `DATE`) because the SQLx harness reads `TIMESTAMPTZ.fixtures`
335- /// directly to parse the RFC3339 strings into `chrono::DateTime<Utc>` at
336- /// runtime — there is no `TIMESTAMPTZ_VALUES` const (chrono is not
337- /// `const`-friendly and `eql-scalars` stays zero-dep).
345+ /// directly to parse the RFC3339 strings into `chrono::DateTime<Utc>` at runtime
346+ /// (no `TIMESTAMPTZ_VALUES` const; `eql-scalars` stays zero-dep).
338347pub const TIMESTAMPTZ : ScalarSpec = ScalarSpec {
339348 token : "timestamptz" ,
340349 kind : ScalarKind :: Timestamptz ,
341- domains : EQ_ONLY_DOMAINS ,
350+ domains : ORDERED_INT_DOMAINS ,
342351 fixtures : TIMESTAMPTZ_FIXTURES ,
343352} ;
344353
354+ /// `numeric` — an **ordered** non-integer scalar backed by
355+ /// `rust_decimal::Decimal`. Uses the four-domain ordered shape: cipherstash
356+ /// encrypts `Plaintext::Decimal` at native 14-block ORE width, which the
357+ /// generalized `eql_v3.compare_ore_block_256_term` comparator orders correctly.
358+ /// `numeric_value` returns `None` (no i128 range); ordering is supplied by the
359+ /// harness `Decimal: Ord`, which `ore-rs` guarantees agrees with the ciphertext
360+ /// order (equivalent scales collide, like `Decimal`'s own `Ord`).
361+ ///
362+ /// Public (like `DATE` / `TIMESTAMPTZ`) so the SQLx harness reads
363+ /// `NUMERIC.fixtures` directly to parse the decimal strings into
364+ /// `rust_decimal::Decimal` at runtime (the catalog stays zero-dep: no
365+ /// `rust_decimal`).
366+ pub const NUMERIC : ScalarSpec = ScalarSpec {
367+ token : "numeric" ,
368+ kind : ScalarKind :: Numeric ,
369+ domains : ORDERED_INT_DOMAINS ,
370+ fixtures : NUMERIC_FIXTURES ,
371+ } ;
372+
345373/// Domains for `text`: the ordered shape plus a `_match` domain backed by the
346374/// `Bloom` term (`@>`/`<@` containment). The ordered subset (`""`, `_eq`,
347375/// `_ord_ore`, `_ord`) is identical to `ORDERED_INT_DOMAINS`; `_match` is the
@@ -396,7 +424,7 @@ pub const TEXT: ScalarSpec = ScalarSpec {
396424
397425/// The scalar catalog — the single source of truth. Order is significant (it
398426/// drives generation order). New types are appended as their SQL surface lands.
399- pub const CATALOG : & [ ScalarSpec ] = & [ INT4 , INT2 , INT8 , DATE , TIMESTAMPTZ , TEXT ] ;
427+ pub const CATALOG : & [ ScalarSpec ] = & [ INT4 , INT2 , INT8 , DATE , TIMESTAMPTZ , NUMERIC , TEXT ] ;
400428
401429/// Materialise an integer scalar's fixtures into a typed `&'static` slice at
402430/// compile time. This is the **single-sourced** plaintext list the SQLx test
0 commit comments