1use std::collections::BTreeSet;
4
5use serde_json::Value;
6
7use crate::operations::{DatabaseError, Entity, FieldType, IndexKey};
8
9pub const LOWERCASE_EXPANSION: u32 = 2;
12
13pub 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
26pub fn lowercase_key(field: &str) -> String {
28 let digest = crate::operations::bytes_fingerprint(field.as_bytes());
29 format!("_pharos_lower_{}", &digest[..32])
30}
31
32pub 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
42pub 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
52pub fn json_comparison_key(field: &str) -> String {
54 format!(
55 "_pharos_json_{}",
56 &crate::operations::bytes_fingerprint(field.as_bytes())[..32]
57 )
58}
59
60pub 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
73pub 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
87pub 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
101pub 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
115pub 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
134pub 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
146pub 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
181pub 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 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
238fn 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 #[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 #[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 #[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}