Skip to main content

pharos_runtime_database/
index_values.rs

1//! Shared value semantics for materialized expression index keys.
2
3use std::collections::BTreeSet;
4
5use serde_json::Value;
6
7use crate::operations::{DatabaseError, Entity, FieldType, IndexKey};
8
9/// Maximum scalar-count expansion of Rust's supported Unicode lowercase mapping.
10/// The exhaustive Unicode unit proof detects any future toolchain expansion.
11pub const LOWERCASE_EXPANSION: u32 = 2;
12
13/// Lists source fields that participate in lowercase expression keys.
14pub fn lowercase_fields(entity: &Entity) -> BTreeSet<&str> {
15    entity
16        .conditional_indexes
17        .iter()
18        .flat_map(|index| &index.keys)
19        .filter_map(|key| match key {
20            IndexKey::Lowercase(name) => Some(name.as_str()),
21            IndexKey::Field(_) => None,
22        })
23        .collect()
24}
25
26/// Creates a bounded adapter-owned column name without app identifier collisions.
27pub fn lowercase_key(field: &str) -> String {
28    let digest = crate::operations::bytes_fingerprint(field.as_bytes());
29    format!("_pharos_lower_{}", &digest[..32])
30}
31
32/// Applies Rust's locale-independent Unicode lowercase mapping with contextual rules.
33/// Null remains null; ordinary index fields always retain their original values.
34pub fn lowercase_value(value: &Value) -> Result<Value, DatabaseError> {
35    match value {
36        Value::Null => Ok(Value::Null),
37        Value::String(text) => Ok(Value::String(text.to_lowercase())),
38        _ => Err(DatabaseError::InvalidInput),
39    }
40}
41
42/// Encodes canonical JSON as UTF-8 text for identical equality and ordering.
43pub fn json_comparison_value(value: &Value) -> Result<Value, DatabaseError> {
44    if value.is_null() {
45        return Ok(Value::Null);
46    }
47    serde_json::to_string(&crate::operations::canonical_json_value(value)?)
48        .map(Value::String)
49        .map_err(|_| DatabaseError::InvalidInput)
50}
51
52/// Creates a bounded adapter-owned JSON comparison column name.
53pub fn json_comparison_key(field: &str) -> String {
54    format!(
55        "_pharos_json_{}",
56        &crate::operations::bytes_fingerprint(field.as_bytes())[..32]
57    )
58}
59
60/// Includes JSON comparison keys and declared Unicode expression keys once each.
61pub fn derived_fields(entity: &Entity) -> BTreeSet<&str> {
62    let mut fields = lowercase_fields(entity);
63    fields.extend(
64        entity
65            .fields
66            .iter()
67            .filter(|field| field.kind == FieldType::Json)
68            .map(|field| field.name.as_str()),
69    );
70    fields
71}
72
73/// Resolves the protected physical key for one declared source field.
74pub fn derived_key(entity: &Entity, name: &str) -> Result<String, DatabaseError> {
75    let field = entity
76        .fields
77        .iter()
78        .find(|field| field.name == name)
79        .ok_or(DatabaseError::InvalidContract)?;
80    Ok(if field.kind == FieldType::Json {
81        json_comparison_key(name)
82    } else {
83        lowercase_key(name)
84    })
85}
86
87/// Computes protected values through a single backend-neutral implementation.
88pub fn derived_value(entity: &Entity, name: &str, value: &Value) -> Result<Value, DatabaseError> {
89    let field = entity
90        .fields
91        .iter()
92        .find(|field| field.name == name)
93        .ok_or(DatabaseError::InvalidContract)?;
94    if field.kind == FieldType::Json {
95        json_comparison_value(value)
96    } else {
97        lowercase_value(value)
98    }
99}
100
101/// Resolves an ordinary logical key to its exact physical comparison representation.
102pub fn comparison_key(entity: &Entity, name: &str) -> Result<String, DatabaseError> {
103    let field = entity
104        .fields
105        .iter()
106        .find(|field| field.name == name)
107        .ok_or(DatabaseError::InvalidContract)?;
108    Ok(if field.kind == FieldType::Json {
109        json_comparison_key(name)
110    } else {
111        name.to_owned()
112    })
113}
114
115/// Maps a JSON relationship to the canonical keys used for its native constraint.
116pub fn reference_keys(
117    field: &crate::operations::Field,
118) -> Result<(String, String, String), DatabaseError> {
119    let (parent, key) = field
120        .references
121        .as_ref()
122        .ok_or(DatabaseError::InvalidContract)?;
123    Ok(if field.kind == FieldType::Json {
124        (
125            json_comparison_key(&field.name),
126            parent.clone(),
127            json_comparison_key(key),
128        )
129    } else {
130        (field.name.clone(), parent.clone(), key.clone())
131    })
132}
133
134/// Checks the expression source type before any physical backend receives a schema.
135pub fn validate_lowercase_sources(entity: &Entity) -> Result<(), DatabaseError> {
136    for name in lowercase_fields(entity) {
137        if !entity.fields.iter().any(|field| {
138            field.name == name && field.kind == FieldType::Text && field.max_length.is_some()
139        }) {
140            return Err(DatabaseError::InvalidContract);
141        }
142    }
143    Ok(())
144}
145
146/// Enforces a portable full-key budget before any native index is created.
147pub fn validate_index_budget(entity: &Entity, keys: &[IndexKey]) -> Result<(), DatabaseError> {
148    let mut total = 0usize;
149    for key in keys {
150        let (IndexKey::Field(name) | IndexKey::Lowercase(name)) = key;
151        let field = entity
152            .fields
153            .iter()
154            .find(|field| field.name == *name)
155            .ok_or(DatabaseError::InvalidContract)?;
156        let bytes = match field.kind {
157            FieldType::Text => {
158                field.max_length.ok_or(DatabaseError::InvalidContract)? as usize
159                    * if matches!(key, IndexKey::Lowercase(_)) {
160                        4 * LOWERCASE_EXPANSION as usize
161                    } else {
162                        4
163                    }
164            }
165            FieldType::Json => field.max_length.ok_or(DatabaseError::InvalidContract)? as usize * 4,
166            FieldType::Binary => field.max_length.ok_or(DatabaseError::InvalidContract)? as usize,
167            FieldType::Timestamp => 120,
168            FieldType::Integer | FieldType::Decimal => 16,
169            FieldType::Boolean => 1,
170        };
171        total = total
172            .checked_add(bytes + 16)
173            .ok_or(DatabaseError::InvalidContract)?;
174    }
175    if total > 2400 {
176        return Err(DatabaseError::InvalidContract);
177    }
178    Ok(())
179}
180
181/// Bounds complete physical declarations, including protected and conditional keys.
182/// All adapters share these limits; oversized declarations fail before storage access.
183pub fn validate_entity_budget(entity: &Entity) -> Result<(), DatabaseError> {
184    let supporting_key = |name: &str| {
185        entity.identity.fields().first().copied() == Some(name)
186            || entity
187                .unique
188                .iter()
189                .chain(&entity.indexes)
190                .any(|key| key.first().is_some_and(|field| field == name))
191            || entity.conditional_indexes.iter().any(|index| {
192                index.filter.is_none()
193                    && matches!(index.keys.first(), Some(IndexKey::Field(field)) if field == name)
194            })
195    };
196    let implicit_references = entity
197        .fields
198        .iter()
199        .filter(|field| field.references.is_some() && !supporting_key(&field.name))
200        .count();
201    // Reserve both native document identity and logical identity indexes.
202    let indexes = 2
203        + entity.unique.len()
204        + entity.indexes.len()
205        + entity.conditional_indexes.len()
206        + implicit_references;
207    if indexes > 64 {
208        return Err(DatabaseError::InvalidContract);
209    }
210    let derived = derived_fields(entity);
211    let hidden = entity
212        .conditional_indexes
213        .iter()
214        .filter(|index| index.filter.is_some())
215        .map(|index| index.keys.len())
216        .sum::<usize>();
217    if entity.fields.len() + derived.len() + hidden > 1000 {
218        return Err(DatabaseError::InvalidContract);
219    }
220    let mut bytes = 128usize;
221    for field in &entity.fields {
222        bytes = bytes
223            .checked_add(inline_field_capacity(field.kind, field.max_length))
224            .ok_or(DatabaseError::InvalidContract)?;
225    }
226    for name in derived {
227        let field = crate::relational::derived_field(entity, name)?;
228        bytes = bytes
229            .checked_add(inline_field_capacity(field.kind, field.max_length))
230            .ok_or(DatabaseError::InvalidContract)?;
231    }
232    if bytes > 60_000 {
233        return Err(DatabaseError::InvalidContract);
234    }
235    Ok(())
236}
237
238/// Accounts for bounded inline values and external-value descriptors conservatively.
239fn inline_field_capacity(kind: FieldType, maximum: Option<u32>) -> usize {
240    match (kind, maximum) {
241        (FieldType::Text, Some(maximum @ 1..=4096)) => maximum as usize * 4 + 2,
242        (FieldType::Binary, Some(maximum @ 1..=65535)) => maximum as usize + 2,
243        (FieldType::Timestamp, _) => 122,
244        _ => 20,
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    /// Wide declarations and excessive indexes fail admission before any adapter executes DDL.
253    #[test]
254    fn complete_entity_capacity_is_portable() {
255        use crate::operations::{Field, Identity, Index};
256        let mut entity = Entity {
257            name: "capacity_record".to_owned(),
258            identity: Identity::AutoInteger("id".to_owned()),
259            fields: vec![Field {
260                name: "id".to_owned(),
261                kind: FieldType::Integer,
262                nullable: false,
263                max_length: None,
264                default: None,
265                references: None,
266                on_delete: None,
267                constraints: Vec::new(),
268            }],
269            unique: Vec::new(),
270            indexes: Vec::new(),
271            conditional_indexes: Vec::new(),
272        };
273        for index in 0..3 {
274            entity.fields.push(Field {
275                name: format!("text_{index}"),
276                kind: FieldType::Text,
277                nullable: true,
278                max_length: Some(4096),
279                default: None,
280                references: None,
281                on_delete: None,
282                constraints: Vec::new(),
283            });
284        }
285        assert_eq!(validate_entity_budget(&entity), Ok(()));
286        let mut extra = entity.fields[1].clone();
287        extra.name = "text_extra".to_owned();
288        entity.fields.push(extra);
289        assert_eq!(
290            validate_entity_budget(&entity),
291            Err(DatabaseError::InvalidContract)
292        );
293        entity.fields.truncate(1);
294        for index in 0..62 {
295            entity.conditional_indexes.push(Index {
296                name: format!("capacity_index_{index}"),
297                keys: vec![IndexKey::Field("id".to_owned())],
298                unique: false,
299                filter: None,
300            });
301        }
302        assert_eq!(validate_entity_budget(&entity), Ok(()));
303        entity.conditional_indexes.push(Index {
304            name: "capacity_index_extra".to_owned(),
305            keys: vec![IndexKey::Field("id".to_owned())],
306            unique: false,
307            filter: None,
308        });
309        assert_eq!(
310            validate_entity_budget(&entity),
311            Err(DatabaseError::InvalidContract)
312        );
313    }
314
315    /// Certifies the bound used by every adapter's protected VARCHAR and index budget.
316    #[test]
317    fn unicode_lowercase_expansion_fits_the_declared_capacity() {
318        for scalar in 0..=0x10ffff {
319            if let Some(character) = char::from_u32(scalar) {
320                assert!(character.to_lowercase().count() <= LOWERCASE_EXPANSION as usize);
321            }
322        }
323    }
324
325    /// Unicode expression keys preserve contextual folding without changing source values.
326    #[test]
327    fn unicode_case_mapping_preserves_ordinary_text_and_context() {
328        for (input, expected) in [
329            ("ÉCOLE", "école"),
330            ("İ", "i\u{307}"),
331            ("ΟΣ", "ος"),
332            ("ẞ", "ß"),
333        ] {
334            assert_eq!(
335                lowercase_value(&Value::String(input.to_owned())).unwrap(),
336                Value::String(expected.to_owned())
337            );
338        }
339        assert_eq!(lowercase_value(&Value::Null).unwrap(), Value::Null);
340        assert!(lowercase_value(&Value::from(1)).is_err());
341        assert!(lowercase_key(&"x".repeat(63)).len() <= 63);
342    }
343}