Skip to main content

pharos_runtime_database/
operations.rs

1//! Logical data operations shared by apps and primary-backend implementations.
2//! App code supplies validated entity and field names; an adapter owns physical
3//! queries, connections, migrations, and durable transaction semantics.
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9use sha2::{Digest, Sha256};
10
11use crate::DbBackendConfig;
12
13/// Maximum declared text length; stored values also obey the one-MiB byte bound.
14pub const MAX_TEXT_LENGTH: u32 = 1_048_576;
15
16/// Maximum atomic write steps, independently bounded by the eight-MiB payload limit.
17pub const MAX_TRANSACTION_STEPS: usize = 2048;
18
19/// Stable failure categories; an unavailable backend never appears as empty data.
20#[derive(Debug, Clone, Copy, Eq, PartialEq)]
21pub enum DatabaseError {
22    InvalidContract,
23    InvalidInput,
24    Unsupported,
25    Unavailable,
26    Conflict,
27    /// A transient database lock prevented the operation from starting or committing.
28    Busy,
29    MissingRecord,
30    AccessDenied,
31    CapacityExceeded,
32    StorageFailure,
33}
34
35/// An unrevoked browser session resolved against the caller's clock.
36#[derive(Debug, Clone, Copy, Eq, PartialEq)]
37pub struct ActiveSession {
38    pub user_id: i64,
39    pub expires_at_epoch: u64,
40}
41
42/// Credentials and account status for shared password verification.
43pub struct UserCredentials {
44    pub user_id: i64,
45    pub password_hash: String,
46    pub is_active: bool,
47}
48
49/// One business visible through an active tenant membership.
50#[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
58/// Bounds one managed-business read before any adapter executes a query.
59pub 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
66/// Accepts a bounded tenant role identifier without interpreting its authority.
67pub 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
82/// Bounds a pre-hashed credential update before an adapter opens a connection.
83pub 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
97/// Reject malformed or unbounded session lookups before reaching a backend.
98pub 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
107/// Validated UTC expiry used by each backend's native session representation.
108pub struct SessionExpiry {
109    pub epoch: i64,
110    pub rfc3339_utc: String,
111}
112
113/// Validates session input and preserves the exact expiry instant.
114pub 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
140/// Encodes a whole-second comparison boundary using the canonical storage representation.
141pub 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
151/// Returns the first whole second at which an exact stored session expiry has passed.
152pub 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/// Declared field types. Decimal values use an exact, two-place decimal string.
164#[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
176/// Parses a bounded two-place decimal without floating-point arithmetic.
177/// JSON numbers are accepted at the input boundary; storage and reads use
178/// canonical strings so adapters cannot silently round monetary values.
179pub 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
215/// Encodes scaled integer storage as the public exact decimal representation.
216pub 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/// One logical field, independent of a SQL column or document representation.
227#[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/// A portable schema default, including database-generated current time.
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241#[serde(rename_all = "snake_case")]
242pub enum FieldDefault {
243    Literal(Value),
244    CurrentTimestamp,
245}
246
247/// Referential action applied when a referenced record is deleted.
248#[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/// Portable field checks retained when app SQL migrations become declarative.
257#[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/// A physical index key derived from a declared field.
268#[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/// Indexes may be unique and conditional without embedding vendor SQL.
276#[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/// One logical entity with declared uniqueness and lookup requirements.
285#[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    /// Accepts a declared unique key or primary identity as an upsert conflict target.
297    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/// A generated numeric key or a declared compound primary key.
310#[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    /// Returns the generated numeric field when an entity uses one.
319    pub fn generated_field(&self) -> Option<&str> {
320        match self {
321            Self::AutoInteger(name) => Some(name),
322            Self::Composite(_) => None,
323        }
324    }
325
326    /// Returns the ordered fields of the primary key.
327    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/// The app-owned logical schema that every configured backend must implement.
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337pub struct AppSchema {
338    pub app_id: String,
339    pub entities: Vec<Entity>,
340}
341
342/// Additive schema changes with a backend-independent migration identity.
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344#[serde(rename_all = "snake_case")]
345pub enum SchemaChange {
346    CreateEntity(Entity),
347    /// Retires exactly one obsolete physical constraint absent from the logical field.
348    /// Other historical constraints, records, identities and indexes remain intact.
349    RetireFieldConstraint {
350        entity: String,
351        field: String,
352        constraint: FieldConstraint,
353    },
354    /// Converts stored values and the physical field using one shared lossless preflight.
355    ConvertField {
356        entity: String,
357        previous: Field,
358        field: Field,
359    },
360    /// Widens an existing integer value to an exact two-place decimal without
361    /// changing its name, identity, or historical value.
362    WidenIntegerToDecimal {
363        entity: String,
364        field: String,
365    },
366    /// Adopts declared records from a historical JSON array into typed storage.
367    /// Source filtering and the explicit field list prevent importing unrelated
368    /// app data or accepting unrecognized columns.
369    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    /// Convert an existing single-column integer primary key into a generated key.
378    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    /// Creates a missing logical record from exactly one qualifying source row.
403    /// Existing target records are preserved; absent or ambiguous sources fail closed.
404    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/// One ordered migration whose checksum must match the committed ledger entry.
430#[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/// A bounded, parameterized predicate over declared fields.
439#[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/// One sort key for a bounded read; ascending puts nulls first and descending last.
457/// Include a unique identity key to make equal-value paging deterministic.
458#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
459pub struct SortKey {
460    pub field: String,
461    pub descending: bool,
462}
463
464/// A logical read; explicit fields and a bounded page prevent accidental scans.
465#[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
475/// Replaces transaction-local identity references with identities produced by earlier steps.
476pub 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
506/// Rejects an identity reference unless an earlier transaction step produces it.
507fn 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/// One bounded, backend-neutral join of two declared entities.
534#[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/// One join result retains the two entity namespaces to avoid field collisions.
547#[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/// Atomically selects one matching record and applies its lease or status fields.
554/// The adapter returns the selected numeric identity after the update commits.
555#[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/// Aggregates required by declared summaries and guarded writes.
564#[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/// One aggregate against an entity and optional filter.
574#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
575pub struct AggregateQuery {
576    pub entity: String,
577    pub filter: Option<Predicate>,
578    pub aggregate: Aggregate,
579}
580
581/// Identifies the logical type of an aggregate result for exact decoding.
582pub 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
605/// Decodes a relational aggregate without guessing a text field's logical type.
606pub 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/// Expiry transition applied before reservation capacity is measured.
647#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
648pub struct ReservationExpiry {
649    /// Additional declared eligibility, such as an unbound checkout reference.
650    #[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
660/// Combines reservation scope and expiry eligibility identically for every adapter.
661pub 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/// A mutation evaluated as one part of a durable transaction.
679#[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/// A write value can refer to an earlier inserted or ensured record.
728#[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    /// Identifies the prior transaction result used by a typed generated value.
746    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    /// Relocates references when a transaction-local plan is appended.
756    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
771/// Resolves generated references uniformly before any adapter binds parameters.
772/// JSON replacement matches the full declared resource marker, never substrings.
773pub 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
806/// Resolves transaction-local expressions and validates the resulting field value
807/// before a physical adapter binds it. Template admission alone cannot certify
808/// the size or constraints of the generated result.
809pub 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
819/// Replaces only declared resource keys within bounded JSON nesting.
820fn 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/// A transaction must commit every step or none, including guard decisions.
852#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
853pub struct Transaction {
854    pub steps: Vec<Mutation>,
855    pub idempotency_key: Option<String>,
856}
857
858/// Per-step affected row counts and numeric identities of inserted or ensured records.
859#[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/// One record in a bounded export page or import batch.
866#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
867pub struct Record {
868    pub entity: String,
869    pub fields: Map<String, Value>,
870}
871
872/// Bounds one returned page without creating a second serialized copy of its records.
873#[derive(Default)]
874pub struct QueryResultBudget {
875    bytes: usize,
876}
877
878impl QueryResultBudget {
879    /// Accounts for a decoded record before retaining it in the returned page.
880    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            /// Rejects encoded output before the page exceeds its fixed memory budget.
887            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            /// Completes a counter-only write without an external output buffer.
898            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/// Explicit resource bounds for a consistent operator snapshot.
913#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
914pub struct SnapshotLimits {
915    pub max_records: u32,
916    pub max_bytes: u64,
917}
918
919/// Shared keyset pagination and accounting. Adapters must keep every page in
920/// the same database read transaction; separate calls to query are insufficient.
921pub 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
1048/// Computes a stable SHA-256 fingerprint for a serialized logical operation.
1049pub 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
1056/// Writes JSON with recursively sorted object keys so migration and replay
1057/// hashes cannot depend on map insertion order or Serde feature unification.
1058fn 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
1107/// Computes a lowercase SHA-256 fingerprint without retaining source bytes.
1108pub 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
1118/// Decodes a logical byte array without interpreting ordinary JSON arrays as binary.
1119pub 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
1134/// Decodes persisted UTC timestamp text without losing fractional seconds.
1135/// RFC 3339 represents logical timestamps; native UTC date-time storage may
1136/// omit its zone. Adapters normalize that physical representation at adoption.
1137pub 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            // Historical physical timestamp columns also stored Unix seconds.
1147            // Logical input validation continues to require RFC 3339.
1148            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    // Fixed precision makes textual ordering agree with chronological ordering,
1158    // including zero fractions and instants sharing a fractional prefix.
1159    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
1166/// Validates JSON depth, node count and encoded size without copying the value tree.
1167pub 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        /// Counts encoded bytes and stops serialization at the shared field limit.
1200        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        /// The counter has no buffered output to flush.
1208        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
1215/// Canonicalizes bounded JSON values so equivalent integral numeric representations
1216/// and object-key order do not acquire backend-specific equality semantics.
1217/// Arrays retain their order, and strings never undergo numeric coercion.
1218pub fn canonical_json_value(value: &Value) -> Result<Value, DatabaseError> {
1219    validate_json_value(value)?;
1220    /// Walks a depth- and node-bounded tree before any serialization or adapter conversion.
1221    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
1262/// Validates a projected stored record against the same typed contract as writes.
1263/// Malformed persisted values indicate storage corruption, not invalid user input.
1264/// This operates on already-loaded rows and performs no additional database reads.
1265pub 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
1283/// Returns canonical typed values from an already-loaded projection without I/O.
1284pub 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
1305/// Executable logical operations implemented by every registered adapter.
1306/// App-facing services resolve this operation contract through the configured registry.
1307pub trait DatabaseOperations: Send + Sync {
1308    fn backend_name(&self) -> &'static str;
1309    /// Identifies an adapter-owned local database artifact format for safe historical adoption.
1310    /// Remote storage returns None; callers must never infer a format from a backend name.
1311    fn local_artifact_format(&self) -> Option<&'static str>;
1312    /// Ensures one app-owned database namespace exists before applying its schema.
1313    fn ensure_database(
1314        &self,
1315        config: &DbBackendConfig,
1316        schema: &AppSchema,
1317    ) -> Result<(), DatabaseError>;
1318    /// Loads credentials for one username without deciding password policy.
1319    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    /// Replaces one user's pre-hashed password. Missing users fail explicitly.
1368    fn update_user_password(
1369        &self,
1370        config: &DbBackendConfig,
1371        user_id: i64,
1372        password_hash: &str,
1373    ) -> Result<(), DatabaseError>;
1374    /// Creates or replaces one browser session in authoritative storage.
1375    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    /// Revokes one browser session by token hash.
1383    fn revoke_session(
1384        &self,
1385        config: &DbBackendConfig,
1386        session_token_hash: &str,
1387    ) -> Result<(), DatabaseError>;
1388    /// Revokes every browser session owned by one user.
1389    fn revoke_user_sessions(
1390        &self,
1391        config: &DbBackendConfig,
1392        user_id: i64,
1393    ) -> Result<(), DatabaseError>;
1394    /// Resolves one unrevoked browser session against the supplied clock.
1395    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    /// Records a bounded security admission outcome in authoritative storage.
1441    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    /// Atomically counts and records one request in a moving security window.
1449    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    /// Applies an app-owned logical migration sequence through this backend.
1458    /// `CreateEntity` creates a table or collection as appropriate for the
1459    /// configured adapter; callers never select a vendor migration file.
1460    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    /// Reports whether a declared field exists in authoritative storage.
1552    /// Document backends report declared fields because records have no fixed columns.
1553    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    /// Stamps one logical migration over a fully verified populated database.
1567    /// Adapters must compare the complete historical ledger, inspect the
1568    /// physical schema, and write the logical marker atomically. A mismatch
1569    /// must never trigger the migration body against populated data.
1570    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    /// Executes a bounded two-entity join through the same logical reads on every backend.
1584    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        // A full page could hide more matching rows, so fail closed instead of truncating a view.
1671        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    /// Exports all declared entities from one consistent read transaction.
1736    fn export_snapshot(
1737        &self,
1738        config: &DbBackendConfig,
1739        schema: &AppSchema,
1740        limits: SnapshotLimits,
1741    ) -> Result<Vec<Record>, DatabaseError>;
1742    /// Replaces declared app records atomically after validating schema and
1743    /// preconditions. No partial import batch may become visible on failure.
1744    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
1760/// Enforces the same bounded security keys before any adapter touches storage.
1761pub 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
1782/// Enforces audit row bounds without interpreting user input as query syntax.
1783pub 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
1802/// Validates a bounded claim and its deterministic selection order.
1803pub 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
1843/// Converts a primary-key cursor to a lexicographic page filter and ordering.
1844/// Composite cursors carry every key field in one JSON object.
1845pub 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
1893/// Accepts a name only if it is a bounded, portable identifier.
1894pub 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
1901/// Keeps app declarations separate from adapter metadata and derived storage keys.
1902fn 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
1913/// Derives one portable physical index name from a declared logical key.
1914pub 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
1934/// Checks a logical value before any adapter can map it to a driver parameter.
1935pub fn validate_field_value(field: &Field, value: &Value) -> Result<(), DatabaseError> {
1936    validate_value(field, value, true)
1937}
1938
1939/// Validates a historical field copy before any backend updates existing records.
1940pub 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
2084/// Converts a typed numeric constraint operand to its exact comparison unit.
2085fn 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
2104/// Validates the logical schema before an adapter can execute it.
2105pub 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
2331/// Finds a declared entity without accepting a physical table name from a request.
2332pub 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
2340/// Validates a nested predicate against one declared entity and a fixed depth bound.
2341pub 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
2428/// Declares one bounded relationship read independently of its configured adapter.
2429pub 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
2437/// Reads a bounded relationship set using the contract's operand and result
2438/// limits. Duplicate keys are removed before paging; no vendor query escapes
2439/// the operation interface.
2440pub 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
2520/// Validates one bounded query before lowering it to a backend operation.
2521pub 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
2560/// Validates a write transaction without exposing a backend statement surface.
2561pub 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
2783/// Guards a bounded read set while preserving transaction capacity for its writes.
2784/// Complete identities permit grouped checks; projections without identities use single-row checks.
2785pub 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    /// Rejects native-storage NUL incompatibilities before any adapter receives JSON.
2888    #[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    /// Rejects a page before retaining another record beyond the encoded result budget.
2900    #[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    /// Numeric aliases normalize recursively without coercing strings or reordering arrays.
2913    #[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    /// Generated references must obey final field constraints before binding.
2937    #[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    /// Resource replacement does not rewrite unrelated user text or nested scalars.
2968    #[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    /// Nanoseconds survive storage normalization and fractional expiry rounds forward.
2989    #[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    /// Supplies the smallest declared entity for portable namespace checks.
3026    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    /// Allows complete source indexes atomically while rejecting oversized transactions.
3050    #[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    /// Rejects declarations that alias another physical name on a primary adapter.
3086    #[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    /// Keeps short historical names unchanged and bounds long foreign-key names.
3147    #[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    /// App schemas cannot overwrite metadata or document identities on any backend.
3169    #[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    /// Native text and timestamp limitations never become backend-dependent errors.
3226    #[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}