1use std::collections::{BTreeMap, BTreeSet};
6
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9use sha2::{Digest, Sha256};
10
11use crate::DbBackendConfig;
12
13pub const MAX_TEXT_LENGTH: u32 = 1_048_576;
15
16pub const MAX_TRANSACTION_STEPS: usize = 2048;
18
19#[derive(Debug, Clone, Copy, Eq, PartialEq)]
21pub enum DatabaseError {
22 InvalidContract,
23 InvalidInput,
24 Unsupported,
25 Unavailable,
26 Conflict,
27 Busy,
29 MissingRecord,
30 AccessDenied,
31 CapacityExceeded,
32 StorageFailure,
33}
34
35#[derive(Debug, Clone, Copy, Eq, PartialEq)]
37pub struct ActiveSession {
38 pub user_id: i64,
39 pub expires_at_epoch: u64,
40}
41
42pub struct UserCredentials {
44 pub user_id: i64,
45 pub password_hash: String,
46 pub is_active: bool,
47}
48
49#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
51pub struct ManagedBusiness {
52 pub id: i64,
53 pub slug: String,
54 pub name: String,
55 pub description: String,
56}
57
58pub fn validate_managed_business_read(user_id: i64, limit: usize) -> Result<(), DatabaseError> {
60 if user_id <= 0 || limit == 0 || limit > 256 {
61 return Err(DatabaseError::InvalidInput);
62 }
63 Ok(())
64}
65
66pub fn valid_tenant_role(role: &str) -> bool {
68 !role.is_empty()
69 && role.len() <= 64
70 && role
71 .bytes()
72 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
73}
74
75pub fn validate_username_lookup(username: &str) -> Result<(), DatabaseError> {
76 if username.is_empty() || username.len() > 256 || username.contains('\0') {
77 return Err(DatabaseError::InvalidInput);
78 }
79 Ok(())
80}
81
82pub fn validate_password_hash_update(
84 user_id: i64,
85 password_hash: &str,
86) -> Result<(), DatabaseError> {
87 if user_id <= 0
88 || password_hash.is_empty()
89 || password_hash.len() > 1024
90 || password_hash.contains('\0')
91 {
92 return Err(DatabaseError::InvalidInput);
93 }
94 Ok(())
95}
96
97pub fn validate_session_lookup(token_hash: &str, now: u64) -> Result<i64, DatabaseError> {
99 if token_hash.is_empty() || token_hash.len() > 256 || token_hash.contains('\0') {
100 return Err(DatabaseError::InvalidInput);
101 }
102 let epoch = i64::try_from(now).map_err(|_| DatabaseError::InvalidInput)?;
103 canonical_timestamp_boundary(epoch)?;
104 Ok(epoch)
105}
106
107pub struct SessionExpiry {
109 pub epoch: i64,
110 pub rfc3339_utc: String,
111}
112
113pub fn validate_session_issue(
115 user_id: i64,
116 token_hash: &str,
117 expires_at: &str,
118) -> Result<SessionExpiry, DatabaseError> {
119 validate_session_lookup(token_hash, 0)?;
120 if user_id <= 0 || expires_at.len() > 128 {
121 return Err(DatabaseError::InvalidInput);
122 }
123 let parsed = chrono::DateTime::parse_from_rfc3339(expires_at)
124 .map_err(|_| DatabaseError::InvalidInput)?
125 .with_timezone(&chrono::Utc);
126 if parsed.timestamp() <= 0 {
127 return Err(DatabaseError::InvalidInput);
128 }
129 let encoded = parsed.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true);
130 if encoded.len() != 30 {
131 return Err(DatabaseError::InvalidInput);
132 }
133 let epoch = parsed.timestamp() + i64::from(parsed.timestamp_subsec_nanos() != 0);
134 Ok(SessionExpiry {
135 epoch,
136 rfc3339_utc: encoded,
137 })
138}
139
140pub fn canonical_timestamp_boundary(epoch: i64) -> Result<String, DatabaseError> {
142 let encoded = chrono::DateTime::<chrono::Utc>::from_timestamp(epoch, 0)
143 .map(|time| time.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true))
144 .ok_or(DatabaseError::InvalidInput)?;
145 if encoded.len() != 30 {
146 return Err(DatabaseError::InvalidInput);
147 }
148 Ok(encoded)
149}
150
151pub fn stored_session_expiry_epoch(value: &str) -> Result<u64, DatabaseError> {
153 let canonical = canonical_stored_timestamp(value)?;
154 let time = chrono::DateTime::parse_from_rfc3339(&canonical)
155 .map_err(|_| DatabaseError::StorageFailure)?;
156 let expiry = time
157 .timestamp()
158 .checked_add(i64::from(time.timestamp_subsec_nanos() != 0))
159 .ok_or(DatabaseError::StorageFailure)?;
160 u64::try_from(expiry).map_err(|_| DatabaseError::StorageFailure)
161}
162
163#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
165#[serde(rename_all = "snake_case")]
166pub enum FieldType {
167 Text,
168 Integer,
169 Decimal,
170 Boolean,
171 Timestamp,
172 Json,
173 Binary,
174}
175
176pub fn decimal_cents(value: &Value) -> Result<i64, DatabaseError> {
180 use std::borrow::Cow;
181 let text: Cow<'_, str> = match value {
182 Value::String(text) => Cow::Borrowed(text.as_str()),
183 Value::Number(number) => Cow::Owned(number.to_string()),
184 _ => return Err(DatabaseError::InvalidInput),
185 };
186 let (negative, digits) = text
187 .strip_prefix('-')
188 .map_or((false, text.as_ref()), |rest| (true, rest));
189 let mut parts = digits.split('.');
190 let whole = parts.next().ok_or(DatabaseError::InvalidInput)?;
191 let fraction = parts.next().unwrap_or("");
192 if parts.next().is_some()
193 || whole.is_empty()
194 || whole.len() > 8
195 || !whole.bytes().all(|byte| byte.is_ascii_digit())
196 || fraction.len() > 2
197 || !fraction.bytes().all(|byte| byte.is_ascii_digit())
198 {
199 return Err(DatabaseError::InvalidInput);
200 }
201 let units = whole
202 .parse::<i64>()
203 .map_err(|_| DatabaseError::InvalidInput)?;
204 let cents = match fraction.len() {
205 0 => 0,
206 1 => i64::from(fraction.as_bytes()[0] - b'0') * 10,
207 _ => fraction
208 .parse::<i64>()
209 .map_err(|_| DatabaseError::InvalidInput)?,
210 };
211 let amount = units * 100 + cents;
212 Ok(if negative { -amount } else { amount })
213}
214
215pub fn decimal_from_cents(cents: i64) -> String {
217 let magnitude = cents.unsigned_abs();
218 format!(
219 "{}{}.{:02}",
220 if cents < 0 { "-" } else { "" },
221 magnitude / 100,
222 magnitude % 100
223 )
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct Field {
229 pub name: String,
230 pub kind: FieldType,
231 pub nullable: bool,
232 pub max_length: Option<u32>,
233 pub default: Option<FieldDefault>,
234 pub references: Option<(String, String)>,
235 pub on_delete: Option<OnDelete>,
236 pub constraints: Vec<FieldConstraint>,
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241#[serde(rename_all = "snake_case")]
242pub enum FieldDefault {
243 Literal(Value),
244 CurrentTimestamp,
245}
246
247#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
249#[serde(rename_all = "snake_case")]
250pub enum OnDelete {
251 Restrict,
252 Cascade,
253 SetNull,
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258#[serde(rename_all = "snake_case")]
259pub enum FieldConstraint {
260 Allowed(Vec<Value>),
261 LengthBetween { minimum: u32, maximum: u32 },
262 GreaterThan(Value),
263 AtMost(Value),
264 Between { minimum: Value, maximum: Value },
265}
266
267#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
269#[serde(rename_all = "snake_case")]
270pub enum IndexKey {
271 Field(String),
272 Lowercase(String),
273}
274
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277pub struct Index {
278 pub name: String,
279 pub keys: Vec<IndexKey>,
280 pub unique: bool,
281 pub filter: Option<Predicate>,
282}
283
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct Entity {
287 pub name: String,
288 pub identity: Identity,
289 pub fields: Vec<Field>,
290 pub unique: Vec<Vec<String>>,
291 pub indexes: Vec<Vec<String>>,
292 pub conditional_indexes: Vec<Index>,
293}
294
295impl Entity {
296 pub fn accepts_upsert_key(&self, key: &[String]) -> bool {
298 if key.is_empty() {
299 return false;
300 }
301 self.unique.iter().any(|candidate| candidate == key)
302 || match &self.identity {
303 Identity::AutoInteger(field) => key.len() == 1 && key[0] == *field,
304 Identity::Composite(fields) => fields == key,
305 }
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(rename_all = "snake_case")]
312pub enum Identity {
313 AutoInteger(String),
314 Composite(Vec<String>),
315}
316
317impl Identity {
318 pub fn generated_field(&self) -> Option<&str> {
320 match self {
321 Self::AutoInteger(name) => Some(name),
322 Self::Composite(_) => None,
323 }
324 }
325
326 pub fn fields(&self) -> Vec<&str> {
328 match self {
329 Self::AutoInteger(name) => vec![name],
330 Self::Composite(names) => names.iter().map(String::as_str).collect(),
331 }
332 }
333}
334
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337pub struct AppSchema {
338 pub app_id: String,
339 pub entities: Vec<Entity>,
340}
341
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344#[serde(rename_all = "snake_case")]
345pub enum SchemaChange {
346 CreateEntity(Entity),
347 RetireFieldConstraint {
350 entity: String,
351 field: String,
352 constraint: FieldConstraint,
353 },
354 ConvertField {
356 entity: String,
357 previous: Field,
358 field: Field,
359 },
360 WidenIntegerToDecimal {
363 entity: String,
364 field: String,
365 },
366 CopyJsonRecords {
370 source_entity: String,
371 source_field: String,
372 source_filter: Predicate,
373 section: String,
374 target_entity: String,
375 fields: Vec<String>,
376 },
377 PromoteIdentity {
379 entity: String,
380 field: String,
381 },
382 AddField {
383 entity: String,
384 field: Field,
385 },
386 AddUnique {
387 entity: String,
388 fields: Vec<String>,
389 },
390 AddIndex {
391 entity: String,
392 fields: Vec<String>,
393 },
394 AddConditionalIndex {
395 entity: String,
396 index: Index,
397 },
398 Seed {
399 entity: String,
400 values: Map<String, Value>,
401 },
402 SeedFrom {
405 entity: String,
406 absent_filter: Predicate,
407 source_entity: String,
408 source_filter: Predicate,
409 fields: BTreeMap<String, String>,
410 values: Map<String, Value>,
411 },
412 Backfill {
413 entity: String,
414 filter: Predicate,
415 values: Map<String, Value>,
416 },
417 CopyField {
418 entity: String,
419 source: String,
420 target: String,
421 filter: Predicate,
422 },
423 DeleteWhere {
424 entity: String,
425 filter: Predicate,
426 },
427}
428
429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
431pub struct Migration {
432 pub app_id: String,
433 pub id: String,
434 pub checksum: String,
435 pub changes: Vec<SchemaChange>,
436}
437
438#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
440#[serde(rename_all = "snake_case")]
441pub enum Predicate {
442 Equal(String, Value),
443 GeneratedEqual(String, usize),
444 NotEqual(String, Value),
445 LessThan(String, Value),
446 AtMost(String, Value),
447 GreaterThan(String, Value),
448 AtLeast(String, Value),
449 Contains(String, String),
450 IsNull(String),
451 In(String, Vec<Value>),
452 All(Vec<Predicate>),
453 Any(Vec<Predicate>),
454}
455
456#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
459pub struct SortKey {
460 pub field: String,
461 pub descending: bool,
462}
463
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
466pub struct Query {
467 pub entity: String,
468 pub fields: Vec<String>,
469 pub filter: Option<Predicate>,
470 pub order: Vec<SortKey>,
471 pub limit: u32,
472 pub offset: u32,
473}
474
475pub fn materialize_generated_predicate(
477 predicate: &Predicate,
478 generated_ids: &[Option<i64>],
479) -> Result<Predicate, DatabaseError> {
480 match predicate {
481 Predicate::GeneratedEqual(field, step) => {
482 let id = generated_ids
483 .get(*step)
484 .copied()
485 .flatten()
486 .filter(|id| *id > 0)
487 .ok_or(DatabaseError::InvalidInput)?;
488 Ok(Predicate::Equal(field.clone(), Value::from(id)))
489 }
490 Predicate::All(parts) => Ok(Predicate::All(
491 parts
492 .iter()
493 .map(|part| materialize_generated_predicate(part, generated_ids))
494 .collect::<Result<Vec<_>, _>>()?,
495 )),
496 Predicate::Any(parts) => Ok(Predicate::Any(
497 parts
498 .iter()
499 .map(|part| materialize_generated_predicate(part, generated_ids))
500 .collect::<Result<Vec<_>, _>>()?,
501 )),
502 other => Ok(other.clone()),
503 }
504}
505
506fn validate_generated_predicate_steps(
508 predicate: &Predicate,
509 steps: &[Mutation],
510 current_step: usize,
511) -> Result<(), DatabaseError> {
512 match predicate {
513 Predicate::GeneratedEqual(_, index) => {
514 if *index >= current_step
515 || !matches!(
516 steps.get(*index),
517 Some(Mutation::Insert { .. } | Mutation::InsertIfAbsent { .. })
518 )
519 {
520 return Err(DatabaseError::InvalidInput);
521 }
522 }
523 Predicate::All(parts) | Predicate::Any(parts) => {
524 for part in parts {
525 validate_generated_predicate_steps(part, steps, current_step)?;
526 }
527 }
528 _ => {}
529 }
530 Ok(())
531}
532
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
535pub struct JoinedQuery {
536 pub primary: Query,
537 pub secondary_entity: String,
538 pub secondary_fields: Vec<String>,
539 pub secondary_order: Vec<SortKey>,
540 pub primary_key: String,
541 pub secondary_key: String,
542 pub secondary_filter: Option<Predicate>,
543 pub include_unmatched: bool,
544}
545
546#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
548pub struct JoinedRecord {
549 pub primary: Map<String, Value>,
550 pub secondary: Option<Map<String, Value>>,
551}
552
553#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
556pub struct Claim {
557 pub entity: String,
558 pub filter: Predicate,
559 pub order: Vec<SortKey>,
560 pub values: BTreeMap<String, WriteValue>,
561}
562
563#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
565#[serde(rename_all = "snake_case")]
566pub enum Aggregate {
567 Count,
568 Sum(String),
569 Minimum(String),
570 Maximum(String),
571}
572
573#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
575pub struct AggregateQuery {
576 pub entity: String,
577 pub filter: Option<Predicate>,
578 pub aggregate: Aggregate,
579}
580
581pub fn aggregate_result_kind(
583 schema: &AppSchema,
584 query: &AggregateQuery,
585) -> Result<FieldType, DatabaseError> {
586 match &query.aggregate {
587 Aggregate::Count => Ok(FieldType::Integer),
588 Aggregate::Sum(name) | Aggregate::Minimum(name) | Aggregate::Maximum(name) => {
589 let kind = entity(schema, &query.entity)?
590 .fields
591 .iter()
592 .find(|field| field.name == *name)
593 .map(|field| field.kind)
594 .ok_or(DatabaseError::InvalidInput)?;
595 if matches!(query.aggregate, Aggregate::Sum(_))
596 && !matches!(kind, FieldType::Integer | FieldType::Decimal)
597 {
598 return Err(DatabaseError::InvalidInput);
599 }
600 Ok(kind)
601 }
602 }
603}
604
605pub fn decode_aggregate_text(kind: FieldType, text: &str) -> Result<Value, DatabaseError> {
607 match kind {
608 FieldType::Text | FieldType::Decimal => Ok(Value::String(text.to_owned())),
609 FieldType::Timestamp => canonical_stored_timestamp(text).map(Value::String),
610 FieldType::Integer => text
611 .parse::<i64>()
612 .map(Value::from)
613 .map_err(|_| DatabaseError::StorageFailure),
614 FieldType::Boolean => match text {
615 "true" | "1" => Ok(Value::Bool(true)),
616 "false" | "0" => Ok(Value::Bool(false)),
617 _ => Err(DatabaseError::StorageFailure),
618 },
619 FieldType::Json => serde_json::from_str::<Value>(text)
620 .map_err(|_| DatabaseError::StorageFailure)
621 .and_then(|value| canonical_json_value(&value)),
622 FieldType::Binary => {
623 if !text.len().is_multiple_of(2) || text.len() > 2_097_152 {
624 return Err(DatabaseError::StorageFailure);
625 }
626 text.as_bytes()
627 .chunks_exact(2)
628 .map(|pair| {
629 let digit = |byte: u8| match byte {
630 b'0'..=b'9' => Some(byte - b'0'),
631 b'a'..=b'f' => Some(byte - b'a' + 10),
632 b'A'..=b'F' => Some(byte - b'A' + 10),
633 _ => None,
634 };
635 Ok(Value::from(
636 digit(pair[0]).ok_or(DatabaseError::StorageFailure)? * 16
637 + digit(pair[1]).ok_or(DatabaseError::StorageFailure)?,
638 ))
639 })
640 .collect::<Result<Vec<_>, DatabaseError>>()
641 .map(Value::Array)
642 }
643 }
644}
645
646#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
648pub struct ReservationExpiry {
649 #[serde(default)]
651 pub filter: Option<Predicate>,
652 pub timestamp_field: String,
653 pub cutoff: Value,
654 pub statuses: Vec<Value>,
655 pub to_status: Value,
656 pub released_at: Option<(String, Value)>,
657 pub reason: Option<(String, Value)>,
658}
659
660pub fn reservation_expiry_filter(
662 scope: Predicate,
663 status_field: &str,
664 rule: &ReservationExpiry,
665 generated_ids: &[Option<i64>],
666) -> Result<Predicate, DatabaseError> {
667 let mut filters = vec![
668 scope,
669 Predicate::In(status_field.to_owned(), rule.statuses.clone()),
670 Predicate::AtMost(rule.timestamp_field.clone(), rule.cutoff.clone()),
671 ];
672 if let Some(filter) = &rule.filter {
673 filters.push(materialize_generated_predicate(filter, generated_ids)?);
674 }
675 Ok(Predicate::All(filters))
676}
677
678#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
680#[serde(rename_all = "snake_case")]
681pub enum Mutation {
682 Insert {
683 entity: String,
684 values: BTreeMap<String, WriteValue>,
685 },
686 Upsert {
687 entity: String,
688 unique_key: Vec<String>,
689 values: BTreeMap<String, WriteValue>,
690 },
691 InsertIfAbsent {
692 entity: String,
693 unique_key: Vec<String>,
694 values: BTreeMap<String, WriteValue>,
695 },
696 Update {
697 entity: String,
698 filter: Predicate,
699 values: BTreeMap<String, WriteValue>,
700 expected_rows: Option<u64>,
701 },
702 Delete {
703 entity: String,
704 filter: Predicate,
705 expected_rows: Option<u64>,
706 },
707 RequireCount {
708 entity: String,
709 filter: Predicate,
710 minimum: u64,
711 maximum: Option<u64>,
712 },
713 Reserve {
714 entity: String,
715 status_field: String,
716 active_statuses: Vec<Value>,
717 owner_filter: Box<Predicate>,
718 scope_filter: Box<Predicate>,
719 #[serde(default, skip_serializing_if = "Option::is_none")]
720 lock_scope_filter: Option<Box<Predicate>>,
721 capacity: u64,
722 expiry: Option<Box<ReservationExpiry>>,
723 values: BTreeMap<String, WriteValue>,
724 },
725}
726
727#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
729#[serde(rename_all = "snake_case")]
730pub enum WriteValue {
731 Literal(Value),
732 GeneratedId(usize),
733 GeneratedText {
734 index: usize,
735 prefix: String,
736 },
737 GeneratedJson {
738 index: usize,
739 marker: String,
740 template: Value,
741 },
742}
743
744impl WriteValue {
745 pub fn generated_index(&self) -> Option<usize> {
747 match self {
748 Self::Literal(_) => None,
749 Self::GeneratedId(index)
750 | Self::GeneratedText { index, .. }
751 | Self::GeneratedJson { index, .. } => Some(*index),
752 }
753 }
754
755 pub fn relocate(&mut self, offset: usize) -> Result<(), DatabaseError> {
757 match self {
758 Self::Literal(_) => {}
759 Self::GeneratedId(index)
760 | Self::GeneratedText { index, .. }
761 | Self::GeneratedJson { index, .. } => {
762 *index = index
763 .checked_add(offset)
764 .ok_or(DatabaseError::CapacityExceeded)?;
765 }
766 }
767 Ok(())
768 }
769}
770
771pub fn resolve_write_value(
774 value: &WriteValue,
775 ids: &[Option<i64>],
776) -> Result<Value, DatabaseError> {
777 let id = |index: usize| {
778 ids.get(index)
779 .copied()
780 .flatten()
781 .filter(|id| *id > 0)
782 .ok_or(DatabaseError::InvalidInput)
783 };
784 match value {
785 WriteValue::Literal(value) => Ok(value.clone()),
786 WriteValue::GeneratedId(index) => Ok(Value::from(id(*index)?)),
787 WriteValue::GeneratedText { index, prefix } => {
788 Ok(Value::String(format!("{prefix}{}", id(*index)?)))
789 }
790 WriteValue::GeneratedJson {
791 index,
792 marker,
793 template,
794 } => {
795 let prefix = marker
796 .strip_suffix('0')
797 .ok_or(DatabaseError::InvalidInput)?;
798 let replacement = format!("{prefix}{}", id(*index)?);
799 let mut result = template.clone();
800 replace_resource_marker(&mut result, marker, &replacement, 0)?;
801 Ok(result)
802 }
803 }
804}
805
806pub fn resolve_field_write(
810 field: &Field,
811 value: &WriteValue,
812 ids: &[Option<i64>],
813) -> Result<Value, DatabaseError> {
814 let resolved = resolve_write_value(value, ids)?;
815 validate_field_value(field, &resolved)?;
816 Ok(resolved)
817}
818
819fn replace_resource_marker(
821 value: &mut Value,
822 marker: &str,
823 replacement: &str,
824 depth: usize,
825) -> Result<(), DatabaseError> {
826 if depth > 32 {
827 return Err(DatabaseError::CapacityExceeded);
828 }
829 match value {
830 Value::String(text) if depth == 0 && text == marker => *text = replacement.to_owned(),
831 Value::Array(items) => {
832 for value in items {
833 replace_resource_marker(value, marker, replacement, depth + 1)?;
834 }
835 }
836 Value::Object(items) => {
837 for (name, value) in items {
838 if name == "resource_id" && value.as_str() == Some(marker) {
839 *value = Value::String(replacement.to_owned());
840 } else {
841 replace_resource_marker(value, marker, replacement, depth + 1)?;
842 }
843 }
844 }
845
846 _ => {}
847 }
848 Ok(())
849}
850
851#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
853pub struct Transaction {
854 pub steps: Vec<Mutation>,
855 pub idempotency_key: Option<String>,
856}
857
858#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
860pub struct TransactionResult {
861 pub affected: Vec<u64>,
862 pub generated_ids: Vec<Option<i64>>,
863}
864
865#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
867pub struct Record {
868 pub entity: String,
869 pub fields: Map<String, Value>,
870}
871
872#[derive(Default)]
874pub struct QueryResultBudget {
875 bytes: usize,
876}
877
878impl QueryResultBudget {
879 pub fn accept(&mut self, record: &Map<String, Value>) -> Result<(), DatabaseError> {
881 struct Counter {
882 bytes: usize,
883 maximum: usize,
884 }
885 impl std::io::Write for Counter {
886 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
888 self.bytes = self
889 .bytes
890 .checked_add(bytes.len())
891 .ok_or_else(|| std::io::Error::other("page limit"))?;
892 if self.bytes > self.maximum {
893 return Err(std::io::Error::other("page limit"));
894 }
895 Ok(bytes.len())
896 }
897 fn flush(&mut self) -> std::io::Result<()> {
899 Ok(())
900 }
901 }
902 let mut counter = Counter {
903 bytes: self.bytes,
904 maximum: 8 * 1024 * 1024,
905 };
906 serde_json::to_writer(&mut counter, record).map_err(|_| DatabaseError::CapacityExceeded)?;
907 self.bytes = counter.bytes;
908 Ok(())
909 }
910}
911
912#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
914pub struct SnapshotLimits {
915 pub max_records: u32,
916 pub max_bytes: u64,
917}
918
919pub struct SnapshotCollector {
922 schema: AppSchema,
923 limits: SnapshotLimits,
924 entity_index: usize,
925 cursor: Option<Value>,
926 records: Vec<Record>,
927 bytes: u64,
928}
929
930impl SnapshotCollector {
931 pub fn new(schema: &AppSchema, limits: SnapshotLimits) -> Result<Self, DatabaseError> {
932 validate_schema(schema)?;
933 if limits.max_records == 0
934 || limits.max_records > 1_000_000
935 || limits.max_bytes < 2
936 || limits.max_bytes > 256 * 1024 * 1024
937 {
938 return Err(DatabaseError::InvalidInput);
939 }
940 Ok(Self {
941 schema: schema.clone(),
942 limits,
943 entity_index: 0,
944 cursor: None,
945 records: Vec::new(),
946 bytes: 2,
947 })
948 }
949
950 pub fn next_query(&self) -> Result<Option<Query>, DatabaseError> {
951 let Some(declaration) = self.schema.entities.get(self.entity_index) else {
952 return Ok(None);
953 };
954 let (filter, order) = identity_page(declaration, self.cursor.clone())?;
955 Ok(Some(Query {
956 entity: declaration.name.clone(),
957 fields: declaration
958 .fields
959 .iter()
960 .map(|field| field.name.clone())
961 .collect(),
962 filter,
963 order,
964 limit: 256,
965 offset: 0,
966 }))
967 }
968
969 pub fn accept(&mut self, rows: Vec<Map<String, Value>>) -> Result<(), DatabaseError> {
970 let declaration = self
971 .schema
972 .entities
973 .get(self.entity_index)
974 .ok_or(DatabaseError::InvalidInput)?;
975 if rows.len() > 256 {
976 return Err(DatabaseError::StorageFailure);
977 }
978 let complete = rows.len() < 256;
979 for fields in rows {
980 if fields.len() != declaration.fields.len() {
981 return Err(DatabaseError::StorageFailure);
982 }
983 for field in &declaration.fields {
984 validate_field_value(
985 field,
986 fields
987 .get(&field.name)
988 .ok_or(DatabaseError::StorageFailure)?,
989 )?;
990 }
991 let cursor = match &declaration.identity {
992 Identity::AutoInteger(name) => fields
993 .get(name)
994 .cloned()
995 .ok_or(DatabaseError::StorageFailure)?,
996 Identity::Composite(names) => Value::Object(
997 names
998 .iter()
999 .map(|name| {
1000 fields
1001 .get(name)
1002 .cloned()
1003 .map(|value| (name.clone(), value))
1004 .ok_or(DatabaseError::StorageFailure)
1005 })
1006 .collect::<Result<Map<_, _>, _>>()?,
1007 ),
1008 };
1009 if self.cursor.as_ref() == Some(&cursor) {
1010 return Err(DatabaseError::StorageFailure);
1011 }
1012 let record = Record {
1013 entity: declaration.name.clone(),
1014 fields,
1015 };
1016 self.bytes = self
1017 .bytes
1018 .checked_add(
1019 serde_json::to_vec(&record)
1020 .map_err(|_| DatabaseError::StorageFailure)?
1021 .len() as u64
1022 + 1,
1023 )
1024 .ok_or(DatabaseError::InvalidInput)?;
1025 if self.records.len() >= self.limits.max_records as usize
1026 || self.bytes > self.limits.max_bytes
1027 {
1028 return Err(DatabaseError::CapacityExceeded);
1029 }
1030 self.cursor = Some(cursor);
1031 self.records.push(record);
1032 }
1033 if complete {
1034 self.entity_index += 1;
1035 self.cursor = None;
1036 }
1037 Ok(())
1038 }
1039
1040 pub fn finish(self) -> Result<Vec<Record>, DatabaseError> {
1041 if self.entity_index != self.schema.entities.len() {
1042 return Err(DatabaseError::InvalidInput);
1043 }
1044 Ok(self.records)
1045 }
1046}
1047
1048pub fn operation_fingerprint<T: Serialize>(value: &T) -> Result<String, DatabaseError> {
1050 let value = serde_json::to_value(value).map_err(|_| DatabaseError::InvalidInput)?;
1051 let mut bytes = Vec::new();
1052 canonical_json(&value, &mut bytes, 0)?;
1053 Ok(bytes_fingerprint(&bytes))
1054}
1055
1056fn canonical_json(value: &Value, output: &mut Vec<u8>, depth: usize) -> Result<(), DatabaseError> {
1059 if depth > 64 || output.len() > 8 * 1024 * 1024 {
1060 return Err(DatabaseError::InvalidInput);
1061 }
1062 match value {
1063 Value::Null => output.extend_from_slice(b"null"),
1064 Value::Bool(true) => output.extend_from_slice(b"true"),
1065 Value::Bool(false) => output.extend_from_slice(b"false"),
1066 Value::Number(number) => output.extend_from_slice(number.to_string().as_bytes()),
1067 Value::String(string) => output.extend_from_slice(
1068 serde_json::to_string(string)
1069 .map_err(|_| DatabaseError::InvalidInput)?
1070 .as_bytes(),
1071 ),
1072 Value::Array(values) => {
1073 output.push(b'[');
1074 for (index, item) in values.iter().enumerate() {
1075 if index > 0 {
1076 output.push(b',');
1077 }
1078 canonical_json(item, output, depth + 1)?;
1079 }
1080 output.push(b']');
1081 }
1082 Value::Object(values) => {
1083 output.push(b'{');
1084 let mut keys = values.keys().collect::<Vec<_>>();
1085 keys.sort_unstable();
1086 for (index, key) in keys.into_iter().enumerate() {
1087 if index > 0 {
1088 output.push(b',');
1089 }
1090 output.extend_from_slice(
1091 serde_json::to_string(key)
1092 .map_err(|_| DatabaseError::InvalidInput)?
1093 .as_bytes(),
1094 );
1095 output.push(b':');
1096 canonical_json(&values[key], output, depth + 1)?;
1097 }
1098 output.push(b'}');
1099 }
1100 }
1101 if output.len() > 8 * 1024 * 1024 {
1102 return Err(DatabaseError::InvalidInput);
1103 }
1104 Ok(())
1105}
1106
1107pub fn bytes_fingerprint(bytes: &[u8]) -> String {
1109 let hash = Sha256::digest(bytes);
1110 let mut result = String::with_capacity(64);
1111 for byte in hash {
1112 result.push(char::from(b"0123456789abcdef"[(byte >> 4) as usize]));
1113 result.push(char::from(b"0123456789abcdef"[(byte & 0x0f) as usize]));
1114 }
1115 result
1116}
1117
1118pub fn binary_bytes(value: &Value) -> Result<Vec<u8>, DatabaseError> {
1120 let bytes = value.as_array().ok_or(DatabaseError::InvalidInput)?;
1121 if bytes.len() > 1_048_576 {
1122 return Err(DatabaseError::CapacityExceeded);
1123 }
1124 bytes
1125 .iter()
1126 .map(|byte| {
1127 byte.as_u64()
1128 .and_then(|byte| u8::try_from(byte).ok())
1129 .ok_or(DatabaseError::InvalidInput)
1130 })
1131 .collect()
1132}
1133
1134pub fn canonical_stored_timestamp(value: &str) -> Result<String, DatabaseError> {
1138 let timestamp = chrono::DateTime::parse_from_rfc3339(value)
1139 .map(|value| value.with_timezone(&chrono::Utc))
1140 .or_else(|_| {
1141 chrono::NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f")
1142 .map(|value| value.and_utc())
1143 })
1144 .ok()
1145 .or_else(|| {
1146 if value.is_empty()
1149 || value.len() > 20
1150 || !value.bytes().all(|byte| byte.is_ascii_digit())
1151 {
1152 return None;
1153 }
1154 chrono::DateTime::<chrono::Utc>::from_timestamp(value.parse().ok()?, 0)
1155 })
1156 .ok_or(DatabaseError::StorageFailure)?;
1157 let encoded = timestamp.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true);
1160 if encoded.len() != 30 {
1161 return Err(DatabaseError::StorageFailure);
1162 }
1163 Ok(encoded)
1164}
1165
1166pub fn validate_json_value(value: &Value) -> Result<(), DatabaseError> {
1168 let mut pending = vec![(value, 0usize)];
1169 let mut nodes = 0usize;
1170 while let Some((value, depth)) = pending.pop() {
1171 nodes += 1;
1172 if nodes > 65536 || depth > 32 {
1173 return Err(DatabaseError::CapacityExceeded);
1174 }
1175 let children = match value {
1176 Value::Array(values) => values.len(),
1177 Value::Object(values) => values.len(),
1178 _ => 0,
1179 };
1180 if nodes + pending.len() + children > 65536 {
1181 return Err(DatabaseError::CapacityExceeded);
1182 }
1183 match value {
1184 Value::String(text) if text.contains('\0') => {
1185 return Err(DatabaseError::InvalidInput);
1186 }
1187 Value::Array(values) => pending.extend(values.iter().map(|value| (value, depth + 1))),
1188 Value::Object(values) => {
1189 if values.keys().any(|key| key.contains('\0')) {
1190 return Err(DatabaseError::InvalidInput);
1191 }
1192 pending.extend(values.values().map(|value| (value, depth + 1)))
1193 }
1194 _ => {}
1195 }
1196 }
1197 struct SizeLimit(usize);
1198 impl std::io::Write for SizeLimit {
1199 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
1201 if bytes.len() > self.0 {
1202 return Err(std::io::Error::other("logical JSON capacity exceeded"));
1203 }
1204 self.0 -= bytes.len();
1205 Ok(bytes.len())
1206 }
1207 fn flush(&mut self) -> std::io::Result<()> {
1209 Ok(())
1210 }
1211 }
1212 serde_json::to_writer(SizeLimit(1_048_576), value).map_err(|_| DatabaseError::CapacityExceeded)
1213}
1214
1215pub fn canonical_json_value(value: &Value) -> Result<Value, DatabaseError> {
1219 validate_json_value(value)?;
1220 fn visit(value: &Value, depth: usize, remaining: &mut usize) -> Result<Value, DatabaseError> {
1222 if depth > 32 || *remaining == 0 {
1223 return Err(DatabaseError::CapacityExceeded);
1224 }
1225 *remaining -= 1;
1226 Ok(match value {
1227 Value::Number(number) if number.as_i64().is_none() && number.as_u64().is_none() => {
1228 let numeric = number.as_f64().ok_or(DatabaseError::InvalidInput)?;
1229 if numeric.fract() == 0.0
1230 && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0)
1231 .contains(&numeric)
1232 {
1233 Value::from(numeric as i64)
1234 } else if numeric.fract() == 0.0
1235 && (0.0..18_446_744_073_709_551_616.0).contains(&numeric)
1236 {
1237 Value::from(numeric as u64)
1238 } else {
1239 value.clone()
1240 }
1241 }
1242 Value::Array(items) => Value::Array(
1243 items
1244 .iter()
1245 .map(|item| visit(item, depth + 1, remaining))
1246 .collect::<Result<_, _>>()?,
1247 ),
1248 Value::Object(fields) => {
1249 let mut canonical = fields
1250 .iter()
1251 .map(|(name, item)| Ok((name.clone(), visit(item, depth + 1, remaining)?)))
1252 .collect::<Result<Map<String, Value>, DatabaseError>>()?;
1253 canonical.sort_keys();
1254 Value::Object(canonical)
1255 }
1256 _ => value.clone(),
1257 })
1258 }
1259 visit(value, 0, &mut 65536)
1260}
1261
1262pub fn validate_stored_record(
1266 entity: &Entity,
1267 record: &Map<String, Value>,
1268) -> Result<(), DatabaseError> {
1269 for (name, value) in record {
1270 let field = entity
1271 .fields
1272 .iter()
1273 .find(|field| field.name == *name)
1274 .ok_or(DatabaseError::InvalidContract)?;
1275 validate_field_value(field, value).map_err(|error| match error {
1276 DatabaseError::CapacityExceeded => DatabaseError::CapacityExceeded,
1277 _ => DatabaseError::StorageFailure,
1278 })?;
1279 }
1280 Ok(())
1281}
1282
1283pub fn normalize_stored_record(
1285 entity: &Entity,
1286 mut record: Map<String, Value>,
1287) -> Result<Map<String, Value>, DatabaseError> {
1288 validate_stored_record(entity, &record)?;
1289 for field in &entity.fields {
1290 if let Some(value) = record.get_mut(&field.name)
1291 && !value.is_null()
1292 {
1293 if field.kind == FieldType::Json {
1294 *value = canonical_json_value(value).map_err(|_| DatabaseError::StorageFailure)?;
1295 } else if field.kind == FieldType::Timestamp {
1296 *value = Value::String(canonical_stored_timestamp(
1297 value.as_str().ok_or(DatabaseError::StorageFailure)?,
1298 )?);
1299 }
1300 }
1301 }
1302 Ok(record)
1303}
1304
1305pub trait DatabaseOperations: Send + Sync {
1308 fn backend_name(&self) -> &'static str;
1309 fn local_artifact_format(&self) -> Option<&'static str>;
1312 fn ensure_database(
1314 &self,
1315 config: &DbBackendConfig,
1316 schema: &AppSchema,
1317 ) -> Result<(), DatabaseError>;
1318 fn user_credentials(
1320 &self,
1321 config: &DbBackendConfig,
1322 schema: &AppSchema,
1323 username: &str,
1324 ) -> Result<Option<UserCredentials>, DatabaseError> {
1325 validate_username_lookup(username)?;
1326 let rows = self.query(
1327 config,
1328 schema,
1329 &Query {
1330 entity: "user_account".to_owned(),
1331 fields: vec![
1332 "id".to_owned(),
1333 "password_hash".to_owned(),
1334 "is_active".to_owned(),
1335 ],
1336 filter: Some(Predicate::Equal(
1337 "username".to_owned(),
1338 Value::from(username),
1339 )),
1340 order: Vec::new(),
1341 limit: 1,
1342 offset: 0,
1343 },
1344 )?;
1345 let Some(row) = rows.first() else {
1346 return Ok(None);
1347 };
1348 let user_id = row
1349 .get("id")
1350 .and_then(Value::as_i64)
1351 .ok_or(DatabaseError::StorageFailure)?;
1352 let password_hash = row
1353 .get("password_hash")
1354 .and_then(Value::as_str)
1355 .ok_or(DatabaseError::StorageFailure)?
1356 .to_owned();
1357 let is_active = row
1358 .get("is_active")
1359 .and_then(Value::as_bool)
1360 .ok_or(DatabaseError::StorageFailure)?;
1361 Ok((user_id > 0).then_some(UserCredentials {
1362 user_id,
1363 password_hash,
1364 is_active,
1365 }))
1366 }
1367 fn update_user_password(
1369 &self,
1370 config: &DbBackendConfig,
1371 user_id: i64,
1372 password_hash: &str,
1373 ) -> Result<(), DatabaseError>;
1374 fn issue_session(
1376 &self,
1377 config: &DbBackendConfig,
1378 user_id: i64,
1379 session_token_hash: &str,
1380 expires_at: &str,
1381 ) -> Result<(), DatabaseError>;
1382 fn revoke_session(
1384 &self,
1385 config: &DbBackendConfig,
1386 session_token_hash: &str,
1387 ) -> Result<(), DatabaseError>;
1388 fn revoke_user_sessions(
1390 &self,
1391 config: &DbBackendConfig,
1392 user_id: i64,
1393 ) -> Result<(), DatabaseError>;
1394 fn active_session(
1396 &self,
1397 config: &DbBackendConfig,
1398 schema: &AppSchema,
1399 session_token_hash: &str,
1400 now_epoch_seconds: u64,
1401 ) -> Result<Option<ActiveSession>, DatabaseError> {
1402 validate_session_lookup(session_token_hash, now_epoch_seconds)?;
1403 let rows = self.query(
1404 config,
1405 schema,
1406 &Query {
1407 entity: "auth_session".to_owned(),
1408 fields: vec!["user_id".to_owned(), "expires_at".to_owned()],
1409 filter: Some(Predicate::All(vec![
1410 Predicate::Equal(
1411 "session_token_hash".to_owned(),
1412 Value::from(session_token_hash),
1413 ),
1414 Predicate::IsNull("revoked_at".to_owned()),
1415 ])),
1416 order: Vec::new(),
1417 limit: 1,
1418 offset: 0,
1419 },
1420 )?;
1421 let Some(row) = rows.first() else {
1422 return Ok(None);
1423 };
1424 let user_id = row
1425 .get("user_id")
1426 .and_then(Value::as_i64)
1427 .ok_or(DatabaseError::StorageFailure)?;
1428 let expires_at_epoch = stored_session_expiry_epoch(
1429 row.get("expires_at")
1430 .and_then(Value::as_str)
1431 .ok_or(DatabaseError::StorageFailure)?,
1432 )?;
1433 Ok(
1434 (user_id > 0 && expires_at_epoch > now_epoch_seconds).then_some(ActiveSession {
1435 user_id,
1436 expires_at_epoch,
1437 }),
1438 )
1439 }
1440 fn append_rate_limit_event(
1442 &self,
1443 config: &DbBackendConfig,
1444 policy_key: &str,
1445 bucket_key: &str,
1446 outcome: &str,
1447 ) -> Result<(), DatabaseError>;
1448 fn admit_rate_limited_request(
1450 &self,
1451 config: &DbBackendConfig,
1452 policy_key: &str,
1453 bucket_key: &str,
1454 maximum: usize,
1455 window_seconds: u64,
1456 ) -> Result<bool, DatabaseError>;
1457 fn apply_contract(
1461 &self,
1462 config: &DbBackendConfig,
1463 contract: &crate::contract::AppDataContract,
1464 ) -> Result<(), DatabaseError> {
1465 crate::contract::validate_contract(contract)?;
1466 let already_applied = self.migration_ledger(config, &contract.schema.app_id)?;
1467 let adoption_schema = crate::contract::legacy_adoption_schema(contract)?;
1468 for (index, migration) in contract.migrations.iter().enumerate() {
1469 if let Some(checksum) = already_applied.get(&migration.id) {
1470 if checksum != &migration.checksum {
1471 return Err(DatabaseError::Conflict);
1472 }
1473 continue;
1474 }
1475 if !already_applied.contains_key(&migration.id)
1476 && let Some(adoption) = contract
1477 .legacy_adoption
1478 .as_ref()
1479 .filter(|adoption| adoption.logical_migrations.contains(&migration.id))
1480 {
1481 let historical_schema = adoption_schema
1482 .as_ref()
1483 .ok_or(DatabaseError::InvalidContract)?;
1484 let mut adopted = false;
1485 let mut conflicting_legacy = false;
1486 for expected in &adoption.ledgers {
1487 match self.adopt_legacy_migration(
1488 config,
1489 historical_schema,
1490 migration,
1491 expected,
1492 ) {
1493 Ok(()) => {
1494 adopted = true;
1495 break;
1496 }
1497 Err(DatabaseError::MissingRecord) => {}
1498 Err(DatabaseError::Conflict) => conflicting_legacy = true,
1499 Err(error) => return Err(error),
1500 }
1501 }
1502 if adopted {
1503 continue;
1504 }
1505 if conflicting_legacy {
1506 return Err(DatabaseError::Conflict);
1507 }
1508 }
1509 let staged = crate::contract::schema_through_migrations(contract, index + 1)?;
1510 self.migrate(config, &staged, migration)?;
1511 }
1512 if let Some(last) = contract.migrations.last() {
1513 self.migrate(config, &contract.schema, last)?;
1514 }
1515 self.verify_contract(config, contract)
1516 }
1517 fn migration_ledger(
1518 &self,
1519 config: &DbBackendConfig,
1520 app_id: &str,
1521 ) -> Result<BTreeMap<String, String>, DatabaseError>;
1522 fn applied_migration_checksum(
1523 &self,
1524 config: &DbBackendConfig,
1525 app_id: &str,
1526 migration_id: &str,
1527 ) -> Result<Option<String>, DatabaseError> {
1528 if !valid_identifier(migration_id) {
1529 return Err(DatabaseError::InvalidInput);
1530 }
1531 Ok(self.migration_ledger(config, app_id)?.remove(migration_id))
1532 }
1533 fn verify_contract(
1534 &self,
1535 config: &DbBackendConfig,
1536 contract: &crate::contract::AppDataContract,
1537 ) -> Result<(), DatabaseError> {
1538 crate::contract::validate_contract(contract)?;
1539 self.ready(config, &contract.schema)?;
1540 let ledger = self.migration_ledger(config, &contract.schema.app_id)?;
1541 for migration in &contract.migrations {
1542 match ledger.get(&migration.id) {
1543 Some(checksum) if checksum == &migration.checksum => {}
1544 Some(_) => return Err(DatabaseError::Conflict),
1545 None => return Err(DatabaseError::MissingRecord),
1546 }
1547 }
1548 Ok(())
1549 }
1550 fn ready(&self, config: &DbBackendConfig, schema: &AppSchema) -> Result<(), DatabaseError>;
1551 fn field_exists(
1554 &self,
1555 config: &DbBackendConfig,
1556 schema: &AppSchema,
1557 entity_name: &str,
1558 field_name: &str,
1559 ) -> Result<bool, DatabaseError>;
1560 fn migrate(
1561 &self,
1562 config: &DbBackendConfig,
1563 schema: &AppSchema,
1564 migration: &Migration,
1565 ) -> Result<(), DatabaseError>;
1566 fn adopt_legacy_migration(
1571 &self,
1572 config: &DbBackendConfig,
1573 schema: &AppSchema,
1574 migration: &Migration,
1575 expected_legacy: &BTreeMap<String, String>,
1576 ) -> Result<(), DatabaseError>;
1577 fn query(
1578 &self,
1579 config: &DbBackendConfig,
1580 schema: &AppSchema,
1581 query: &Query,
1582 ) -> Result<Vec<Map<String, Value>>, DatabaseError>;
1583 fn join_query(
1585 &self,
1586 config: &DbBackendConfig,
1587 schema: &AppSchema,
1588 request: &JoinedQuery,
1589 ) -> Result<Vec<JoinedRecord>, DatabaseError> {
1590 validate_query(schema, &request.primary)?;
1591 let primary_entity = entity(schema, &request.primary.entity)?;
1592 let secondary_entity = entity(schema, &request.secondary_entity)?;
1593 let primary_field = primary_entity
1594 .fields
1595 .iter()
1596 .find(|field| field.name == request.primary_key)
1597 .ok_or(DatabaseError::InvalidInput)?;
1598 let secondary_field = secondary_entity
1599 .fields
1600 .iter()
1601 .find(|field| field.name == request.secondary_key)
1602 .ok_or(DatabaseError::InvalidInput)?;
1603 if primary_field.kind != secondary_field.kind
1604 || !matches!(primary_field.kind, FieldType::Integer | FieldType::Text)
1605 || !request.primary.fields.contains(&request.primary_key)
1606 || request.primary.limit > 64
1607 || request.secondary_fields.is_empty()
1608 || request.secondary_fields.len() > 64
1609 || request.secondary_fields.iter().any(|name| {
1610 !secondary_entity
1611 .fields
1612 .iter()
1613 .any(|field| field.name == *name)
1614 })
1615 {
1616 return Err(DatabaseError::InvalidInput);
1617 }
1618 if let Some(filter) = &request.secondary_filter {
1619 validate_predicate(secondary_entity, filter)?;
1620 }
1621 let primary = self.query(config, schema, &request.primary)?;
1622 if primary.is_empty() {
1623 return Ok(Vec::new());
1624 }
1625 let keys = primary
1626 .iter()
1627 .filter_map(|record| {
1628 record
1629 .get(&request.primary_key)
1630 .filter(|value| !value.is_null())
1631 })
1632 .map(|value| (value.to_string(), value.clone()))
1633 .collect::<BTreeMap<_, _>>();
1634 if keys.is_empty() {
1635 return Ok(if request.include_unmatched {
1636 primary
1637 .into_iter()
1638 .map(|record| JoinedRecord {
1639 primary: record,
1640 secondary: None,
1641 })
1642 .collect()
1643 } else {
1644 Vec::new()
1645 });
1646 }
1647 let mut secondary_fields = request.secondary_fields.clone();
1648 if !secondary_fields.contains(&request.secondary_key) {
1649 secondary_fields.push(request.secondary_key.clone());
1650 }
1651 let key_filter = Predicate::In(request.secondary_key.clone(), keys.into_values().collect());
1652 let filter = request
1653 .secondary_filter
1654 .clone()
1655 .map_or(key_filter.clone(), |other| {
1656 Predicate::All(vec![key_filter, other])
1657 });
1658 let secondary = self.query(
1659 config,
1660 schema,
1661 &Query {
1662 entity: request.secondary_entity.clone(),
1663 fields: secondary_fields,
1664 filter: Some(filter),
1665 order: request.secondary_order.clone(),
1666 limit: 1000,
1667 offset: 0,
1668 },
1669 )?;
1670 if secondary.len() == 1000 {
1672 return Err(DatabaseError::InvalidInput);
1673 }
1674 let mut indexed = BTreeMap::<String, Vec<Map<String, Value>>>::new();
1675 for record in secondary {
1676 let key = record
1677 .get(&request.secondary_key)
1678 .ok_or(DatabaseError::StorageFailure)?;
1679 indexed.entry(key.to_string()).or_default().push(record);
1680 }
1681 let mut result = Vec::new();
1682 let mut budget = QueryResultBudget::default();
1683 for record in primary {
1684 let matches = record
1685 .get(&request.primary_key)
1686 .and_then(|key| indexed.get(&key.to_string()));
1687 if let Some(matches) = matches {
1688 for secondary in matches {
1689 budget.accept(&record)?;
1690 budget.accept(secondary)?;
1691 result.push(JoinedRecord {
1692 primary: record.clone(),
1693 secondary: Some(secondary.clone()),
1694 });
1695 if result.len() > 1000 {
1696 return Err(DatabaseError::InvalidInput);
1697 }
1698 }
1699 } else if request.include_unmatched {
1700 budget.accept(&record)?;
1701 result.push(JoinedRecord {
1702 primary: record,
1703 secondary: None,
1704 });
1705 }
1706 }
1707 Ok(result)
1708 }
1709 fn aggregate(
1710 &self,
1711 config: &DbBackendConfig,
1712 schema: &AppSchema,
1713 query: &AggregateQuery,
1714 ) -> Result<Value, DatabaseError>;
1715 fn transact(
1716 &self,
1717 config: &DbBackendConfig,
1718 schema: &AppSchema,
1719 transaction: &Transaction,
1720 ) -> Result<TransactionResult, DatabaseError>;
1721 fn claim_one(
1722 &self,
1723 config: &DbBackendConfig,
1724 schema: &AppSchema,
1725 claim: &Claim,
1726 ) -> Result<Option<i64>, DatabaseError>;
1727 fn export_page(
1728 &self,
1729 config: &DbBackendConfig,
1730 schema: &AppSchema,
1731 entity: &str,
1732 after_identity: Option<Value>,
1733 limit: u32,
1734 ) -> Result<Vec<Record>, DatabaseError>;
1735 fn export_snapshot(
1737 &self,
1738 config: &DbBackendConfig,
1739 schema: &AppSchema,
1740 limits: SnapshotLimits,
1741 ) -> Result<Vec<Record>, DatabaseError>;
1742 fn restore_snapshot(
1745 &self,
1746 config: &DbBackendConfig,
1747 schema: &AppSchema,
1748 records: &[Record],
1749 limits: SnapshotLimits,
1750 counts: &[crate::snapshot::RestoreCount],
1751 ) -> Result<(), DatabaseError>;
1752 fn import_batch(
1753 &self,
1754 config: &DbBackendConfig,
1755 schema: &AppSchema,
1756 records: &[Record],
1757 ) -> Result<(), DatabaseError>;
1758}
1759
1760pub fn validate_rate_limit_request(
1762 policy_key: &str,
1763 bucket_key: &str,
1764 maximum: usize,
1765 window_seconds: u64,
1766) -> Result<(), DatabaseError> {
1767 if policy_key.is_empty()
1768 || policy_key.len() > 256
1769 || policy_key.contains('\0')
1770 || bucket_key.len() > 512
1771 || bucket_key.contains('\0')
1772 || maximum == 0
1773 || maximum > 10_000
1774 || window_seconds == 0
1775 || window_seconds > 86_400
1776 {
1777 return Err(DatabaseError::InvalidInput);
1778 }
1779 Ok(())
1780}
1781
1782pub fn validate_rate_limit_event(
1784 policy_key: &str,
1785 bucket_key: &str,
1786 outcome: &str,
1787) -> Result<(), DatabaseError> {
1788 if policy_key.is_empty()
1789 || policy_key.len() > 256
1790 || policy_key.contains('\0')
1791 || bucket_key.len() > 512
1792 || bucket_key.contains('\0')
1793 || outcome.is_empty()
1794 || outcome.len() > 128
1795 || outcome.contains('\0')
1796 {
1797 return Err(DatabaseError::InvalidInput);
1798 }
1799 Ok(())
1800}
1801
1802pub fn validate_claim(schema: &AppSchema, claim: &Claim) -> Result<(), DatabaseError> {
1804 validate_schema(schema)?;
1805 let declaration = entity(schema, &claim.entity)?;
1806 validate_predicate(declaration, &claim.filter)?;
1807 let Some(identity) = declaration.identity.generated_field() else {
1808 return Err(DatabaseError::InvalidInput);
1809 };
1810 if claim.order.is_empty()
1811 || claim.order.len() > 8
1812 || !claim.order.iter().any(|key| key.field == identity)
1813 || claim.order.iter().any(|key| {
1814 !declaration
1815 .fields
1816 .iter()
1817 .any(|field| field.name == key.field)
1818 })
1819 || claim.values.is_empty()
1820 || claim.values.len() > 32
1821 || claim.values.contains_key(identity)
1822 || claim.values.iter().any(|(name, value)| {
1823 !declaration.fields.iter().any(|field| field.name == *name)
1824 || !matches!(value, WriteValue::Literal(_))
1825 })
1826 {
1827 return Err(DatabaseError::InvalidInput);
1828 }
1829 for (name, value) in &claim.values {
1830 let field = declaration
1831 .fields
1832 .iter()
1833 .find(|field| field.name == *name)
1834 .ok_or(DatabaseError::InvalidInput)?;
1835 let WriteValue::Literal(value) = value else {
1836 return Err(DatabaseError::InvalidInput);
1837 };
1838 validate_field_value(field, value)?;
1839 }
1840 Ok(())
1841}
1842
1843pub fn identity_page(
1846 declaration: &Entity,
1847 after: Option<Value>,
1848) -> Result<(Option<Predicate>, Vec<SortKey>), DatabaseError> {
1849 let fields = declaration.identity.fields();
1850 let order = fields
1851 .iter()
1852 .map(|field| SortKey {
1853 field: (*field).to_owned(),
1854 descending: false,
1855 })
1856 .collect();
1857 let Some(after) = after else {
1858 return Ok((None, order));
1859 };
1860 let values = match &declaration.identity {
1861 Identity::AutoInteger(_) => vec![after],
1862 Identity::Composite(_) => {
1863 let map = after.as_object().ok_or(DatabaseError::InvalidInput)?;
1864 if map.len() != fields.len() {
1865 return Err(DatabaseError::InvalidInput);
1866 }
1867 fields
1868 .iter()
1869 .map(|field| map.get(*field).cloned().ok_or(DatabaseError::InvalidInput))
1870 .collect::<Result<Vec<_>, _>>()?
1871 }
1872 };
1873 if values.iter().any(Value::is_null) {
1874 return Err(DatabaseError::InvalidInput);
1875 }
1876 let branches = (0..fields.len())
1877 .map(|index| {
1878 let mut prefix = (0..index)
1879 .map(|earlier| {
1880 Predicate::Equal(fields[earlier].to_owned(), values[earlier].clone())
1881 })
1882 .collect::<Vec<_>>();
1883 prefix.push(Predicate::GreaterThan(
1884 fields[index].to_owned(),
1885 values[index].clone(),
1886 ));
1887 Predicate::All(prefix)
1888 })
1889 .collect();
1890 Ok((Some(Predicate::Any(branches)), order))
1891}
1892
1893pub fn valid_identifier(value: &str) -> bool {
1895 let mut bytes = value.bytes();
1896 value.len() <= 63
1897 && matches!(bytes.next(), Some(first) if first.is_ascii_alphabetic() || first == b'_')
1898 && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1899}
1900
1901fn valid_app_identifier(value: &str) -> bool {
1903 valid_identifier(value)
1904 && !value.eq_ignore_ascii_case("_id")
1905 && !value
1906 .get(..7)
1907 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("pharos_"))
1908 && !value
1909 .get(..8)
1910 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("_pharos_"))
1911}
1912
1913pub fn generated_index_name(
1915 prefix: &str,
1916 entity: &str,
1917 fields: &[String],
1918) -> Result<String, DatabaseError> {
1919 if !matches!(prefix, "pk" | "uq" | "ix" | "fk")
1920 || !valid_identifier(entity)
1921 || fields.is_empty()
1922 || fields.iter().any(|field| !valid_identifier(field))
1923 {
1924 return Err(DatabaseError::InvalidContract);
1925 }
1926 let full_name = format!("{}_{}_{}", prefix, entity, fields.join("_"));
1927 if full_name.len() <= 63 {
1928 return Ok(full_name);
1929 }
1930 let digest = operation_fingerprint(&full_name)?;
1931 Ok(format!("{}_{}", &full_name[..46], &digest[..16]))
1932}
1933
1934pub fn validate_field_value(field: &Field, value: &Value) -> Result<(), DatabaseError> {
1936 validate_value(field, value, true)
1937}
1938
1939pub fn validate_copy_field(
1941 declaration: &Entity,
1942 source: &str,
1943 target: &str,
1944 filter: &Predicate,
1945) -> Result<(), DatabaseError> {
1946 let source_field = declaration
1947 .fields
1948 .iter()
1949 .find(|field| field.name == source)
1950 .ok_or(DatabaseError::InvalidContract)?;
1951 let target_field = declaration
1952 .fields
1953 .iter()
1954 .find(|field| field.name == target)
1955 .ok_or(DatabaseError::InvalidContract)?;
1956 if source == target
1957 || source_field.kind != target_field.kind
1958 || (source_field.nullable && !target_field.nullable)
1959 || target_field.max_length.is_some_and(|limit| {
1960 source_field
1961 .max_length
1962 .is_none_or(|source_limit| source_limit > limit)
1963 })
1964 || (!target_field.constraints.is_empty()
1965 && source_field.constraints != target_field.constraints)
1966 {
1967 return Err(DatabaseError::InvalidContract);
1968 }
1969 validate_predicate(declaration, filter)
1970}
1971
1972fn validate_value(
1973 field: &Field,
1974 value: &Value,
1975 check_constraints: bool,
1976) -> Result<(), DatabaseError> {
1977 if value.is_null() {
1978 return if field.nullable {
1979 Ok(())
1980 } else {
1981 Err(DatabaseError::InvalidInput)
1982 };
1983 }
1984 match field.kind {
1985 FieldType::Text => {
1986 let text = value.as_str().ok_or(DatabaseError::InvalidInput)?;
1987 if field
1988 .max_length
1989 .is_some_and(|maximum| text.chars().count() > maximum as usize)
1990 || text.len() > 1_048_576
1991 || text.contains('\0')
1992 {
1993 return Err(DatabaseError::InvalidInput);
1994 }
1995 }
1996 FieldType::Integer => {
1997 value.as_i64().ok_or(DatabaseError::InvalidInput)?;
1998 }
1999 FieldType::Decimal => {
2000 decimal_cents(value)?;
2001 }
2002 FieldType::Boolean => {
2003 value.as_bool().ok_or(DatabaseError::InvalidInput)?;
2004 }
2005 FieldType::Timestamp => {
2006 let text = value
2007 .as_str()
2008 .filter(|text| text.len() <= 64)
2009 .ok_or(DatabaseError::InvalidInput)?;
2010 chrono::DateTime::parse_from_rfc3339(text).map_err(|_| DatabaseError::InvalidInput)?;
2011 canonical_stored_timestamp(text).map_err(|_| DatabaseError::InvalidInput)?;
2012 }
2013 FieldType::Json => {
2014 validate_json_value(value)?;
2015 if let Some(maximum) = field.max_length {
2016 let canonical = canonical_json_value(value)?;
2017 if serde_json::to_vec(&canonical)
2018 .map_err(|_| DatabaseError::InvalidInput)?
2019 .len()
2020 > maximum as usize
2021 {
2022 return Err(DatabaseError::InvalidInput);
2023 }
2024 }
2025 }
2026 FieldType::Binary => {
2027 let bytes = value
2028 .as_array()
2029 .filter(|bytes| bytes.len() <= 1_048_576)
2030 .ok_or(DatabaseError::InvalidInput)?;
2031 if bytes
2032 .iter()
2033 .any(|byte| byte.as_u64().is_none_or(|number| number > 255))
2034 {
2035 return Err(DatabaseError::InvalidInput);
2036 }
2037 if field
2038 .max_length
2039 .is_some_and(|maximum| bytes.len() > maximum as usize)
2040 {
2041 return Err(DatabaseError::InvalidInput);
2042 }
2043 }
2044 }
2045 if !check_constraints {
2046 return Ok(());
2047 }
2048 for constraint in &field.constraints {
2049 let valid = match constraint {
2050 FieldConstraint::Allowed(allowed) if field.kind == FieldType::Json => {
2051 let canonical = canonical_json_value(value)?;
2052 allowed
2053 .iter()
2054 .map(canonical_json_value)
2055 .collect::<Result<Vec<_>, _>>()?
2056 .contains(&canonical)
2057 }
2058 FieldConstraint::Allowed(allowed) => allowed.contains(value),
2059 FieldConstraint::LengthBetween { minimum, maximum } => {
2060 value.as_str().is_some_and(|text| {
2061 (*minimum as usize..=*maximum as usize).contains(&text.chars().count())
2062 })
2063 }
2064 FieldConstraint::GreaterThan(minimum) => numeric_constraint_value(field.kind, value)
2065 .zip(numeric_constraint_value(field.kind, minimum))
2066 .is_some_and(|(value, minimum)| value > minimum),
2067 FieldConstraint::AtMost(maximum) => numeric_constraint_value(field.kind, value)
2068 .zip(numeric_constraint_value(field.kind, maximum))
2069 .is_some_and(|(value, maximum)| value <= maximum),
2070 FieldConstraint::Between { minimum, maximum } => {
2071 numeric_constraint_value(field.kind, value)
2072 .zip(numeric_constraint_value(field.kind, minimum))
2073 .zip(numeric_constraint_value(field.kind, maximum))
2074 .is_some_and(|((value, minimum), maximum)| (minimum..=maximum).contains(&value))
2075 }
2076 };
2077 if !valid {
2078 return Err(DatabaseError::InvalidInput);
2079 }
2080 }
2081 Ok(())
2082}
2083
2084fn numeric_constraint_value(kind: FieldType, value: &Value) -> Option<i64> {
2086 match kind {
2087 FieldType::Integer => value.as_i64(),
2088 FieldType::Decimal => decimal_cents(value).ok(),
2089 _ => None,
2090 }
2091}
2092
2093fn field_value_type_matches(kind: FieldType, value: &Value) -> bool {
2094 match kind {
2095 FieldType::Text | FieldType::Timestamp => value.is_string(),
2096 FieldType::Integer => value.as_i64().is_some(),
2097 FieldType::Boolean => value.is_boolean(),
2098 FieldType::Json => true,
2099 FieldType::Binary => value.is_array(),
2100 FieldType::Decimal => decimal_cents(value).is_ok(),
2101 }
2102}
2103
2104pub fn validate_schema(schema: &AppSchema) -> Result<(), DatabaseError> {
2106 if !valid_identifier(&schema.app_id)
2107 || schema.entities.is_empty()
2108 || schema.entities.len() > 256
2109 {
2110 return Err(DatabaseError::InvalidContract);
2111 }
2112 let mut names = BTreeSet::new();
2113 let mut index_names = schema
2114 .entities
2115 .iter()
2116 .map(|entity| entity.name.to_ascii_lowercase())
2117 .collect::<BTreeSet<_>>();
2118 for entity in &schema.entities {
2119 if !valid_app_identifier(&entity.name) || !names.insert(entity.name.to_ascii_lowercase()) {
2120 return Err(DatabaseError::InvalidContract);
2121 }
2122 if entity.fields.is_empty() || entity.fields.len() > 256 {
2123 return Err(DatabaseError::InvalidContract);
2124 }
2125 let mut fields = BTreeSet::new();
2126 let mut folded_fields = BTreeSet::new();
2127 for field in &entity.fields {
2128 if !valid_app_identifier(&field.name)
2129 || !fields.insert(field.name.as_str())
2130 || !folded_fields.insert(field.name.to_ascii_lowercase())
2131 {
2132 return Err(DatabaseError::InvalidContract);
2133 }
2134 if field
2135 .max_length
2136 .is_some_and(|length| length == 0 || length > MAX_TEXT_LENGTH)
2137 {
2138 return Err(DatabaseError::InvalidContract);
2139 }
2140 if field.on_delete.is_some() != field.references.is_some() {
2141 return Err(DatabaseError::InvalidContract);
2142 }
2143 if field.on_delete == Some(OnDelete::SetNull) && !field.nullable {
2144 return Err(DatabaseError::InvalidContract);
2145 }
2146 match &field.default {
2147 Some(FieldDefault::Literal(value)) => {
2148 validate_field_value(field, value)
2149 .map_err(|_| DatabaseError::InvalidContract)?;
2150 }
2151 Some(FieldDefault::CurrentTimestamp) if field.kind != FieldType::Timestamp => {
2152 return Err(DatabaseError::InvalidContract);
2153 }
2154 _ => {}
2155 }
2156 if field.constraints.len() > 8 {
2157 return Err(DatabaseError::InvalidContract);
2158 }
2159 for constraint in &field.constraints {
2160 match constraint {
2161 FieldConstraint::Allowed(values) if values.is_empty() || values.len() > 64 => {
2162 return Err(DatabaseError::InvalidContract);
2163 }
2164 FieldConstraint::Allowed(values)
2165 if values.iter().any(|value| {
2166 value.is_null() || !field_value_type_matches(field.kind, value)
2167 }) =>
2168 {
2169 return Err(DatabaseError::InvalidContract);
2170 }
2171 FieldConstraint::LengthBetween { minimum, maximum }
2172 if minimum > maximum || *maximum > MAX_TEXT_LENGTH =>
2173 {
2174 return Err(DatabaseError::InvalidContract);
2175 }
2176 FieldConstraint::LengthBetween { .. } if field.kind != FieldType::Text => {
2177 return Err(DatabaseError::InvalidContract);
2178 }
2179 FieldConstraint::GreaterThan(value) | FieldConstraint::AtMost(value)
2180 if !matches!(field.kind, FieldType::Integer | FieldType::Decimal)
2181 || (field.kind == FieldType::Integer && value.as_i64().is_none())
2182 || (field.kind == FieldType::Decimal
2183 && decimal_cents(value).is_err()) =>
2184 {
2185 return Err(DatabaseError::InvalidContract);
2186 }
2187 FieldConstraint::Between { minimum, maximum }
2188 if !matches!(field.kind, FieldType::Integer | FieldType::Decimal)
2189 || match field.kind {
2190 FieldType::Integer => minimum.as_i64().zip(maximum.as_i64()),
2191 FieldType::Decimal => {
2192 decimal_cents(minimum).ok().zip(decimal_cents(maximum).ok())
2193 }
2194 _ => None,
2195 }
2196 .is_none_or(|(minimum, maximum)| minimum > maximum) =>
2197 {
2198 return Err(DatabaseError::InvalidContract);
2199 }
2200 _ => {}
2201 }
2202 }
2203 }
2204 match &entity.identity {
2205 Identity::AutoInteger(name) => {
2206 if !entity.fields.iter().any(|field| {
2207 field.name == *name && field.kind == FieldType::Integer && !field.nullable
2208 }) {
2209 return Err(DatabaseError::InvalidContract);
2210 }
2211 }
2212 Identity::Composite(names) => {
2213 let mut seen = BTreeSet::new();
2214 if names.is_empty()
2215 || names.len() > 8
2216 || names.iter().any(|name| {
2217 !seen.insert(name.as_str())
2218 || !entity
2219 .fields
2220 .iter()
2221 .any(|field| field.name == *name && !field.nullable)
2222 })
2223 {
2224 return Err(DatabaseError::InvalidContract);
2225 }
2226 }
2227 }
2228 let identity_keys = entity
2229 .identity
2230 .fields()
2231 .into_iter()
2232 .map(|name| IndexKey::Field(name.to_owned()))
2233 .collect::<Vec<_>>();
2234 crate::index_values::validate_index_budget(entity, &identity_keys)?;
2235 if entity.unique.iter().chain(&entity.indexes).any(|key| {
2236 key.is_empty()
2237 || key.len() > 16
2238 || key.iter().any(|field| !fields.contains(field.as_str()))
2239 }) {
2240 return Err(DatabaseError::InvalidContract);
2241 }
2242 if entity
2243 .unique
2244 .iter()
2245 .chain(&entity.indexes)
2246 .flatten()
2247 .any(|name| {
2248 entity.fields.iter().any(|field| {
2249 field.name == *name
2250 && field.kind == FieldType::Text
2251 && field.max_length.is_none()
2252 })
2253 })
2254 {
2255 return Err(DatabaseError::InvalidContract);
2256 }
2257 if entity.conditional_indexes.len() > 128 {
2258 return Err(DatabaseError::InvalidContract);
2259 }
2260 crate::index_values::validate_lowercase_sources(entity)?;
2261 for (prefix, keys) in [("uq", &entity.unique), ("ix", &entity.indexes)] {
2262 for key in keys {
2263 crate::index_values::validate_index_budget(
2264 entity,
2265 &key.iter().cloned().map(IndexKey::Field).collect::<Vec<_>>(),
2266 )?;
2267 let name = generated_index_name(prefix, &entity.name, key)?;
2268 if !index_names.insert(name.to_ascii_lowercase()) {
2269 return Err(DatabaseError::InvalidContract);
2270 }
2271 }
2272 }
2273 for index in &entity.conditional_indexes {
2274 crate::index_values::validate_index_budget(entity, &index.keys)?;
2275 if !valid_app_identifier(&index.name)
2276 || index.name.eq_ignore_ascii_case("primary")
2277 || index.name.eq_ignore_ascii_case("_id_")
2278 || !index_names.insert(index.name.to_ascii_lowercase())
2279 || index.keys.is_empty()
2280 || index.keys.len() > 16
2281 {
2282 return Err(DatabaseError::InvalidContract);
2283 }
2284 for key in &index.keys {
2285 let field = match key {
2286 IndexKey::Field(name) | IndexKey::Lowercase(name) => name,
2287 };
2288 if !fields.contains(field.as_str()) {
2289 return Err(DatabaseError::InvalidContract);
2290 }
2291 }
2292 if let Some(filter) = &index.filter {
2293 validate_predicate(entity, filter)?;
2294 }
2295 }
2296 crate::index_values::validate_entity_budget(entity)?;
2297 }
2298 for entity in &schema.entities {
2299 for field in &entity.fields {
2300 if let Some((other_entity, other_field)) = &field.references {
2301 let Some(target) = schema
2302 .entities
2303 .iter()
2304 .find(|candidate| candidate.name == *other_entity)
2305 else {
2306 return Err(DatabaseError::InvalidContract);
2307 };
2308 let Some(target_field) = target
2309 .fields
2310 .iter()
2311 .find(|candidate| candidate.name == *other_field)
2312 else {
2313 return Err(DatabaseError::InvalidContract);
2314 };
2315 if field.kind != target_field.kind
2316 || !(target.identity.fields().contains(&other_field.as_str())
2317 && target.identity.fields().len() == 1
2318 || target
2319 .unique
2320 .iter()
2321 .any(|key| key.len() == 1 && key[0] == *other_field))
2322 {
2323 return Err(DatabaseError::InvalidContract);
2324 }
2325 }
2326 }
2327 }
2328 Ok(())
2329}
2330
2331pub fn entity<'a>(schema: &'a AppSchema, name: &str) -> Result<&'a Entity, DatabaseError> {
2333 schema
2334 .entities
2335 .iter()
2336 .find(|entity| entity.name == name)
2337 .ok_or(DatabaseError::InvalidContract)
2338}
2339
2340pub fn validate_predicate(entity: &Entity, predicate: &Predicate) -> Result<(), DatabaseError> {
2342 let mut pending = vec![(predicate, 0usize)];
2343 let fields = entity
2344 .fields
2345 .iter()
2346 .map(|field| field.name.as_str())
2347 .collect::<BTreeSet<_>>();
2348 let mut nodes = 0usize;
2349 while let Some((part, depth)) = pending.pop() {
2350 nodes += 1;
2351 if nodes > 256 || depth > 8 {
2352 return Err(DatabaseError::InvalidInput);
2353 }
2354 match part {
2355 Predicate::Equal(field, _)
2356 | Predicate::GeneratedEqual(field, _)
2357 | Predicate::NotEqual(field, _)
2358 | Predicate::LessThan(field, _)
2359 | Predicate::AtMost(field, _)
2360 | Predicate::GreaterThan(field, _)
2361 | Predicate::AtLeast(field, _)
2362 | Predicate::Contains(field, _)
2363 | Predicate::IsNull(field)
2364 | Predicate::In(field, _) => {
2365 if !fields.contains(field.as_str()) {
2366 return Err(DatabaseError::InvalidInput);
2367 }
2368 let declaration = entity
2369 .fields
2370 .iter()
2371 .find(|item| item.name == *field)
2372 .ok_or(DatabaseError::InvalidInput)?;
2373 match part {
2374 Predicate::Equal(_, value) | Predicate::NotEqual(_, value) => {
2375 if !value.is_null() {
2376 validate_value(declaration, value, false)?;
2377 }
2378 }
2379 Predicate::GeneratedEqual(_, _) => {
2380 if declaration.kind != FieldType::Integer {
2381 return Err(DatabaseError::InvalidInput);
2382 }
2383 }
2384 Predicate::LessThan(_, value)
2385 | Predicate::AtMost(_, value)
2386 | Predicate::GreaterThan(_, value)
2387 | Predicate::AtLeast(_, value) => {
2388 if value.is_null() {
2389 return Err(DatabaseError::InvalidInput);
2390 }
2391 validate_value(declaration, value, false)?;
2392 }
2393 Predicate::In(_, values) => {
2394 if values.len() > 64 {
2395 return Err(DatabaseError::InvalidInput);
2396 }
2397 for value in values {
2398 if value.is_null() {
2399 return Err(DatabaseError::InvalidInput);
2400 }
2401 validate_value(declaration, value, false)?;
2402 }
2403 }
2404 Predicate::IsNull(_) => {}
2405 Predicate::Contains(_, needle) => {
2406 if declaration.kind != FieldType::Text
2407 || needle.is_empty()
2408 || needle.len() > 256
2409 || needle.contains('\0')
2410 {
2411 return Err(DatabaseError::InvalidInput);
2412 }
2413 }
2414 Predicate::All(_) | Predicate::Any(_) => unreachable!(),
2415 }
2416 }
2417 Predicate::All(parts) | Predicate::Any(parts) => {
2418 if parts.is_empty() || parts.len() > 32 {
2419 return Err(DatabaseError::InvalidInput);
2420 }
2421 pending.extend(parts.iter().map(|part| (part, depth + 1)));
2422 }
2423 }
2424 }
2425 Ok(())
2426}
2427
2428pub struct RelatedRowsQuery<'a> {
2430 pub entity: &'a str,
2431 pub foreign_key: &'a str,
2432 pub keys: &'a [Value],
2433 pub fields: &'a [String],
2434 pub maximum: usize,
2435}
2436
2437pub fn query_related_rows(
2441 adapter: &dyn DatabaseOperations,
2442 config: &DbBackendConfig,
2443 schema: &AppSchema,
2444 query: RelatedRowsQuery<'_>,
2445) -> Result<Vec<serde_json::Map<String, Value>>, DatabaseError> {
2446 let RelatedRowsQuery {
2447 entity: entity_name,
2448 foreign_key,
2449 keys,
2450 fields,
2451 maximum,
2452 } = query;
2453 if keys.len() > 1000 || maximum == 0 || maximum > 10_000 {
2454 return Err(DatabaseError::InvalidInput);
2455 }
2456 let definition = entity(schema, entity_name)?;
2457 let selected = if fields.is_empty() {
2458 definition
2459 .fields
2460 .iter()
2461 .map(|field| field.name.clone())
2462 .collect()
2463 } else {
2464 fields.to_vec()
2465 };
2466 let mut unique = BTreeMap::new();
2467 for key in keys {
2468 if key.is_null() || key.is_array() || key.is_object() {
2469 return Err(DatabaseError::InvalidInput);
2470 }
2471 unique.insert(
2472 serde_json::to_string(key).map_err(|_| DatabaseError::InvalidInput)?,
2473 key.clone(),
2474 );
2475 }
2476 let keys = unique.into_values().collect::<Vec<_>>();
2477 let mut result = Vec::new();
2478 for keys in keys.chunks(64) {
2479 let mut offset = 0;
2480 loop {
2481 let limit = (maximum - result.len() + 1).min(1000) as u32;
2482 let rows = adapter.query(
2483 config,
2484 schema,
2485 &Query {
2486 entity: entity_name.to_owned(),
2487 fields: selected.clone(),
2488 filter: Some(Predicate::In(foreign_key.to_owned(), keys.to_vec())),
2489 order: match &definition.identity {
2490 Identity::AutoInteger(field) => vec![SortKey {
2491 field: field.clone(),
2492 descending: false,
2493 }],
2494 Identity::Composite(fields) => fields
2495 .iter()
2496 .map(|field| SortKey {
2497 field: field.clone(),
2498 descending: false,
2499 })
2500 .collect(),
2501 },
2502 limit,
2503 offset,
2504 },
2505 )?;
2506 let received = rows.len();
2507 result.extend(rows);
2508 if result.len() > maximum {
2509 return Err(DatabaseError::CapacityExceeded);
2510 }
2511 if received < limit as usize {
2512 break;
2513 }
2514 offset += limit;
2515 }
2516 }
2517 Ok(result)
2518}
2519
2520pub fn validate_query(schema: &AppSchema, query: &Query) -> Result<(), DatabaseError> {
2522 validate_schema(schema)?;
2523 let entity = entity(schema, &query.entity)?;
2524 let fields = entity
2525 .fields
2526 .iter()
2527 .map(|field| field.name.as_str())
2528 .collect::<BTreeSet<_>>();
2529 if query.fields.is_empty()
2530 || query.fields.len() > 256
2531 || query
2532 .fields
2533 .iter()
2534 .any(|field| !fields.contains(field.as_str()))
2535 || query.order.len() > 8
2536 || query
2537 .order
2538 .iter()
2539 .any(|key| !fields.contains(key.field.as_str()))
2540 || query.limit == 0
2541 || query.limit > 1000
2542 || query.offset > 1_000_000
2543 {
2544 return Err(DatabaseError::InvalidInput);
2545 }
2546 if serde_json::to_vec(query)
2547 .map_err(|_| DatabaseError::InvalidInput)?
2548 .len()
2549 > 1_048_576
2550 {
2551 return Err(DatabaseError::InvalidInput);
2552 }
2553 if let Some(filter) = &query.filter {
2554 validate_predicate(entity, filter)?;
2555 materialize_generated_predicate(filter, &[])?;
2556 }
2557 Ok(())
2558}
2559
2560pub fn validate_transaction(
2562 schema: &AppSchema,
2563 transaction: &Transaction,
2564) -> Result<(), DatabaseError> {
2565 validate_schema(schema)?;
2566 if transaction.steps.is_empty()
2567 || transaction.steps.len() > MAX_TRANSACTION_STEPS
2568 || transaction
2569 .idempotency_key
2570 .as_ref()
2571 .is_some_and(|key| key.is_empty() || key.len() > 128)
2572 {
2573 return Err(DatabaseError::InvalidInput);
2574 }
2575 if serde_json::to_vec(transaction)
2576 .map_err(|_| DatabaseError::InvalidInput)?
2577 .len()
2578 > 8 * 1024 * 1024
2579 {
2580 return Err(DatabaseError::InvalidInput);
2581 }
2582 for (step_index, step) in transaction.steps.iter().enumerate() {
2583 let (name, values, filter) = match step {
2584 Mutation::Insert { entity, values }
2585 | Mutation::Upsert { entity, values, .. }
2586 | Mutation::InsertIfAbsent { entity, values, .. }
2587 | Mutation::Reserve { entity, values, .. } => (entity, Some(values), None),
2588 Mutation::Update {
2589 entity,
2590 values,
2591 filter,
2592 ..
2593 } => (entity, Some(values), Some(filter)),
2594 Mutation::Delete { entity, filter, .. }
2595 | Mutation::RequireCount { entity, filter, .. } => (entity, None, Some(filter)),
2596 };
2597 let declaration = entity(schema, name)?;
2598 if let Mutation::Reserve {
2599 status_field,
2600 active_statuses,
2601 owner_filter,
2602 scope_filter,
2603 lock_scope_filter,
2604 capacity,
2605 expiry,
2606 ..
2607 } = step
2608 {
2609 let status = declaration
2610 .fields
2611 .iter()
2612 .find(|field| field.name == *status_field)
2613 .ok_or(DatabaseError::InvalidContract)?;
2614 if active_statuses.is_empty() || active_statuses.len() > 16 || *capacity > 100_000 {
2615 return Err(DatabaseError::InvalidInput);
2616 }
2617 for value in active_statuses {
2618 validate_field_value(status, value)?;
2619 }
2620 validate_predicate(declaration, owner_filter)?;
2621 validate_predicate(declaration, scope_filter)?;
2622 validate_generated_predicate_steps(owner_filter, &transaction.steps, step_index)?;
2623 validate_generated_predicate_steps(scope_filter, &transaction.steps, step_index)?;
2624 if let Some(lock_scope_filter) = lock_scope_filter {
2625 validate_predicate(declaration, lock_scope_filter)?;
2626 validate_generated_predicate_steps(
2627 lock_scope_filter,
2628 &transaction.steps,
2629 step_index,
2630 )?;
2631 }
2632 if let Some(expiry) = expiry {
2633 if let Some(filter) = &expiry.filter {
2634 validate_predicate(declaration, filter)?;
2635 validate_generated_predicate_steps(filter, &transaction.steps, step_index)?;
2636 }
2637 let timestamp = declaration
2638 .fields
2639 .iter()
2640 .find(|field| field.name == expiry.timestamp_field)
2641 .ok_or(DatabaseError::InvalidContract)?;
2642 validate_field_value(timestamp, &expiry.cutoff)?;
2643 validate_field_value(status, &expiry.to_status)?;
2644 if expiry.statuses.is_empty() || expiry.statuses.len() > 16 {
2645 return Err(DatabaseError::InvalidInput);
2646 }
2647 for value in &expiry.statuses {
2648 validate_field_value(status, value)?;
2649 }
2650 for extra in [&expiry.released_at, &expiry.reason].into_iter().flatten() {
2651 let field = declaration
2652 .fields
2653 .iter()
2654 .find(|field| field.name == extra.0)
2655 .ok_or(DatabaseError::InvalidContract)?;
2656 validate_field_value(field, &extra.1)?;
2657 }
2658 }
2659 }
2660 let fields = declaration
2661 .fields
2662 .iter()
2663 .map(|field| field.name.as_str())
2664 .collect::<BTreeSet<_>>();
2665 if let Some(values) = values
2666 && (values.is_empty()
2667 || values.len() > 64
2668 || values.keys().any(|name| !fields.contains(name.as_str())))
2669 {
2670 return Err(DatabaseError::InvalidInput);
2671 }
2672 if let Some(values) = values
2673 && values.values().any(|value| {
2674 value.generated_index().is_some_and(|index| {
2675 index >= step_index
2676 || !matches!(
2677 transaction.steps[index],
2678 Mutation::Insert { .. } | Mutation::InsertIfAbsent { .. }
2679 )
2680 })
2681 })
2682 {
2683 return Err(DatabaseError::InvalidInput);
2684 }
2685 if let Some(values) = values {
2686 for (name, value) in values {
2687 let field = declaration
2688 .fields
2689 .iter()
2690 .find(|field| field.name == *name)
2691 .ok_or(DatabaseError::InvalidInput)?;
2692 match value {
2693 WriteValue::Literal(value) => validate_field_value(field, value)?,
2694 WriteValue::GeneratedId(index)
2695 | WriteValue::GeneratedText { index, .. }
2696 | WriteValue::GeneratedJson { index, .. } => {
2697 let source = match transaction.steps.get(*index) {
2698 Some(
2699 Mutation::Insert { entity, .. }
2700 | Mutation::InsertIfAbsent { entity, .. },
2701 ) => entity,
2702 _ => return Err(DatabaseError::InvalidInput),
2703 };
2704 if entity(schema, source)?.identity.generated_field().is_none() {
2705 return Err(DatabaseError::InvalidInput);
2706 }
2707 match value {
2708 WriteValue::GeneratedId(_) if field.kind == FieldType::Integer => {}
2709 WriteValue::GeneratedText { prefix, .. }
2710 if field.kind == FieldType::Text && prefix.len() <= 128 =>
2711 {
2712 if field.max_length.is_some_and(|maximum| {
2713 prefix.chars().count() + 20 > maximum as usize
2714 }) {
2715 return Err(DatabaseError::InvalidInput);
2716 }
2717 }
2718 WriteValue::GeneratedJson {
2719 marker, template, ..
2720 } if field.kind == FieldType::Json
2721 && marker.len() <= 128
2722 && marker.ends_with(":0") =>
2723 {
2724 validate_field_value(field, template)?;
2725 }
2726 _ => return Err(DatabaseError::InvalidInput),
2727 }
2728 }
2729 }
2730 }
2731 if matches!(
2732 step,
2733 Mutation::Insert { .. }
2734 | Mutation::Upsert { .. }
2735 | Mutation::InsertIfAbsent { .. }
2736 | Mutation::Reserve { .. }
2737 ) && declaration.fields.iter().any(|field| {
2738 !field.nullable
2739 && field.default.is_none()
2740 && declaration.identity.generated_field() != Some(field.name.as_str())
2741 && !values.contains_key(&field.name)
2742 }) {
2743 return Err(DatabaseError::InvalidInput);
2744 }
2745 if matches!(step, Mutation::Update { .. })
2746 && declaration
2747 .identity
2748 .fields()
2749 .iter()
2750 .any(|name| values.contains_key(*name))
2751 {
2752 return Err(DatabaseError::InvalidInput);
2753 }
2754 }
2755 if let Some(filter) = filter {
2756 validate_predicate(declaration, filter)?;
2757 validate_generated_predicate_steps(filter, &transaction.steps, step_index)?;
2758 }
2759 if let Mutation::Upsert {
2760 unique_key, values, ..
2761 }
2762 | Mutation::InsertIfAbsent {
2763 unique_key, values, ..
2764 } = step
2765 && (!declaration.accepts_upsert_key(unique_key)
2766 || unique_key.iter().any(|field| !values.contains_key(field)))
2767 {
2768 return Err(DatabaseError::InvalidContract);
2769 }
2770 if let Mutation::RequireCount {
2771 minimum,
2772 maximum: Some(maximum),
2773 ..
2774 } = step
2775 && minimum > maximum
2776 {
2777 return Err(DatabaseError::InvalidInput);
2778 }
2779 }
2780 Ok(())
2781}
2782
2783pub fn guard_read_set(
2786 schema: &AppSchema,
2787 entity_name: &str,
2788 rows: &[&serde_json::Map<String, Value>],
2789) -> Result<Vec<Mutation>, DatabaseError> {
2790 let declaration = entity(schema, entity_name)?;
2791 if rows.len() > 100 {
2792 return Err(DatabaseError::CapacityExceeded);
2793 }
2794 if rows.is_empty() {
2795 return Ok(Vec::new());
2796 }
2797 let identity = declaration.identity.fields();
2798 let complete = rows
2799 .iter()
2800 .all(|row| identity.iter().all(|field| row.contains_key(*field)));
2801 let fields = rows
2802 .iter()
2803 .map(|row| row.len())
2804 .max()
2805 .ok_or(DatabaseError::InvalidInput)?;
2806 if fields == 0 || fields > 64 {
2807 return Err(DatabaseError::InvalidInput);
2808 }
2809 let group_size = if complete {
2810 (255 / (fields + 3)).clamp(1, 32)
2811 } else {
2812 1
2813 };
2814 let mut seen = BTreeSet::new();
2815 let mut guards = Vec::new();
2816 for chunk in rows.chunks(group_size) {
2817 let mut alternatives = Vec::with_capacity(chunk.len());
2818 for row in chunk {
2819 if complete {
2820 let key = identity
2821 .iter()
2822 .map(|field| &row[*field])
2823 .collect::<Vec<_>>();
2824 if !seen.insert(operation_fingerprint(&key)?) {
2825 return Err(DatabaseError::InvalidInput);
2826 }
2827 }
2828 let predicates = row
2829 .iter()
2830 .map(|(field, value)| {
2831 if value.is_null() {
2832 Predicate::IsNull(field.clone())
2833 } else {
2834 Predicate::Equal(field.clone(), value.clone())
2835 }
2836 })
2837 .collect::<Vec<_>>();
2838 let mut groups = predicates
2839 .chunks(32)
2840 .map(|chunk| Predicate::All(chunk.to_vec()))
2841 .collect::<Vec<_>>();
2842 alternatives.push(if groups.len() == 1 {
2843 groups.remove(0)
2844 } else {
2845 Predicate::All(groups)
2846 });
2847 }
2848 let filter = if alternatives.len() == 1 {
2849 alternatives.remove(0)
2850 } else {
2851 Predicate::Any(alternatives)
2852 };
2853 validate_predicate(declaration, &filter)?;
2854 guards.push(Mutation::RequireCount {
2855 entity: entity_name.to_owned(),
2856 filter,
2857 minimum: chunk.len() as u64,
2858 maximum: Some(chunk.len() as u64),
2859 });
2860 }
2861 Ok(guards)
2862}
2863
2864#[cfg(test)]
2865mod fingerprint_tests {
2866 use super::{bytes_fingerprint, operation_fingerprint};
2867
2868 #[test]
2869 fn nested_object_order_does_not_change_contract_checksum() {
2870 let mut nested = serde_json::Map::new();
2871 nested.insert("c".to_owned(), serde_json::json!(3));
2872 nested.insert("b".to_owned(), serde_json::json!(2));
2873 let mut object = serde_json::Map::new();
2874 object.insert("z".to_owned(), serde_json::Value::Object(nested));
2875 object.insert("a".to_owned(), serde_json::json!(1));
2876 let checksum = operation_fingerprint(&serde_json::Value::Object(object))
2877 .expect("bounded contract checksum");
2878 assert_eq!(checksum, bytes_fingerprint(br#"{"a":1,"z":{"b":2,"c":3}}"#));
2879 }
2880}
2881
2882#[cfg(test)]
2883mod generated_value_tests {
2884 use super::*;
2885 use serde_json::json;
2886
2887 #[test]
2889 fn json_nul_is_rejected_at_the_shared_boundary() {
2890 for value in [json!("a\0b"), json!({"a\0b": 1}), json!([{"nested":"\0"}])] {
2891 assert_eq!(
2892 validate_json_value(&value),
2893 Err(DatabaseError::InvalidInput)
2894 );
2895 }
2896 assert_eq!(validate_json_value(&json!([0, 1, 255])), Ok(()));
2897 }
2898
2899 #[test]
2901 fn query_budget_rejects_the_first_overflowing_record() {
2902 let mut budget = QueryResultBudget {
2903 bytes: 8 * 1024 * 1024 - 2,
2904 };
2905 assert_eq!(budget.accept(&Map::new()), Ok(()));
2906 assert_eq!(
2907 budget.accept(&Map::new()),
2908 Err(DatabaseError::CapacityExceeded)
2909 );
2910 }
2911
2912 #[test]
2914 fn json_numeric_identity_is_backend_neutral() {
2915 assert_eq!(
2916 canonical_json_value(&json!({"n": [1.0, -0.0, 1.25], "text": "1.0", "wide": u64::MAX})),
2917 Ok(json!({"n": [1, 0, 1.25], "text": "1.0", "wide": u64::MAX}))
2918 );
2919 let mut nested = Value::Null;
2920 for _ in 0..34 {
2921 nested = Value::Array(vec![nested]);
2922 }
2923 assert_eq!(
2924 canonical_json_value(&nested),
2925 Err(DatabaseError::CapacityExceeded)
2926 );
2927 assert_eq!(
2928 serde_json::to_string(
2929 &canonical_json_value(&json!({"z": {"b": 1, "a": 2}, "a": 0})).unwrap()
2930 )
2931 .unwrap(),
2932 r#"{"a":0,"z":{"a":2,"b":1}}"#
2933 );
2934 }
2935
2936 #[test]
2938 fn generated_text_validates_the_resolved_value() {
2939 let field = Field {
2940 name: "resource_id".to_owned(),
2941 kind: FieldType::Text,
2942 nullable: false,
2943 max_length: Some(8),
2944 default: None,
2945 references: None,
2946 on_delete: None,
2947 constraints: Vec::new(),
2948 };
2949 let expression = WriteValue::GeneratedText {
2950 index: 0,
2951 prefix: "metric:".to_owned(),
2952 };
2953 assert_eq!(
2954 resolve_field_write(&field, &expression, &[Some(1)]),
2955 Ok(json!("metric:1"))
2956 );
2957 assert_eq!(
2958 resolve_field_write(&field, &expression, &[Some(100)]),
2959 Err(DatabaseError::InvalidInput)
2960 );
2961 assert_eq!(
2962 resolve_field_write(&field, &expression, &[None]),
2963 Err(DatabaseError::InvalidInput)
2964 );
2965 }
2966
2967 #[test]
2969 fn generated_json_changes_only_resource_keys() {
2970 let expression = WriteValue::GeneratedJson {
2971 index: 0,
2972 marker: "metric:0".to_owned(),
2973 template: json!({"resource_id": "metric:0", "note": "metric:0", "nested": ["metric:0", {"resource_id": "metric:0"}]}),
2974 };
2975 assert_eq!(
2976 resolve_write_value(&expression, &[Some(42)]),
2977 Ok(json!({
2978 "resource_id": "metric:42", "note": "metric:0", "nested": ["metric:0", {"resource_id": "metric:42"}]
2979 }))
2980 );
2981 }
2982}
2983
2984#[cfg(test)]
2985mod timestamp_storage_tests {
2986 use super::canonical_stored_timestamp;
2987
2988 #[test]
2990 fn timestamp_storage_preserves_fractional_instants() {
2991 assert_eq!(
2992 super::stored_session_expiry_epoch("2033-05-18T03:33:20.000000001Z").unwrap(),
2993 2_000_000_001
2994 );
2995 assert_eq!(
2996 super::stored_session_expiry_epoch("2033-05-18T03:33:20Z").unwrap(),
2997 2_000_000_000
2998 );
2999 assert_eq!(
3000 canonical_stored_timestamp("2026-10-06 01:02:03.123456").unwrap(),
3001 "2026-10-06T01:02:03.123456000Z"
3002 );
3003 assert_eq!(
3004 canonical_stored_timestamp("2026-10-06T10:02:03.123456+09:00").unwrap(),
3005 "2026-10-06T01:02:03.123456000Z"
3006 );
3007 assert_eq!(
3008 canonical_stored_timestamp("2026-10-06T01:02:03Z").unwrap(),
3009 "2026-10-06T01:02:03.000000000Z"
3010 );
3011 assert_eq!(
3012 canonical_stored_timestamp("1").unwrap(),
3013 "1970-01-01T00:00:01.000000000Z"
3014 );
3015 for invalid in ["", "2026-13-01 00:00:00", "2030-01-01"] {
3016 assert!(canonical_stored_timestamp(invalid).is_err());
3017 }
3018 }
3019}
3020
3021#[cfg(test)]
3022mod storage_namespace_tests {
3023 use super::*;
3024
3025 fn simple_schema() -> AppSchema {
3027 AppSchema {
3028 app_id: "namespace_test".to_owned(),
3029 entities: vec![Entity {
3030 name: "record".to_owned(),
3031 identity: Identity::AutoInteger("id".to_owned()),
3032 fields: vec![Field {
3033 name: "id".to_owned(),
3034 kind: FieldType::Integer,
3035 nullable: false,
3036 max_length: None,
3037 default: None,
3038 references: None,
3039 on_delete: None,
3040 constraints: Vec::new(),
3041 }],
3042 unique: Vec::new(),
3043 indexes: Vec::new(),
3044 conditional_indexes: Vec::new(),
3045 }],
3046 }
3047 }
3048
3049 #[test]
3051 fn transaction_step_limit_supports_complete_app_indexes_and_remains_bounded() {
3052 let schema = simple_schema();
3053 let step = Mutation::RequireCount {
3054 entity: "record".to_owned(),
3055 filter: Predicate::Equal("id".to_owned(), serde_json::json!(1)),
3056 minimum: 0,
3057 maximum: None,
3058 };
3059 let mut transaction = Transaction {
3060 steps: vec![step.clone(); MAX_TRANSACTION_STEPS],
3061 idempotency_key: None,
3062 };
3063 assert_eq!(validate_transaction(&schema, &transaction), Ok(()));
3064 transaction.steps.push(step);
3065 assert_eq!(
3066 validate_transaction(&schema, &transaction),
3067 Err(DatabaseError::InvalidInput)
3068 );
3069 transaction.steps = vec![
3070 Mutation::RequireCount {
3071 entity: "record".to_owned(),
3072 filter: Predicate::Equal("id".to_owned(), serde_json::json!("a".repeat(64 * 1024))),
3073 minimum: 0,
3074 maximum: None,
3075 };
3076 128
3077 ];
3078 assert!(serde_json::to_vec(&transaction).unwrap().len() > 8 * 1024 * 1024);
3079 assert_eq!(
3080 validate_transaction(&schema, &transaction),
3081 Err(DatabaseError::InvalidInput)
3082 );
3083 }
3084
3085 #[test]
3087 fn ambiguous_names_fail_before_adapter_dispatch() {
3088 let mut schema = simple_schema();
3089 let mut duplicate = schema.entities[0].clone();
3090 duplicate.name = "Record".to_owned();
3091 schema.entities.push(duplicate);
3092 assert_eq!(
3093 validate_schema(&schema),
3094 Err(DatabaseError::InvalidContract)
3095 );
3096
3097 let mut schema = simple_schema();
3098 let mut duplicate = schema.entities[0].fields[0].clone();
3099 duplicate.name = "ID".to_owned();
3100 schema.entities[0].fields.push(duplicate);
3101 assert_eq!(
3102 validate_schema(&schema),
3103 Err(DatabaseError::InvalidContract)
3104 );
3105
3106 for name in ["record", "PRIMARY", "_id_", "ix_record_id"] {
3107 let mut schema = simple_schema();
3108 schema.entities[0].indexes.push(vec!["id".to_owned()]);
3109 schema.entities[0].conditional_indexes.push(Index {
3110 name: name.to_owned(),
3111 keys: vec![IndexKey::Field("id".to_owned())],
3112 unique: false,
3113 filter: None,
3114 });
3115 assert_eq!(
3116 validate_schema(&schema),
3117 Err(DatabaseError::InvalidContract),
3118 "{name}"
3119 );
3120 }
3121
3122 let mut schema = simple_schema();
3123 schema.entities[0].indexes = vec![vec!["id".to_owned()], vec!["id".to_owned()]];
3124 assert_eq!(
3125 validate_schema(&schema),
3126 Err(DatabaseError::InvalidContract)
3127 );
3128
3129 let mut schema = simple_schema();
3130 schema.entities[0].conditional_indexes = vec![Index {
3131 name: "record_custom".to_owned(),
3132 keys: vec![IndexKey::Field("id".to_owned())],
3133 unique: false,
3134 filter: None,
3135 }];
3136 let mut other = schema.entities[0].clone();
3137 other.name = "other".to_owned();
3138 other.conditional_indexes[0].name = "Record_Custom".to_owned();
3139 schema.entities.push(other);
3140 assert_eq!(
3141 validate_schema(&schema),
3142 Err(DatabaseError::InvalidContract)
3143 );
3144 }
3145
3146 #[test]
3148 fn foreign_key_names_are_portable_and_stable() {
3149 assert_eq!(
3150 generated_index_name("fk", "child", &["parent_id".to_owned()]).unwrap(),
3151 "fk_child_parent_id"
3152 );
3153 let table = "a".repeat(63);
3154 let key = "b".repeat(63);
3155 let name = generated_index_name("fk", &table, std::slice::from_ref(&key)).unwrap();
3156 assert_eq!(name.len(), 63);
3157 assert!(valid_identifier(&name));
3158 assert_eq!(
3159 generated_index_name("fk", &table, std::slice::from_ref(&key)).unwrap(),
3160 name
3161 );
3162 assert_ne!(
3163 generated_index_name("fk", &table, &["c".repeat(63)]).unwrap(),
3164 name
3165 );
3166 }
3167
3168 #[test]
3170 fn internal_storage_identifiers_are_reserved() {
3171 let schema = AppSchema {
3172 app_id: "example".to_owned(),
3173 entities: vec![Entity {
3174 name: "record".to_owned(),
3175 identity: Identity::AutoInteger("id".to_owned()),
3176 fields: vec![Field {
3177 name: "id".to_owned(),
3178 kind: FieldType::Integer,
3179 nullable: false,
3180 max_length: None,
3181 default: None,
3182 references: None,
3183 on_delete: None,
3184 constraints: Vec::new(),
3185 }],
3186 unique: Vec::new(),
3187 indexes: Vec::new(),
3188 conditional_indexes: Vec::new(),
3189 }],
3190 };
3191 validate_schema(&schema).unwrap();
3192 for name in [
3193 "pharos_migration_ledger",
3194 "PHAROS_transaction_ledger",
3195 "_pharos_lower_name",
3196 "_id",
3197 "_ID",
3198 ] {
3199 let mut entity_collision = schema.clone();
3200 entity_collision.entities[0].name = name.to_owned();
3201 assert_eq!(
3202 validate_schema(&entity_collision),
3203 Err(DatabaseError::InvalidContract)
3204 );
3205 let mut field_collision = schema.clone();
3206 field_collision.entities[0].fields[0].name = name.to_owned();
3207 field_collision.entities[0].identity = Identity::AutoInteger(name.to_owned());
3208 assert_eq!(
3209 validate_schema(&field_collision),
3210 Err(DatabaseError::InvalidContract)
3211 );
3212 }
3213 for name in ["user_account", "business", "_custom_value"] {
3214 let mut ordinary = schema.clone();
3215 ordinary.entities[0].name = name.to_owned();
3216 validate_schema(&ordinary).unwrap();
3217 }
3218 }
3219}
3220
3221#[cfg(test)]
3222mod security_input_tests {
3223 use super::*;
3224
3225 #[test]
3227 fn security_keys_and_session_clocks_use_portable_input_bounds() {
3228 assert_eq!(
3229 validate_session_lookup("bad\0token", 0),
3230 Err(DatabaseError::InvalidInput)
3231 );
3232 assert_eq!(
3233 validate_session_lookup("hash", 253_402_300_800),
3234 Err(DatabaseError::InvalidInput)
3235 );
3236 assert_eq!(
3237 validate_session_lookup("hash", 253_402_300_799),
3238 Ok(253_402_300_799)
3239 );
3240 assert!(matches!(
3241 validate_session_issue(1, "bad\0token", "2030-01-01T00:00:00Z"),
3242 Err(DatabaseError::InvalidInput)
3243 ));
3244 assert_eq!(
3245 validate_rate_limit_request("bad\0policy", "bucket", 1, 1),
3246 Err(DatabaseError::InvalidInput)
3247 );
3248 assert_eq!(
3249 validate_rate_limit_request("policy", "bad\0bucket", 1, 1),
3250 Err(DatabaseError::InvalidInput)
3251 );
3252 assert_eq!(
3253 validate_rate_limit_event("policy", "bucket", "bad\0outcome"),
3254 Err(DatabaseError::InvalidInput)
3255 );
3256 assert_eq!(
3257 decode_aggregate_text(FieldType::Binary, "abc"),
3258 Err(DatabaseError::StorageFailure)
3259 );
3260 assert_eq!(
3261 decode_aggregate_text(FieldType::Binary, "0080ff"),
3262 Ok(serde_json::json!([0, 128, 255]))
3263 );
3264 }
3265}