Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 0 additions & 48 deletions crates/eql-domains/src/fixture.rs

This file was deleted.

111 changes: 111 additions & 0 deletions crates/eql-domains/src/fixtures/fixture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! [`Fixture`] — the value-kind-tagged plaintext fixture value, its
//! `numeric_value` impl, and the `fixtures!` builder macro. Def + impl + macro
//! co-located here (the fixture-layer vocabulary).

use super::kind::ScalarKind;

/// Builds a `&[Fixture]`. The `int <ty>;` arm (a tt-muncher over `Min`/`Max`/
/// `Zero` and `N(<lit>)`) range-checks each literal against `<ty>` at compile
/// time via `const _RANGE_CHECK`, so out-of-range literals do not compile;
/// `text;`/`numeric;`/`jsonb;` wrap string literals. The reject case has no
/// in-crate test (macro isn't exported, no `trybuild` under zero-deps) — verify
/// by hand with a bad `N(..)`.
macro_rules! fixtures {
(int $t:ty; $($body:tt)*) => { fixtures!(@int $t; [] $($body)*) };
(@int $t:ty; [$($acc:expr),*]) => { &[$($acc),*] };
(@int $t:ty; [$($acc:expr),*] , $($r:tt)*) => { fixtures!(@int $t; [$($acc),*] $($r)*) };
(@int $t:ty; [$($acc:expr),*] Min $($r:tt)*) => { fixtures!(@int $t; [$($acc,)* Fixture::Min ] $($r)*) };
(@int $t:ty; [$($acc:expr),*] Max $($r:tt)*) => { fixtures!(@int $t; [$($acc,)* Fixture::Max ] $($r)*) };
(@int $t:ty; [$($acc:expr),*] Zero $($r:tt)*) => { fixtures!(@int $t; [$($acc,)* Fixture::Zero] $($r)*) };
(@int $t:ty; [$($acc:expr),*] N($v:literal) $($r:tt)*) => {
fixtures!(@int $t; [$($acc,)* Fixture::Int({ const _RANGE_CHECK: $t = $v; $v as i128 })] $($r)*)
};
(text; $($s:literal),* $(,)?) => { &[$(Fixture::Text($s)),*] };
(numeric; $($s:literal),* $(,)?) => { &[$(Fixture::Numeric($s)),*] };
(jsonb; $($s:literal),* $(,)?) => { &[$(Fixture::Jsonb($s)),*] };
(date; $($s:literal),* $(,)?) => { &[$(Fixture::Date($s)),*] };
(timestamptz; $($s:literal),* $(,)?) => { &[$(Fixture::Timestamptz($s)),*] };
(bool; $($b:literal),* $(,)?) => { &[$(Fixture::Bool($b)),*] };
(float; $($s:literal),* $(,)?) => { &[$(Fixture::Float($s)),*] };
}

