Skip to main content

riffdb_types/
exact_text.rs

1//! Frozen exact text-match semantics (ADR-0131).
2
3use std::error::Error;
4use std::fmt;
5
6/// Semantic exact-text profile version.
7pub const EXACT_TEXT_PROFILE_VERSION_V1: u16 = 1;
8/// Frozen opaque schema identity for canonical `RXTS` provider checkpoints.
9pub const EXACT_TEXT_PROVIDER_STATE_SCHEMA_HASH_V1: [u8; 32] = [
10    0x9b, 0x8a, 0x2f, 0x7e, 0xd9, 0xfd, 0xf1, 0xc5, 0xd9, 0x68, 0x8f, 0xbd, 0x39, 0xad, 0x3d, 0x86,
11    0x20, 0x70, 0xa7, 0x17, 0x74, 0xdc, 0x77, 0x4d, 0x0c, 0xe0, 0x0e, 0xdb, 0x69, 0x1a, 0x1e, 0x40,
12];
13/// Opaque schema identity for activated V2 checkpoints with typed output rows.
14pub const EXACT_TEXT_PROVIDER_STATE_SCHEMA_HASH_V2: [u8; 32] = [
15    0x58, 0xa4, 0x1e, 0x51, 0x2c, 0x04, 0xef, 0x93, 0xe7, 0xc1, 0xfd, 0xb2, 0xbf, 0xfb, 0x2d, 0x44,
16    0xbb, 0x54, 0xfc, 0x76, 0xac, 0x61, 0xab, 0x8e, 0x47, 0x93, 0xf1, 0xf0, 0x1d, 0x2b, 0x2f, 0x8a,
17];
18/// Opaque schema identity for V3 checkpoints with one compiler-bound typed filter.
19pub const EXACT_TEXT_PROVIDER_STATE_SCHEMA_HASH_V3: [u8; 32] = [
20    0xda, 0x13, 0x39, 0xc5, 0x30, 0x4c, 0x8d, 0x9d, 0x90, 0x58, 0x74, 0x74, 0x1d, 0xf8, 0xd8, 0xa2,
21    0x9a, 0xe8, 0xa6, 0x14, 0xbd, 0x11, 0xe5, 0x53, 0xc7, 0xd9, 0xfc, 0xd0, 0x06, 0x8f, 0xcc, 0x4b,
22];
23/// Opaque schema identity for ADR-0134 indexed predicate/order checkpoints.
24pub const EXACT_PREDICATE_PROVIDER_STATE_SCHEMA_HASH_V4: [u8; 32] = [
25    0x60, 0xe6, 0x99, 0xf7, 0x7d, 0x47, 0x2b, 0xb0, 0xc1, 0xa7, 0xc7, 0x78, 0x48, 0x8a, 0x6e, 0xf8,
26    0x9e, 0xe7, 0x7e, 0x04, 0x83, 0xdc, 0x62, 0x5e, 0x85, 0xdd, 0xc4, 0x0b, 0x88, 0xc2, 0x0a, 0x28,
27];
28/// Opaque schema identity for ADR-0145 nullable exact-order checkpoints.
29pub const EXACT_PREDICATE_PROVIDER_STATE_SCHEMA_HASH_V5: [u8; 32] = [
30    0x7e, 0x64, 0x5e, 0x51, 0x89, 0x2e, 0x83, 0xca, 0x4e, 0x05, 0xaa, 0x24, 0xa4, 0x7a, 0xa9, 0x21,
31    0xc4, 0xc5, 0x86, 0xbc, 0xb9, 0x9d, 0x2a, 0x7e, 0x0b, 0x4a, 0x0f, 0x13, 0x39, 0x47, 0xc0, 0xa1,
32];
33/// Maximum indexed UTF-8 value bytes admitted by V1.
34pub const MAX_EXACT_TEXT_VALUE_BYTES_V1: usize = 256;
35/// Maximum canonical typed output bytes retained for one exact result row.
36pub const MAX_EXACT_TEXT_OUTPUT_ROW_BYTES_V1: usize = 64 * 1024;
37/// Maximum bound needle bytes admitted by V1.
38pub const MAX_EXACT_TEXT_NEEDLE_BYTES_V1: usize = 64;
39/// Maximum indexed rows in one policy/partition state.
40pub const MAX_EXACT_TEXT_ROWS_PER_PARTITION_V1: usize = 4_096;
41/// Maximum distinct nonempty character-boundary substrings generated by one row.
42pub const MAX_EXACT_TEXT_TERMS_PER_ROW_V1: usize =
43    MAX_EXACT_TEXT_VALUE_BYTES_V1 * (MAX_EXACT_TEXT_VALUE_BYTES_V1 + 1) / 2;
44
45/// Closed exact matching profiles.
46#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
47#[repr(u8)]
48pub enum ExactTextProfileV1 {
49    /// Compare exact UTF-8 byte sequences.
50    BinaryUtf8V1 = 1,
51    /// Compare under ADR-0172's frozen `unicode_fold_v1` transform: Unicode
52    /// 17.0.0 NFKC followed by full non-Turkic case folding. Values and needles
53    /// are folded by the same function, so matching is case- and
54    /// compatibility-insensitive while remaining exact and bytewise.
55    UnicodeFoldV1 = 2,
56}
57
58impl ExactTextProfileV1 {
59    /// Decodes a durable profile discriminant.
60    #[must_use]
61    pub const fn from_discriminant(value: u8) -> Option<Self> {
62        match value {
63            1 => Some(Self::BinaryUtf8V1),
64            2 => Some(Self::UnicodeFoldV1),
65            _ => None,
66        }
67    }
68
69    /// Applies this profile's transform to matching text.
70    ///
71    /// A stored value and a submitted needle must both pass through here, or
72    /// matching is not symmetric.
73    #[must_use]
74    pub fn matched_form(self, value: &str) -> String {
75        match self {
76            Self::BinaryUtf8V1 => value.to_owned(),
77            Self::UnicodeFoldV1 => crate::unicode_fold_v1(value),
78        }
79    }
80}
81
82/// Closed exact operators; no wildcard, regex, token, fuzzy, or score shape exists.
83#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84#[repr(u8)]
85pub enum ExactTextOperatorV1 {
86    /// Complete byte equality.
87    Equals = 1,
88    /// Needle is an exact leading byte sequence.
89    StartsWith = 2,
90    /// Needle is an exact trailing byte sequence.
91    EndsWith = 3,
92    /// Needle is an exact contiguous byte sequence.
93    Contains = 4,
94}
95
96/// Closed total orders supported by exact binary UTF-8 result sets.
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98#[repr(u8)]
99pub enum ExactTextOrderV1 {
100    /// Indexed value ascending, then canonical authoritative entity-key bytes ascending.
101    ValueAscEntityKey = 1,
102    /// Indexed value descending, then canonical authoritative entity-key bytes ascending.
103    ValueDescEntityKey = 2,
104}
105
106/// Input field state before exact matching.
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub enum ExactTextFieldValueV1<'a> {
109    /// Field is absent from an older/optional record.
110    Missing,
111    /// Field is explicitly null.
112    Null,
113    /// Field has a canonical UTF-8 value.
114    Value(&'a str),
115}
116
117/// Canonically bound nonempty exact-text parameter.
118#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
119pub struct ExactTextNeedleV1(String);
120
121impl ExactTextNeedleV1 {
122    /// Exact UTF-8 value, with no normalization or interpretation.
123    #[must_use]
124    pub fn as_str(&self) -> &str {
125        &self.0
126    }
127
128    /// Canonical profile/version/length/value binding.
129    #[must_use]
130    pub fn to_canonical_bytes(&self) -> Vec<u8> {
131        let bytes = self.0.as_bytes();
132        let mut output = Vec::with_capacity(8 + bytes.len());
133        output.extend_from_slice(b"RXTN");
134        output.extend_from_slice(&EXACT_TEXT_PROFILE_VERSION_V1.to_be_bytes());
135        output.extend_from_slice(&(bytes.len() as u16).to_be_bytes());
136        output.extend_from_slice(bytes);
137        output
138    }
139}
140
141impl ExactTextProfileV1 {
142    /// Binds one nonempty canonical UTF-8 needle under the fixed V1 maximum.
143    pub fn bind_needle(self, value: &str) -> Result<ExactTextNeedleV1, ExactTextBindErrorV1> {
144        if value.is_empty() {
145            return Err(ExactTextBindErrorV1::EmptyNeedle);
146        }
147        // ADR-0172: transform BEFORE bounding. A folded needle can be longer
148        // than its source, so a caller-supplied value checked pre-fold could
149        // expand past the limit the check was meant to enforce.
150        let bound = self.matched_form(value);
151        if bound.len() > MAX_EXACT_TEXT_NEEDLE_BYTES_V1 {
152            return Err(ExactTextBindErrorV1::NeedleTooLong);
153        }
154        // An empty fold of a nonempty needle would match everything; refuse it
155        // as the empty needle it effectively is.
156        if bound.is_empty() {
157            return Err(ExactTextBindErrorV1::EmptyNeedle);
158        }
159        Ok(ExactTextNeedleV1(bound))
160    }
161
162    /// Evaluates the frozen truth table. Missing and null never match.
163    #[must_use]
164    pub fn matches(
165        self,
166        operator: ExactTextOperatorV1,
167        value: ExactTextFieldValueV1<'_>,
168        needle: &ExactTextNeedleV1,
169    ) -> bool {
170        let ExactTextFieldValueV1::Value(value) = value else {
171            return false;
172        };
173        match operator {
174            ExactTextOperatorV1::Equals => value == needle.as_str(),
175            ExactTextOperatorV1::StartsWith => value.starts_with(needle.as_str()),
176            ExactTextOperatorV1::EndsWith => value.ends_with(needle.as_str()),
177            ExactTextOperatorV1::Contains => value.contains(needle.as_str()),
178        }
179    }
180}
181
182/// Typed exact-text bind failures.
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub enum ExactTextBindErrorV1 {
185    /// Empty needles are refused rather than matching every row.
186    EmptyNeedle,
187    /// Needle exceeds the compiler-frozen maximum.
188    NeedleTooLong,
189}
190
191impl fmt::Display for ExactTextBindErrorV1 {
192    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
193        formatter.write_str(match self {
194            Self::EmptyNeedle => "exact text needle must not be empty",
195            Self::NeedleTooLong => "exact text needle exceeds its compiled bound",
196        })
197    }
198}
199
200impl Error for ExactTextBindErrorV1 {}
201
202// Exhaustive non-durable ownership inventories for exact-provider admission.
203crate::inspect_owned_capacity_copy_v1!(ExactTextOperatorV1);
204
205crate::inspect_owned_capacity_copy_v1!(ExactTextOrderV1);
206
207// Exhaustive non-durable ownership inventories for retained provider data.
208crate::inspect_owned_capacity_copy_v1!(ExactTextProfileV1);