/// A single fixture plaintext value, value-kind tagged: `Min`/`Max`/`Zero` are
/// the integer matrix pivots (resolved per-kind); `Int` is an integer literal;
/// `Numeric`/`Text`/`Jsonb` carry rendered string literals.
///
/// `fixtures!` range-checks `Int` literals at compile time, but a hand-built
/// `Fixture::Int(n)` is not — hence the runtime invariant tests. `Int(MIN)` and
/// `Min` resolve to the same numeric value via `numeric_value`.
/// (`numeric_value` is impl'd below.)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fixture {
Min,
Max,
Zero,
Int(i128),
Numeric(&'static str),
Text(&'static str),
Jsonb(&'static str),
/// An ISO-8601 date string (`"1970-01-01"`). The catalog stays zero-dep, so
/// the string is parsed into a `chrono::NaiveDate` in the SQLx harness, not
/// here. Distinct by literal, like the other string-backed fixtures.
Date(&'static str),
/// An RFC3339 UTC timestamp string (`"1970-01-01T00:00:00Z"`). The catalog
/// stays zero-dep, so the string is parsed into a `chrono::DateTime<Utc>` in
/// the SQLx harness, not here. Distinct by literal, like `Date`.
Timestamptz(&'static str),
/// A boolean plaintext (`true` / `false`). The `bool` scalar is
/// storage-only, so this fixture is encrypted (ciphertext only, no index
/// term) and never participates in a comparison pivot. Distinct by value.
Bool(bool),
/// An IEEE-754 float plaintext rendered as a string (`"0.5"`, `"-inf"`).
/// The catalog stays zero-dep, so the string is parsed into `f32`/`f64` in
/// the SQLx harness, not here. Distinct by parsed value (the harness
/// `float_fixtures_are_distinct_by_value` guard enforces this). NaN and
/// `-0.0` are deliberately excluded; `±Inf` (`"inf"`/`"-inf"`) ARE fixtures.
Float(&'static str),
}

impl Fixture {
/// The integer value for this fixture (`Min`/`Max` -> kind bounds, `Zero` ->
/// 0, `Int(n)` -> n), or `None` for the string-backed kinds. Does not
/// range-check; `every_fixture_value_is_within_kind_bounds` guards the bounds.
///
/// `const fn` so the `int_values!` materialiser can resolve a whole fixture
/// list into a typed `&'static` array at compile time.
pub const fn numeric_value(self, kind: ScalarKind) -> Option<i128> {
match self {
// `?` is not allowed in `const fn`, so match `as_bounded_int()`
// explicitly. A pivot on a non-integer kind resolves to `None`; the
// `pivot_sentinels_only_appear_with_integer_kinds` catalog test
// guarantees that combination never reaches a real `CATALOG` row.
Fixture::Min => match kind.as_bounded_int() {
Some(k) => Some(k.min_value()),
None => None,
},
Fixture::Max => match kind.as_bounded_int() {
Some(k) => Some(k.max_value()),
None => None,
},
Fixture::Zero => match kind.as_bounded_int() {
Some(_) => Some(0),
None => None,
},
// Gate the literal on the integer kinds too, mirroring the sentinels
// above: a hand-built `Int(n)` on a non-integer kind resolves to
// `None` rather than fabricating a number for a `Text`/`Date`/`Bool`
// kind that has no integer projection.
Fixture::Int(n) => match kind.as_bounded_int() {
Some(_) => Some(n),
None => None,
},
Fixture::Numeric(_)
| Fixture::Text(_)
| Fixture::Jsonb(_)
| Fixture::Date(_)
| Fixture::Timestamptz(_)
| Fixture::Float(_)
| Fixture::Bool(_) => None,
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,77 @@
//! Inherent impls for the scalar-kind vocabulary: [`BoundedIntKind`] (the total
//! accessors for fixed-width integer kinds) and [`ScalarKind`] (the native
//! scalar a domain maps onto). Definitions live in `lib.rs`.
//! [`ScalarKind`] / [`BoundedIntKind`] the native scalar a domain maps onto
//! plus the total fixed-width-integer accessors. Defs and impls co-located here
//! (the fixture-layer vocabulary).

use crate::{BoundedIntKind, ScalarKind};
/// The fixed-width integer kinds — exactly those scalar kinds with an `i128`
/// range and `MIN`/`MAX`/`Zero` sentinels. These accessors are **total**: every
/// variant answers every method. The non-integer kinds (`Numeric`/`Text`/
/// `Jsonb`/`Date`/`Timestamptz`/`Bool`/`F32`/`F64`) are simply not representable
/// here, so there is no partial function to panic — `ScalarKind::Date` cannot
/// call `min_symbol()` because `Date` is not a `BoundedIntKind`. Reach this type
/// from a `ScalarKind` via [`ScalarKind::as_bounded_int`]. (Accessors are impl'd
/// below.)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundedIntKind {
I16,
I32,
I64,
}

/// The native scalar a domain type maps onto. The integer kinds (`I16`/`I32`/
/// `I64`) carry i128 bounds; the non-integer kinds (`Numeric`/`Text`/`Jsonb`/
/// `Date`/`Timestamptz`/`Bool`/`F32`/`F64`) have no i128 range and string- or
/// bool-backed fixtures. All but `Jsonb` and `Bool` are still ORE-orderable —
/// `Jsonb` has no order, and `Bool` is storage-only (no comparison surface).
/// Capability layer only: `CATALOG` declares which kinds actually exist.
///
/// The bounded-numeric accessors live on the total [`BoundedIntKind`], reached
/// via [`ScalarKind::as_bounded_int`]; non-integer kinds have no such accessor,
/// so misuse is a compile error rather than a runtime panic. (Accessors are
/// impl'd below.)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarKind {
I16,
I32,
I64,
Numeric,
Text,
Jsonb,
/// Calendar date (`chrono::NaiveDate`). Ordered like the integer kinds via
/// ORE, but string-backed (ISO-8601) at the catalog layer and with no i128
/// range — so it is *not* `is_int()` and `as_bounded_int()` returns `None`
/// for it, like the other non-integer kinds. The bounded-numeric accessors
/// live on `BoundedIntKind`, which `Date` cannot be, so they are
/// unreachable for it by construction rather than by a runtime panic.
Date,
/// UTC timestamp (`chrono::DateTime<Utc>`). Ordered like the integer kinds
/// via ORE, but string-backed (RFC3339) at the catalog layer and with no
/// i128 range — so it is *not* `is_int()` and `as_bounded_int()` returns
/// `None` for it, like the other non-integer kinds. The bounded-numeric
/// accessors live on `BoundedIntKind`, which `Timestamptz` cannot be, so they
/// are unreachable for it by construction rather than by a runtime panic.
/// UTC-normalized: cipherstash has no tz-preserving type, so it maps to the
/// `timestamp` cast and the SQL `timestamp with time zone` plaintext type.
Timestamptz,
/// Boolean (`bool`). **Encryption-only / storage-only**: it carries no index
/// term and is *not* `is_int()`/`is_temporal()`/`is_text()`. A two-value
/// column has such low cardinality that any searchable index (even HMAC
/// equality) would trivially leak the plaintext distribution, so the catalog
/// gives `bool` a single term-less storage domain and no `_eq`/`_ord` — the
/// value is encrypted at rest and decrypted by the proxy, never searched
/// server-side. Like the other non-integer kinds, the bounded-numeric
/// accessors are unreachable for it by construction.
Bool,
/// 32-bit IEEE-754 binary float (`f32`, Postgres `real`/`float4`).
/// Ordered like the integer kinds via ORE, but with no i128 range
/// (`as_bounded_int()` returns `None`) and string-backed at the catalog
/// layer. Encrypts through the single f64 float crypto path
/// (`Plaintext::Float`) — the f32→f64 widening is exact and monotonic.
F32,
/// 64-bit IEEE-754 binary float (`f64`, Postgres `double precision`/
/// `float8`). The native width of the float crypto path (`F32` widens into
/// it); otherwise classified exactly like [`ScalarKind::F32`].
F64,
}

impl BoundedIntKind {
/// The Rust type name as it appears in generated source (e.g. `"i32"`).
Expand Down
23 changes: 23 additions & 0 deletions crates/eql-domains/src/fixtures/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! The fixture/test layer of the catalog: the native-scalar vocabulary
//! (`ScalarKind` / `BoundedIntKind`), the `Fixture` value tag + `fixtures!`
//! builder, the per-type `TypeFixtures` records + `FIXTURES` table, and the
//! materialised `*_VALUES` slices. One-way dependency: this module references
//! catalog rows (`crate::INT4` …); the catalog never references this module.
//!
//! `#[macro_use]` order matters: `fixture` (which defines `fixtures!`) must be
//! declared before `record` (which invokes it), without `#[macro_export]`.

#[macro_use]
pub(crate) mod fixture;
pub(crate) mod kind;
pub(crate) mod record;
pub(crate) mod values;

pub use fixture::Fixture;
pub use kind::{BoundedIntKind, ScalarKind};
pub use record::{
TypeFixtures, BOOL_FIXTURES, DATE_FIXTURES, FIXTURES, FLOAT4_FIXTURES, FLOAT8_FIXTURES,
INT2_FIXTURES, INT4_FIXTURES, INT8_FIXTURES, NUMERIC_FIXTURES, TEXT_FIXTURES,
TIMESTAMPTZ_FIXTURES,
};
pub use values::{INT2_VALUES, INT4_VALUES, INT8_VALUES, TEXT_VALUES};
Loading
Loading