Skip to main content

feltdb/
state_contract.rs

1//! Query Execution Contract
2//!
3//! This module defines the observable behavior of query execution in FeltDB.
4//! The query execution contract has three key guarantees:
5//!
6//! 1. PLANNER-EXECUTOR ALIGNMENT: The access_method field in QueryPlan indicates
7//!    whether an index will be used (value: "index_lookup") or a full scan (value: "full_table_scan").
8//!    The executor MUST honor this decision. If plan says "index_lookup", the executor MUST
9//!    use an index. Fallback to scan violates the contract.
10//!
11//! 2. ACTUAL MEASUREMENTS: All timing fields (planning_ms, index_lookup_ms, filtering_ms,
12//!    serialization_ms, total_ms) and metrics (index_hits, index_misses) MUST be actual
13//!    measurements from real execution using Instant::now(). Estimates or derived values
14//!    violate the contract. The index_metrics struct MUST reflect what actually executed,
15//!    not what was planned.
16//!
17//! 3. FALLBACK TRACKING: If a query cannot use an index (field not indexed, expression too complex),
18//!    the planner sets access_method: "full_table_scan" and fallback_reason describes why.
19//!    The executor then scans all rows. This is not a contract violation—it's the correct
20//!    fallback path. The violation only occurs if the planner said "use index" but executor
21//!    fell back to scan without updating these fields.
22
23use crate::{
24    AtomicMutation, AtomicPrecondition, FeltDb, FlowError, PreconditionFailure, RecordPrecondition,
25    StoredRow,
26    application::{ApplicationManifest, ApplicationRevision, FieldDefinition},
27    policy_evaluation::{
28        Actor, AuthorizationState, PolicyEvaluator, PolicySubject, RecordAuthorizationContext,
29    },
30};
31use serde::{Deserialize, Serialize};
32use serde_json::{Map, Value};
33use sha2::{Digest, Sha256};
34use std::{
35    cmp::Ordering,
36    collections::{BTreeMap, BTreeSet},
37    sync::Arc,
38    time::Instant,
39};
40
41pub const STATE_CONTRACT_VERSION: u32 = 1;
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum PrimitiveType {
46    String,
47    Integer,
48    Number,
49    Boolean,
50    Timestamp,
51    Date,
52    Time,
53    Json,
54    Uuid,
55    Decimal,
56    Money,
57    Bigint,
58    Email,
59    Url,
60    Phone,
61    Binary,
62    File,
63    GeoPoint,
64    Object,
65}
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67#[serde(tag = "kind", rename_all = "snake_case")]
68pub enum FieldType {
69    Primitive { primitive: PrimitiveType },
70    Enum { values: Vec<String> },
71    Reference { collection: String },
72    Array { items: Box<FieldType> },
73    Map { values: Box<FieldType> },
74    Vector { dimensions: Option<usize> },
75}
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct FieldConstraints {
78    #[serde(default)]
79    pub minimum: Option<f64>,
80    #[serde(default)]
81    pub maximum: Option<f64>,
82    #[serde(default)]
83    pub min_length: Option<usize>,
84    #[serde(default)]
85    pub max_length: Option<usize>,
86    #[serde(default)]
87    pub pattern: Option<String>,
88}
89impl Default for FieldConstraints {
90    fn default() -> Self {
91        Self {
92            minimum: None,
93            maximum: None,
94            min_length: None,
95            max_length: None,
96            pattern: None,
97        }
98    }
99}
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct FieldSchema {
102    pub name: String,
103    pub field_type: FieldType,
104    #[serde(default)]
105    pub nullable: bool,
106    #[serde(default)]
107    pub required: bool,
108    #[serde(default)]
109    pub default: Option<Value>,
110    #[serde(default)]
111    pub constraints: FieldConstraints,
112    #[serde(default)]
113    pub computed: Option<String>,
114}
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116pub struct SchemaIndex {
117    pub name: String,
118    pub fields: Vec<String>,
119    #[serde(default)]
120    pub unique: bool,
121    #[serde(default)]
122    pub sparse: bool,
123    #[serde(default = "one")]
124    pub version: u32,
125}
126fn one() -> u32 {
127    1
128}
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
130pub struct CollectionSchema {
131    pub name: String,
132    #[serde(default = "one")]
133    pub version: u32,
134    #[serde(default)]
135    pub fields: Vec<FieldSchema>,
136    #[serde(default)]
137    pub indexes: Vec<SchemaIndex>,
138}
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140pub struct StateSchema {
141    pub contract_version: u32,
142    pub schema_version: u64,
143    pub application_id: String,
144    pub revision_id: String,
145    #[serde(default)]
146    pub collections: Vec<CollectionSchema>,
147}
148fn parse_manifest_type(value: &str) -> FieldType {
149    let value = value.trim();
150    if let Some(inner) = value
151        .strip_prefix("array<")
152        .and_then(|v| v.strip_suffix('>'))
153    {
154        return FieldType::Array {
155            items: Box::new(parse_manifest_type(inner)),
156        };
157    }
158    if let Some(inner) = value.strip_prefix("map<").and_then(|v| v.strip_suffix('>')) {
159        return FieldType::Map {
160            values: Box::new(parse_manifest_type(inner)),
161        };
162    }
163    if value == "vector" {
164        return FieldType::Vector { dimensions: None };
165    }
166    if let Some(inner) = value
167        .strip_prefix("vector<")
168        .and_then(|v| v.strip_suffix('>'))
169    {
170        return FieldType::Vector {
171            dimensions: inner.parse().ok(),
172        };
173    }
174    FieldType::Primitive {
175        primitive: match value {
176            "int" | "integer" => PrimitiveType::Integer,
177            "number" | "float" => PrimitiveType::Number,
178            "bool" | "boolean" => PrimitiveType::Boolean,
179            "timestamp" | "datetime" => PrimitiveType::Timestamp,
180            "date" => PrimitiveType::Date,
181            "time" => PrimitiveType::Time,
182            "json" => PrimitiveType::Json,
183            "uuid" => PrimitiveType::Uuid,
184            "decimal" => PrimitiveType::Decimal,
185            "money" => PrimitiveType::Money,
186            "bigint" => PrimitiveType::Bigint,
187            "email" => PrimitiveType::Email,
188            "url" => PrimitiveType::Url,
189            "phone" => PrimitiveType::Phone,
190            "binary" | "bytes" => PrimitiveType::Binary,
191            "file" => PrimitiveType::File,
192            "geo" | "geopoint" => PrimitiveType::GeoPoint,
193            "object" => PrimitiveType::Object,
194            _ => PrimitiveType::String,
195        },
196    }
197}
198pub fn valid_manifest_field_type(value: &str) -> bool {
199    let value = value.trim();
200    if matches!(
201        value,
202        "text"
203            | "string"
204            | "int"
205            | "integer"
206            | "number"
207            | "float"
208            | "bool"
209            | "boolean"
210            | "timestamp"
211            | "datetime"
212            | "date"
213            | "time"
214            | "json"
215            | "uuid"
216            | "decimal"
217            | "money"
218            | "bigint"
219            | "email"
220            | "url"
221            | "phone"
222            | "binary"
223            | "bytes"
224            | "file"
225            | "geo"
226            | "geopoint"
227            | "object"
228            | "enum"
229            | "reference"
230            | "vector"
231    ) {
232        return true;
233    }
234    if value
235        .strip_prefix("vector<")
236        .and_then(|v| v.strip_suffix('>'))
237        .is_some_and(|v| v.parse::<usize>().is_ok_and(|n| n > 0))
238    {
239        return true;
240    }
241    value
242        .strip_prefix("array<")
243        .and_then(|v| v.strip_suffix('>'))
244        .or_else(|| value.strip_prefix("map<").and_then(|v| v.strip_suffix('>')))
245        .is_some_and(valid_manifest_field_type)
246}
247fn manifest_field_type(field: &FieldDefinition) -> FieldType {
248    if let Some(collection) = &field.reference {
249        FieldType::Reference {
250            collection: collection.clone(),
251        }
252    } else if !field.enum_values.is_empty() {
253        FieldType::Enum {
254            values: field.enum_values.clone(),
255        }
256    } else {
257        parse_manifest_type(&field.field_type)
258    }
259}
260pub fn schema_from_manifest(
261    manifest: &ApplicationManifest,
262    revision_id: impl Into<String>,
263) -> StateSchema {
264    StateSchema {
265        contract_version: STATE_CONTRACT_VERSION,
266        schema_version: manifest.state_schema_version,
267        application_id: manifest.application_id.clone(),
268        revision_id: revision_id.into(),
269        collections: manifest
270            .collections
271            .iter()
272            .map(|collection| CollectionSchema {
273                name: collection.name.clone(),
274                version: manifest.state_schema_version as u32,
275                fields: collection
276                    .fields
277                    .iter()
278                    .map(|field| FieldSchema {
279                        name: field.name.clone(),
280                        field_type: manifest_field_type(field),
281                        nullable: field.nullable,
282                        required: field.required,
283                        default: field.default.clone(),
284                        constraints: Default::default(),
285                        computed: field.computed.clone(),
286                    })
287                    .collect(),
288                indexes: manifest
289                    .indexes
290                    .iter()
291                    .filter(|index| index.collection == collection.name)
292                    .map(|index| SchemaIndex {
293                        name: index.name.clone(),
294                        fields: index.fields.clone(),
295                        unique: index.unique,
296                        sparse: index.sparse,
297                        version: index.version,
298                    })
299                    .collect(),
300            })
301            .collect(),
302    }
303}
304pub fn schema_from_revision(revision: &ApplicationRevision) -> StateSchema {
305    schema_from_manifest(&revision.manifest, revision.revision_id.clone())
306}
307#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
308pub struct SchemaIssue {
309    pub path: String,
310    pub code: String,
311    pub message: String,
312}
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct SchemaValidation {
315    pub valid: bool,
316    pub schema_hash: Option<String>,
317    pub issues: Vec<SchemaIssue>,
318}
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
320#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
321pub enum SchemaCompatibility {
322    Safe,
323    RequiresMigration,
324    Destructive,
325    Incompatible,
326}
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct SchemaChange {
329    pub collection: String,
330    pub field: Option<String>,
331    pub classification: SchemaCompatibility,
332    pub description: String,
333}
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct SchemaCompatibilityReport {
336    pub from_version: u64,
337    pub to_version: u64,
338    pub classification: SchemaCompatibility,
339    pub changes: Vec<SchemaChange>,
340}
341
342fn sorted_schema(mut schema: StateSchema) -> StateSchema {
343    schema.collections.sort_by(|a, b| a.name.cmp(&b.name));
344    for collection in &mut schema.collections {
345        collection.fields.sort_by(|a, b| a.name.cmp(&b.name));
346        collection.indexes.sort_by(|a, b| a.name.cmp(&b.name));
347        for field in &mut collection.fields {
348            if let FieldType::Enum { values } = &mut field.field_type {
349                values.sort();
350            }
351        }
352    }
353    schema
354}
355pub fn canonical_schema_bytes(schema: &StateSchema) -> Result<Vec<u8>, String> {
356    serde_json::to_vec(&sorted_schema(schema.clone())).map_err(|e| e.to_string())
357}
358pub fn schema_hash(schema: &StateSchema) -> Result<String, String> {
359    Ok(format!(
360        "sha256:{:x}",
361        Sha256::digest(canonical_schema_bytes(schema)?)
362    ))
363}
364pub fn validate_schema(schema: &StateSchema) -> SchemaValidation {
365    let mut issues = vec![];
366    if schema.contract_version != STATE_CONTRACT_VERSION {
367        issues.push(SchemaIssue {
368            path: "contract_version".into(),
369            code: "UNSUPPORTED_CONTRACT".into(),
370            message: "unsupported state contract version".into(),
371        });
372    }
373    let mut collections = BTreeSet::new();
374    for collection in &schema.collections {
375        if collection.name.trim().is_empty() || !collections.insert(collection.name.clone()) {
376            issues.push(SchemaIssue {
377                path: format!("collections.{}", collection.name),
378                code: "INVALID_COLLECTION".into(),
379                message: "collection names must be non-empty and unique".into(),
380            });
381        }
382        let mut fields = BTreeSet::new();
383        for field in &collection.fields {
384            if field.name.trim().is_empty() || !fields.insert(field.name.clone()) {
385                issues.push(SchemaIssue {
386                    path: format!("collections.{}.fields.{}", collection.name, field.name),
387                    code: "INVALID_FIELD".into(),
388                    message: "field names must be non-empty and unique".into(),
389                });
390            }
391            if field.required && field.nullable {
392                issues.push(SchemaIssue {
393                    path: format!("collections.{}.fields.{}", collection.name, field.name),
394                    code: "INVALID_NULLABILITY".into(),
395                    message: "required fields cannot be nullable".into(),
396                });
397            }
398            if field.computed.is_some() && field.default.is_some() {
399                issues.push(SchemaIssue {
400                    path: format!("collections.{}.fields.{}", collection.name, field.name),
401                    code: "INVALID_COMPUTED_DEFAULT".into(),
402                    message: "computed fields cannot declare defaults".into(),
403                });
404            }
405            if let Some(expression) = &field.computed {
406                let sources = expression
407                    .strip_prefix("copy:")
408                    .map(|v| vec![v])
409                    .or_else(|| {
410                        expression
411                            .strip_prefix("concat:")
412                            .map(|v| v.split(',').collect())
413                    })
414                    .unwrap_or_default();
415                if sources.is_empty()
416                    || sources.iter().any(|source| {
417                        !collection
418                            .fields
419                            .iter()
420                            .any(|candidate| candidate.name == source.trim())
421                    })
422                {
423                    issues.push(SchemaIssue{path:format!("collections.{}.fields.{}.computed",collection.name,field.name),code:"INVALID_COMPUTED_FIELD".into(),message:"computed expressions must use copy:<field> or concat:<field>,<field> with known fields".into()});
424                }
425            }
426            if field
427                .default
428                .as_ref()
429                .is_some_and(|value| !valid_type(value, field))
430            {
431                issues.push(SchemaIssue {
432                    path: format!(
433                        "collections.{}.fields.{}.default",
434                        collection.name, field.name
435                    ),
436                    code: "INVALID_DEFAULT".into(),
437                    message: "default does not satisfy the field type".into(),
438                });
439            }
440            if let FieldType::Enum { values } = &field.field_type {
441                let unique = values.iter().collect::<BTreeSet<_>>();
442                if values.is_empty() || unique.len() != values.len() {
443                    issues.push(SchemaIssue {
444                        path: format!("collections.{}.fields.{}", collection.name, field.name),
445                        code: "INVALID_ENUM".into(),
446                        message: "enum values must be non-empty and unique".into(),
447                    });
448                }
449            }
450        }
451        for index in &collection.indexes {
452            if index.fields.is_empty() || index.fields.iter().any(|f| !fields.contains(f)) {
453                issues.push(SchemaIssue {
454                    path: format!("collections.{}.indexes.{}", collection.name, index.name),
455                    code: "INVALID_INDEX".into(),
456                    message: "indexes must reference existing fields".into(),
457                });
458            }
459        }
460    }
461    for collection in &schema.collections {
462        for field in &collection.fields {
463            if let FieldType::Reference { collection: target } = &field.field_type {
464                if !collections.contains(target) {
465                    issues.push(SchemaIssue {
466                        path: format!("collections.{}.fields.{}", collection.name, field.name),
467                        code: "INVALID_REFERENCE".into(),
468                        message: format!("unknown reference target {target}"),
469                    });
470                }
471            }
472        }
473    }
474    let valid = issues.is_empty();
475    SchemaValidation {
476        valid,
477        schema_hash: valid.then(|| schema_hash(schema).ok()).flatten(),
478        issues,
479    }
480}
481pub fn compare_schemas(before: &StateSchema, after: &StateSchema) -> SchemaCompatibilityReport {
482    let mut changes = vec![];
483    let old: BTreeMap<_, _> = before.collections.iter().map(|v| (&v.name, v)).collect();
484    let new: BTreeMap<_, _> = after.collections.iter().map(|v| (&v.name, v)).collect();
485    for (name, collection) in &new {
486        if !old.contains_key(name) {
487            changes.push(SchemaChange {
488                collection: (*name).clone(),
489                field: None,
490                classification: SchemaCompatibility::Safe,
491                description: "collection added".into(),
492            });
493            continue;
494        }
495        let previous = old[name];
496        let old_fields: BTreeMap<_, _> = previous.fields.iter().map(|v| (&v.name, v)).collect();
497        let new_fields: BTreeMap<_, _> = collection.fields.iter().map(|v| (&v.name, v)).collect();
498        for (field, current) in &new_fields {
499            match old_fields.get(field) {
500                None => changes.push(SchemaChange {
501                    collection: (*name).clone(),
502                    field: Some((*field).clone()),
503                    classification: if current.required && current.default.is_none() {
504                        SchemaCompatibility::Destructive
505                    } else {
506                        SchemaCompatibility::Safe
507                    },
508                    description: "field added".into(),
509                }),
510                Some(prior) if prior.field_type != current.field_type => {
511                    let classification = match (&prior.field_type, &current.field_type) {
512                        (FieldType::Enum { values: a }, FieldType::Enum { values: b })
513                            if a.iter().all(|v| b.contains(v)) =>
514                        {
515                            SchemaCompatibility::Safe
516                        }
517                        (FieldType::Enum { .. }, FieldType::Enum { .. }) => {
518                            SchemaCompatibility::Destructive
519                        }
520                        _ => SchemaCompatibility::Incompatible,
521                    };
522                    changes.push(SchemaChange {
523                        collection: (*name).clone(),
524                        field: Some((*field).clone()),
525                        classification,
526                        description: "field type changed".into(),
527                    });
528                }
529                Some(prior)
530                    if prior.required != current.required || prior.nullable != current.nullable =>
531                {
532                    changes.push(SchemaChange {
533                        collection: (*name).clone(),
534                        field: Some((*field).clone()),
535                        classification: if current.required {
536                            SchemaCompatibility::Destructive
537                        } else {
538                            SchemaCompatibility::Safe
539                        },
540                        description: "field requirement changed".into(),
541                    })
542                }
543                _ => {}
544            }
545        }
546        for field in old_fields.keys().filter(|f| !new_fields.contains_key(*f)) {
547            changes.push(SchemaChange {
548                collection: (*name).clone(),
549                field: Some((*field).clone()),
550                classification: SchemaCompatibility::Destructive,
551                description: "field removed".into(),
552            });
553        }
554    }
555    for name in old.keys().filter(|n| !new.contains_key(*n)) {
556        changes.push(SchemaChange {
557            collection: (*name).clone(),
558            field: None,
559            classification: SchemaCompatibility::Destructive,
560            description: "collection removed".into(),
561        });
562    }
563    let classification = changes
564        .iter()
565        .map(|v| v.classification.clone())
566        .max()
567        .unwrap_or(SchemaCompatibility::Safe);
568    SchemaCompatibilityReport {
569        from_version: before.schema_version,
570        to_version: after.schema_version,
571        classification,
572        changes,
573    }
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct AuthorizationContext {
578    pub subject: String,
579    pub tenant_id: String,
580    pub application_id: String,
581    pub revision_id: String,
582    #[serde(default)]
583    pub capabilities: BTreeSet<String>,
584    #[serde(default)]
585    pub readable_collections: BTreeSet<String>,
586    #[serde(default)]
587    pub writable_collections: BTreeSet<String>,
588    #[serde(default)]
589    pub field_projections: BTreeMap<String, BTreeSet<String>>,
590}
591impl AuthorizationContext {
592    fn can(&self, cap: &str) -> bool {
593        self.capabilities.contains("*") || self.capabilities.contains(cap)
594    }
595}
596#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct ReadContext {
598    pub application_id: String,
599    pub revision_id: String,
600    pub schema_version: u64,
601    pub state_version: u64,
602    pub causal_cursor: BTreeMap<String, u64>,
603    pub state_namespace: String,
604    pub authorization: AuthorizationContext,
605    #[serde(skip, default)]
606    snapshot_rows: Vec<StoredRow>,
607    #[serde(skip, default)]
608    live_query: bool,
609}
610pub fn begin_read(
611    db: &FeltDb,
612    schema: &StateSchema,
613    state_namespace: &str,
614    authorization: AuthorizationContext,
615) -> Result<ReadContext, StateFailure> {
616    begin_read_internal(db, schema, state_namespace, authorization, true)
617}
618
619/// Begin a query read without cloning the namespace up front. Query execution
620/// uses authoritative collection/index access and loads a namespace snapshot
621/// only when policy evaluation or reference expansion requires it.
622pub fn begin_query_read(
623    db: &FeltDb,
624    schema: &StateSchema,
625    state_namespace: &str,
626    authorization: AuthorizationContext,
627) -> Result<ReadContext, StateFailure> {
628    build_read_context(db, schema, state_namespace, authorization, Vec::new(), true)
629}
630
631fn begin_read_internal(
632    db: &FeltDb,
633    schema: &StateSchema,
634    state_namespace: &str,
635    authorization: AuthorizationContext,
636    capture_snapshot: bool,
637) -> Result<ReadContext, StateFailure> {
638    if authorization.application_id != schema.application_id
639        || authorization.revision_id != schema.revision_id
640    {
641        return Err(StateFailure::new(
642            "SCHEMA_MISMATCH",
643            "authorization context does not match schema",
644        ));
645    }
646    let snapshot_rows = if capture_snapshot {
647        db.state_rows_for_namespace(state_namespace)
648            .map_err(StateFailure::storage)?
649    } else {
650        Vec::new()
651    };
652    build_read_context(
653        db,
654        schema,
655        state_namespace,
656        authorization,
657        snapshot_rows,
658        !capture_snapshot,
659    )
660}
661
662fn build_read_context(
663    db: &FeltDb,
664    schema: &StateSchema,
665    state_namespace: &str,
666    authorization: AuthorizationContext,
667    snapshot_rows: Vec<StoredRow>,
668    live_query: bool,
669) -> Result<ReadContext, StateFailure> {
670    Ok(ReadContext {
671        application_id: schema.application_id.clone(),
672        revision_id: schema.revision_id.clone(),
673        schema_version: schema.schema_version,
674        state_version: db.sequence().map_err(StateFailure::storage)?,
675        causal_cursor: db
676            .operation_versions()
677            .map_err(StateFailure::storage)?
678            .into_iter()
679            .collect(),
680        state_namespace: state_namespace.into(),
681        authorization,
682        snapshot_rows,
683        live_query,
684    })
685}
686
687fn namespace_capability(state_namespace: &str, collection: &str) -> String {
688    format!("{state_namespace}:{collection}")
689}
690
691fn membership_collection_name(schema: &StateSchema) -> Option<String> {
692    AuthorizationState::new(Arc::new(schema.clone()), "", Arc::new(Vec::new()))
693        .find_membership_collection()
694}
695
696fn member_policy_snapshot_rows(
697    db: &FeltDb,
698    schema: &StateSchema,
699    state_namespace: &str,
700) -> Result<Vec<StoredRow>, StateFailure> {
701    let Some(collection) = membership_collection_name(schema) else {
702        return Ok(Vec::new());
703    };
704    db.list_collection(&namespace_capability(state_namespace, &collection))
705        .map_err(StateFailure::storage)
706}
707
708fn load_reference_rows(
709    db: &FeltDb,
710    state_namespace: &str,
711    values: &[Value],
712    references: &[QueryReference],
713) -> Result<BTreeMap<(String, String), Value>, StateFailure> {
714    let mut rows = BTreeMap::new();
715    let mut requested = BTreeMap::<String, BTreeSet<String>>::new();
716    for reference in references {
717        for id in values
718            .iter()
719            .filter_map(|value| value.get(&reference.field).and_then(Value::as_str))
720        {
721            requested
722                .entry(reference.collection.clone())
723                .or_default()
724                .insert(id.to_string());
725        }
726    }
727    for (collection, ids) in requested {
728        let capability = namespace_capability(state_namespace, &collection);
729        let loaded = db
730            .get_collection_records(&capability, ids.iter().map(String::as_str))
731            .map_err(StateFailure::storage)?;
732        let present = loaded
733            .into_iter()
734            .map(|row| (row.key, row.value))
735            .collect::<BTreeMap<_, _>>();
736        for id in ids {
737            rows.insert(
738                (collection.clone(), id.clone()),
739                present.get(&id).cloned().unwrap_or(Value::Null),
740            );
741        }
742    }
743    Ok(rows)
744}
745
746fn transaction_snapshot_rows(
747    db: &FeltDb,
748    schema: &StateSchema,
749    request: &TransactionRequest,
750    uniform_write_policy: Option<&PolicySubject>,
751    collection_write_policies: Option<&BTreeMap<String, PolicySubject>>,
752) -> Result<Vec<StoredRow>, StateFailure> {
753    let namespace = request
754        .state_namespace
755        .as_deref()
756        .unwrap_or(&request.application_id);
757    let mut full_collections = BTreeSet::new();
758    let mut records = BTreeSet::new();
759    for condition in &request.preconditions {
760        if let TransactionPrecondition::CollectionVersion { collection, .. } = condition {
761            full_collections.insert(collection.clone());
762        }
763    }
764    let membership_collection = membership_collection_name(schema);
765    for operation in &request.operations {
766        let substrate = is_substrate_collection(&operation.collection);
767        records.insert((operation.collection.clone(), operation.id.clone()));
768        let write_policy = collection_write_policies
769            .and_then(|policies| policies.get(&operation.collection))
770            .or(uniform_write_policy);
771        if matches!(write_policy, Some(&PolicySubject::Member)) {
772            if let Some(collection) = &membership_collection {
773                full_collections.insert(collection.clone());
774            }
775        }
776        if substrate || matches!(operation.kind, TransactionOperationKind::Delete) {
777            continue;
778        }
779        let definition = collection(schema, &operation.collection)?;
780        if definition.indexes.iter().any(|index| index.unique) {
781            full_collections.insert(operation.collection.clone());
782        }
783    }
784
785    let mut snapshot = Vec::new();
786    let mut materialized = BTreeSet::new();
787    for collection in &full_collections {
788        snapshot.extend(
789            db.list_collection(&namespace_capability(namespace, collection))
790                .map_err(StateFailure::storage)?,
791        );
792        materialized.insert(collection.clone());
793    }
794    for (collection, key) in records {
795        if materialized.contains(&collection) {
796            continue;
797        }
798        if let Some(row) = db
799            .get_collection_record(&namespace_capability(namespace, &collection), &key)
800            .map_err(StateFailure::storage)?
801        {
802            snapshot.push(row);
803        }
804    }
805    Ok(snapshot)
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize)]
809#[serde(tag = "operator", rename_all = "camelCase")]
810pub enum QueryFilter {
811    Eq { field: String, value: Value },
812    Neq { field: String, value: Value },
813    Lt { field: String, value: Value },
814    Lte { field: String, value: Value },
815    Gt { field: String, value: Value },
816    Gte { field: String, value: Value },
817    In { field: String, values: Vec<Value> },
818    Contains { field: String, value: Value },
819    StartsWith { field: String, value: String },
820    EndsWith { field: String, value: String },
821    Exists { field: String, exists: bool },
822    And { filters: Vec<QueryFilter> },
823    Or { filters: Vec<QueryFilter> },
824    Not { filter: Box<QueryFilter> },
825}
826#[derive(Debug, Clone, Serialize, Deserialize)]
827#[serde(rename_all = "snake_case")]
828pub enum SortDirection {
829    Asc,
830    Desc,
831}
832#[derive(Debug, Clone, Serialize, Deserialize)]
833pub struct QueryOrder {
834    pub field: String,
835    pub direction: SortDirection,
836}
837#[derive(Debug, Clone, Serialize, Deserialize)]
838#[serde(rename_all = "snake_case")]
839pub enum AggregateFunction {
840    Count,
841    Sum,
842    Avg,
843    Min,
844    Max,
845}
846#[derive(Debug, Clone, Serialize, Deserialize)]
847pub struct AggregateDefinition {
848    pub name: String,
849    pub function: AggregateFunction,
850    #[serde(default)]
851    pub field: Option<String>,
852}
853#[derive(Debug, Clone, Serialize, Deserialize)]
854pub struct QueryReference {
855    pub field: String,
856    pub collection: String,
857    #[serde(default)]
858    pub projection: Vec<String>,
859    pub as_name: String,
860}
861#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct CanonicalQuery {
863    pub collection: String,
864    #[serde(default)]
865    pub filter: Option<QueryFilter>,
866    #[serde(default)]
867    pub order_by: Vec<QueryOrder>,
868    #[serde(default)]
869    pub limit: Option<usize>,
870    #[serde(default)]
871    pub offset: usize,
872    #[serde(default)]
873    pub cursor: Option<String>,
874    #[serde(default)]
875    pub projection: Vec<String>,
876    #[serde(default)]
877    pub group_by: Vec<String>,
878    #[serde(default)]
879    pub aggregates: Vec<AggregateDefinition>,
880    #[serde(default)]
881    pub references: Vec<QueryReference>,
882    /// A semantic clause is a recognized query concept that this surface does
883    /// not compose. It is carried through deserialization so `execute_query`
884    /// can refuse it with a structured failure instead of an unknown key
885    /// silently disappearing; see `docs/reference/semantic-query.md`.
886    #[serde(default, skip_serializing_if = "Option::is_none")]
887    pub semantic: Option<Value>,
888}
889/// Actual execution timing measurements for a query.
890/// All fields are MEASURED using Instant::now(), not estimated or derived.
891/// Each field captures time for its phase: planning, index lookup, filtering, serialization.
892#[derive(Debug, Clone, Serialize, Deserialize)]
893pub struct ExecutionTiming {
894    /// Time spent in query planning (determining access method)
895    #[serde(default)]
896    pub planning_ms: Option<f64>,
897    /// Time spent in index lookup phase (pre-filtering via indexed field)
898    #[serde(default)]
899    pub index_lookup_ms: Option<f64>,
900    /// Time spent evaluating full query filter predicates
901    #[serde(default)]
902    pub filtering_ms: Option<f64>,
903    /// Time spent serializing results to JSON
904    #[serde(default)]
905    pub serialization_ms: Option<f64>,
906    /// Total end-to-end execution time
907    #[serde(default)]
908    pub total_ms: Option<f64>,
909}
910
911/// Actual index usage metrics from query execution.
912/// These fields MUST reflect what the executor actually did, not what was planned.
913/// If the planner selected an index but executor fell back to scan, these metrics MUST show
914/// fallback_used=true and fallback_reason must explain why, else the contract is violated.
915#[derive(Debug, Clone, Serialize, Deserialize)]
916pub struct IndexMetrics {
917    /// Which index was used, if any (e.g., "status_idx"). None if no index used.
918    #[serde(default)]
919    pub index_used: Option<String>,
920    /// Number of rows returned by index pre-filter phase (actual count, not estimate)
921    #[serde(default)]
922    pub index_hits: usize,
923    /// Number of rows filtered out by index pre-filter (actual count, not estimate)
924    #[serde(default)]
925    pub index_misses: usize,
926    /// True if executor fell back to full scan (violates contract if planner said "index_lookup")
927    #[serde(default)]
928    pub fallback_used: bool,
929    /// Reason for fallback (e.g., "index_not_found", "unsupported_expression")
930    #[serde(default)]
931    pub fallback_reason: Option<String>,
932}
933
934impl ExecutionTiming {
935    pub fn new() -> Self {
936        ExecutionTiming {
937            planning_ms: None,
938            index_lookup_ms: None,
939            filtering_ms: None,
940            serialization_ms: None,
941            total_ms: None,
942        }
943    }
944}
945
946impl Default for ExecutionTiming {
947    fn default() -> Self {
948        Self::new()
949    }
950}
951
952/// Query execution plan with CONTRACT guarantees.
953/// The planner makes a decision (access_method: "index_lookup" or "full_table_scan").
954/// The executor MUST honor this decision and update index_metrics and timing to prove it did.
955/// Violation: planner says "index_lookup" but executor provides metrics showing full_table_scan.
956#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct QueryPlan {
958    pub implementation: String,
959    pub collection: String,
960    pub index: Option<String>,
961    pub scan_fallback: bool,
962    /// ACCESS METHOD DECISION: "index_lookup" or "full_table_scan" (planner's choice)
963    /// Executor MUST follow this decision and update index_metrics to prove it did.
964    #[serde(default)]
965    pub access_method: Option<String>,
966    #[serde(default)]
967    pub estimated_rows: Option<usize>,
968    /// Reason for fallback to scan (only set if access_method="full_table_scan")
969    #[serde(default)]
970    pub fallback_reason: Option<String>,
971    /// Number of rows examined by executor (actual, not estimate)
972    #[serde(default)]
973    pub actual_rows_scanned: Option<usize>,
974    /// Number of rows returned after all filtering (actual, not estimate)
975    #[serde(default)]
976    pub actual_rows_returned: Option<usize>,
977    /// Records that matched the complete scoped predicate before pagination.
978    #[serde(default)]
979    pub matching_records: Option<usize>,
980    /// Candidates outside the requested equality scope that were examined.
981    #[serde(default)]
982    pub unrelated_records_examined: Option<usize>,
983    /// Zero-based position within the scoped, deterministically ordered set.
984    #[serde(default)]
985    pub pagination_position: Option<usize>,
986    /// Total execution time in milliseconds (actual measurement)
987    #[serde(default)]
988    pub execution_ms: Option<f64>,
989    /// Detailed timing breakdown (planning, index_lookup, filtering, serialization)
990    #[serde(default)]
991    pub timing: Option<ExecutionTiming>,
992    /// Proof that executor followed planner's access_method decision
993    #[serde(default)]
994    pub index_metrics: Option<IndexMetrics>,
995}
996#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
997#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
998pub enum OperationType {
999    Read,
1000    Query,
1001    Insert,
1002    Update,
1003    Delete,
1004    Transaction,
1005    Sync,
1006    Replication,
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1010pub struct QueryResult {
1011    pub query_id: String,
1012    pub query_hash: String,
1013    pub application_revision: String,
1014    pub schema_version: u64,
1015    pub state_version: u64,
1016    pub causal_cursor: BTreeMap<String, u64>,
1017    pub records: Vec<Value>,
1018    pub aggregates: Vec<Value>,
1019    pub next_cursor: Option<String>,
1020    pub plan: QueryPlan,
1021    #[serde(default)]
1022    pub operation_type: Option<OperationType>,
1023}
1024
1025#[derive(Debug, Clone, Serialize, Deserialize)]
1026pub struct StateFailure {
1027    pub code: String,
1028    pub message: String,
1029    #[serde(default)]
1030    pub expected: Option<Value>,
1031    #[serde(default)]
1032    pub actual: Option<Value>,
1033    #[serde(default)]
1034    pub resource: Option<String>,
1035    #[serde(default)]
1036    pub transaction: Option<String>,
1037    #[serde(default)]
1038    pub causal_position: Option<u64>,
1039    /// The structured predicate failure, when the refusal was a record
1040    /// precondition. Named `failure` because that is what the direct
1041    /// authority's refused transaction already calls it: one refusal, one
1042    /// shape, whichever endpoint answered.
1043    #[serde(default, skip_serializing_if = "Option::is_none")]
1044    pub failure: Option<Value>,
1045}
1046impl StateFailure {
1047    fn new(code: &str, message: &str) -> Self {
1048        Self {
1049            code: code.into(),
1050            message: message.into(),
1051            expected: None,
1052            actual: None,
1053            resource: None,
1054            transaction: None,
1055            causal_position: None,
1056            failure: None,
1057        }
1058    }
1059    fn storage(error: impl std::fmt::Display) -> Self {
1060        Self::new("STORAGE_FAILURE", &error.to_string())
1061    }
1062}
1063
1064fn idempotency_conflict() -> StateFailure {
1065    StateFailure::new(
1066        "IDEMPOTENCY_CONFLICT",
1067        "transaction ID was already committed with a different canonical payload",
1068    )
1069}
1070
1071fn collection<'a>(
1072    schema: &'a StateSchema,
1073    name: &str,
1074) -> Result<&'a CollectionSchema, StateFailure> {
1075    schema
1076        .collections
1077        .iter()
1078        .find(|v| v.name == name)
1079        .ok_or_else(|| StateFailure::new("UNKNOWN_COLLECTION", name))
1080}
1081fn filter_fields(filter: &QueryFilter, out: &mut BTreeSet<String>) {
1082    match filter {
1083        QueryFilter::Eq { field, .. }
1084        | QueryFilter::Neq { field, .. }
1085        | QueryFilter::Lt { field, .. }
1086        | QueryFilter::Lte { field, .. }
1087        | QueryFilter::Gt { field, .. }
1088        | QueryFilter::Gte { field, .. }
1089        | QueryFilter::In { field, .. }
1090        | QueryFilter::Contains { field, .. }
1091        | QueryFilter::StartsWith { field, .. }
1092        | QueryFilter::EndsWith { field, .. }
1093        | QueryFilter::Exists { field, .. } => {
1094            out.insert(field.clone());
1095        }
1096        QueryFilter::And { filters } | QueryFilter::Or { filters } => {
1097            for f in filters {
1098                filter_fields(f, out)
1099            }
1100        }
1101        QueryFilter::Not { filter } => filter_fields(filter, out),
1102    }
1103}
1104fn compare_value(left: &Value, right: &Value) -> Option<Ordering> {
1105    match (left, right) {
1106        (Value::Number(a), Value::Number(b)) => a.as_f64()?.partial_cmp(&b.as_f64()?),
1107        (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
1108        (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
1109        _ => None,
1110    }
1111}
1112fn matches(value: &Value, filter: &QueryFilter) -> bool {
1113    let get = |field: &str| value.get(field);
1114    match filter {
1115        QueryFilter::Eq { field, value: v } => get(field) == Some(v),
1116        QueryFilter::Neq { field, value: v } => get(field) != Some(v),
1117        QueryFilter::Lt { field, value: v } => {
1118            get(field).and_then(|x| compare_value(x, v)) == Some(Ordering::Less)
1119        }
1120        QueryFilter::Lte { field, value: v } => matches!(
1121            get(field).and_then(|x| compare_value(x, v)),
1122            Some(Ordering::Less | Ordering::Equal)
1123        ),
1124        QueryFilter::Gt { field, value: v } => {
1125            get(field).and_then(|x| compare_value(x, v)) == Some(Ordering::Greater)
1126        }
1127        QueryFilter::Gte { field, value: v } => matches!(
1128            get(field).and_then(|x| compare_value(x, v)),
1129            Some(Ordering::Greater | Ordering::Equal)
1130        ),
1131        QueryFilter::In { field, values } => get(field).is_some_and(|x| values.contains(x)),
1132        QueryFilter::Contains { field, value: v } => get(field).is_some_and(|x| {
1133            x.as_str()
1134                .zip(v.as_str())
1135                .is_some_and(|(a, b)| a.contains(b))
1136                || x.as_array().is_some_and(|a| a.contains(v))
1137        }),
1138        QueryFilter::StartsWith { field, value: v } => get(field)
1139            .and_then(Value::as_str)
1140            .is_some_and(|x| x.starts_with(v)),
1141        QueryFilter::EndsWith { field, value: v } => get(field)
1142            .and_then(Value::as_str)
1143            .is_some_and(|x| x.ends_with(v)),
1144        QueryFilter::Exists { field, exists } => get(field).is_some() == *exists,
1145        QueryFilter::And { filters } => filters.iter().all(|f| matches(value, f)),
1146        QueryFilter::Or { filters } => filters.iter().any(|f| matches(value, f)),
1147        QueryFilter::Not { filter } => !matches(value, filter),
1148    }
1149}
1150fn projected(value: &Value, fields: &[String]) -> Value {
1151    if fields.is_empty() {
1152        return value.clone();
1153    }
1154    let mut out = Map::new();
1155    for field in fields {
1156        if let Some(v) = value.get(field) {
1157            out.insert(field.clone(), v.clone());
1158        }
1159    }
1160    Value::Object(out)
1161}
1162pub fn query_hash(query: &CanonicalQuery) -> Result<String, StateFailure> {
1163    Ok(format!(
1164        "sha256:{:x}",
1165        Sha256::digest(serde_json::to_vec(query).map_err(StateFailure::storage)?)
1166    ))
1167}
1168
1169/// Hash only the immutable query shape. Pagination state is deliberately
1170/// excluded so every page remains bound to the same collection, scope,
1171/// ordering, projection, and limit.
1172fn pagination_query_hash(query: &CanonicalQuery) -> Result<String, StateFailure> {
1173    let mut shape = query.clone();
1174    shape.cursor = None;
1175    shape.offset = 0;
1176    query_hash(&shape)
1177}
1178
1179fn encode_query_cursor(query_hash: &str, position: usize) -> String {
1180    format!("feltdb-query-v1.{position}.{}", &query_hash[7..])
1181}
1182
1183fn decode_query_cursor(cursor: &str, expected_hash: &str) -> Result<usize, StateFailure> {
1184    let mut parts = cursor.split('.');
1185    let version = parts.next();
1186    let position = parts.next();
1187    let hash = parts.next();
1188    if version != Some("feltdb-query-v1")
1189        || parts.next().is_some()
1190        || hash != Some(&expected_hash[7..])
1191    {
1192        return Err(StateFailure::new(
1193            "INVALID_CURSOR",
1194            "cursor does not match the query scope, collection, ordering, or limit",
1195        ));
1196    }
1197    position
1198        .and_then(|value| value.parse::<usize>().ok())
1199        .ok_or_else(|| StateFailure::new("INVALID_CURSOR", "cursor is malformed"))
1200}
1201
1202fn query_value(row: &crate::StoredRow) -> Value {
1203    let mut value = row.value.clone();
1204    if let Some(object) = value.as_object_mut() {
1205        object.insert("_id".into(), Value::String(row.key.clone()));
1206        object.insert(
1207            "_version".into(),
1208            Value::from(
1209                row.operation
1210                    .as_ref()
1211                    .map(|value| value.sequence)
1212                    .unwrap_or(0),
1213            ),
1214        );
1215    }
1216    value
1217}
1218fn extract_equality_predicate(filter: &QueryFilter) -> Option<(String, Value)> {
1219    match filter {
1220        QueryFilter::Eq { field, value } => Some((field.clone(), value.clone())),
1221        QueryFilter::And { filters } => filters.iter().find_map(extract_equality_predicate),
1222        _ => None,
1223    }
1224}
1225fn extract_indexed_equality(
1226    filter: &QueryFilter,
1227    definition: &CollectionSchema,
1228) -> Option<(String, Value)> {
1229    match filter {
1230        QueryFilter::Eq { field, value } if can_use_index(definition, field) => {
1231            Some((field.clone(), value.clone()))
1232        }
1233        QueryFilter::And { filters } => filters
1234            .iter()
1235            .find_map(|filter| extract_indexed_equality(filter, definition)),
1236        _ => None,
1237    }
1238}
1239
1240fn contains_managed_scope(filter: &QueryFilter) -> bool {
1241    match filter {
1242        QueryFilter::Eq { field, .. } => matches!(
1243            field.as_str(),
1244            "tenant_id" | "tenantId" | "organization_id" | "organizationId" | "scope_prefix"
1245        ),
1246        QueryFilter::And { filters } => filters.iter().any(contains_managed_scope),
1247        _ => false,
1248    }
1249}
1250
1251fn extract_range_predicate(filter: &QueryFilter) -> Option<(String, Option<Value>, Option<Value>)> {
1252    match filter {
1253        QueryFilter::Gte { field, value } => Some((field.clone(), Some(value.clone()), None)),
1254        QueryFilter::Lte { field, value } => Some((field.clone(), None, Some(value.clone()))),
1255        QueryFilter::Gt { field, value } => Some((field.clone(), Some(value.clone()), None)),
1256        QueryFilter::Lt { field, value } => Some((field.clone(), None, Some(value.clone()))),
1257        _ => None,
1258    }
1259}
1260
1261fn can_use_index(definition: &CollectionSchema, field: &str) -> bool {
1262    definition
1263        .indexes
1264        .iter()
1265        .any(|idx| idx.fields.contains(&field.to_string()))
1266}
1267
1268fn can_use_index_for_range(definition: &CollectionSchema, field: &str) -> bool {
1269    definition
1270        .indexes
1271        .iter()
1272        .any(|idx| idx.fields.contains(&field.to_string()))
1273}
1274
1275pub fn explain_query(
1276    schema: &StateSchema,
1277    query: &CanonicalQuery,
1278) -> Result<QueryPlan, StateFailure> {
1279    let definition = collection(schema, &query.collection)?;
1280
1281    let mut predicate_fields = BTreeSet::new();
1282    if let Some(filter) = &query.filter {
1283        filter_fields(filter, &mut predicate_fields)
1284    }
1285
1286    let (used_index, index_name, _index_predicate_field_and_value) =
1287        if let Some(filter) = &query.filter {
1288            if let Some((field, _value)) = extract_equality_predicate(filter) {
1289                if can_use_index(definition, &field) {
1290                    let index_name = definition
1291                        .indexes
1292                        .iter()
1293                        .find(|idx| idx.fields.contains(&field))
1294                        .map(|idx| idx.name.clone());
1295
1296                    if index_name.is_some() {
1297                        (true, index_name, Some((field, Value::Null)))
1298                    } else {
1299                        (false, None, None)
1300                    }
1301                } else {
1302                    (false, None, None)
1303                }
1304            } else {
1305                (false, None, None)
1306            }
1307        } else {
1308            (false, None, None)
1309        };
1310
1311    let (execution_strategy, scan_fallback_flag, access_method) = if used_index {
1312        ("index_lookup".into(), false, Some("index".to_string()))
1313    } else {
1314        (
1315            "full_table_scan".into(),
1316            true,
1317            Some("sequential_scan".to_string()),
1318        )
1319    };
1320
1321    Ok(QueryPlan {
1322        implementation: execution_strategy,
1323        collection: query.collection.clone(),
1324        index: index_name.clone(),
1325        scan_fallback: scan_fallback_flag,
1326        access_method,
1327        estimated_rows: if used_index { Some(1) } else { None },
1328        fallback_reason: if scan_fallback_flag {
1329            Some("no_applicable_index".to_string())
1330        } else {
1331            None
1332        },
1333        actual_rows_scanned: None,
1334        actual_rows_returned: None,
1335        matching_records: None,
1336        unrelated_records_examined: None,
1337        pagination_position: None,
1338        execution_ms: None,
1339        timing: None,
1340        index_metrics: None,
1341    })
1342}
1343
1344pub fn execute_query(
1345    _db: &FeltDb,
1346    schema: &StateSchema,
1347    context: &ReadContext,
1348    query: &CanonicalQuery,
1349    read_policy: Option<PolicySubject>,
1350) -> Result<QueryResult, StateFailure> {
1351    let query_start = Instant::now();
1352
1353    // Fail closed on semantic intent before any deterministic work. Every query
1354    // surface must either implement a recognized semantic clause per the
1355    // semantic-query contract or refuse it; this surface refuses, and it never
1356    // answers the deterministic part as though the clause had been absent.
1357    if let Some(clause) = &query.semantic {
1358        return Err(if clause.is_object() {
1359            StateFailure::new(
1360                "SEMANTIC_UNSUPPORTED",
1361                "canonical queries recognize the semantic clause but do not compose semantic decisions; use the bounded authority query",
1362            )
1363        } else {
1364            StateFailure::new("INVALID_QUERY", "semantic clause must be an object")
1365        });
1366    }
1367    if query.limit == Some(0) || query.limit.is_some_and(|limit| limit > 1000) {
1368        return Err(StateFailure::new(
1369            "INVALID_QUERY",
1370            "limit must be between 1 and 1000",
1371        ));
1372    }
1373    if context.schema_version != schema.schema_version || context.revision_id != schema.revision_id
1374    {
1375        return Err(StateFailure::new(
1376            "SCHEMA_MISMATCH",
1377            "read context does not match schema",
1378        ));
1379    }
1380    // Substrate collections carry no application schema: their records are
1381    // validated by the substrate that writes them, and they are read only
1382    // under `composition:read`.
1383    let substrate = is_substrate_collection(&query.collection);
1384    let substrate_definition;
1385    let definition = if substrate {
1386        if !query.references.is_empty() {
1387            return Err(StateFailure::new(
1388                "INVALID_QUERY",
1389                "substrate collections do not expand references",
1390            ));
1391        }
1392        substrate_definition = CollectionSchema {
1393            name: query.collection.clone(),
1394            version: 1,
1395            fields: vec![],
1396            indexes: vec![],
1397        };
1398        &substrate_definition
1399    } else {
1400        collection(schema, &query.collection)?
1401    };
1402    let readable = if substrate {
1403        context.authorization.can("composition:read")
1404    } else {
1405        context.authorization.can("state:read")
1406            && (context.authorization.readable_collections.is_empty()
1407                || context
1408                    .authorization
1409                    .readable_collections
1410                    .contains(&query.collection))
1411    };
1412    if !readable {
1413        return Err(StateFailure::new(
1414            "AUTHORIZATION_DENIED",
1415            "collection is not readable",
1416        ));
1417    }
1418    let available: BTreeSet<_> = definition
1419        .fields
1420        .iter()
1421        .map(|v| v.name.clone())
1422        .chain(query.references.iter().map(|v| v.as_name.clone()))
1423        .chain(["_id".into(), "_version".into()])
1424        .collect();
1425    let mut requested = query.projection.iter().cloned().collect::<BTreeSet<_>>();
1426    let mut predicate_fields = BTreeSet::new();
1427    if let Some(filter) = &query.filter {
1428        filter_fields(filter, &mut requested);
1429        filter_fields(filter, &mut predicate_fields)
1430    }
1431    for order in &query.order_by {
1432        requested.insert(order.field.clone());
1433    }
1434    for group in &query.group_by {
1435        requested.insert(group.clone());
1436    }
1437    for agg in &query.aggregates {
1438        if let Some(field) = &agg.field {
1439            requested.insert(field.clone());
1440        }
1441    }
1442    for reference in &query.references {
1443        requested.insert(reference.field.clone());
1444    }
1445    if !substrate && requested.iter().any(|f| !available.contains(f)) {
1446        return Err(StateFailure::new(
1447            "UNKNOWN_FIELD",
1448            "query references an unknown field",
1449        ));
1450    }
1451    if let Some(allowed) = context
1452        .authorization
1453        .field_projections
1454        .get(&query.collection)
1455    {
1456        if requested
1457            .iter()
1458            .any(|f| !allowed.contains(f) && f != "_id" && f != "_version")
1459        {
1460            return Err(StateFailure::new(
1461                "UNAUTHORIZED_PROJECTION",
1462                "query references a protected field",
1463            ));
1464        }
1465    }
1466    let effective_projection = context
1467        .authorization
1468        .field_projections
1469        .get(&query.collection)
1470        .filter(|_| query.projection.is_empty())
1471        .map(|allowed| allowed.iter().cloned().collect::<Vec<_>>())
1472        .unwrap_or_else(|| query.projection.clone());
1473    let capability = format!("{}:{}", context.state_namespace, query.collection);
1474    for reference in &query.references {
1475        let target = collection(schema, &reference.collection)?;
1476        if !context.authorization.readable_collections.is_empty()
1477            && !context
1478                .authorization
1479                .readable_collections
1480                .contains(&reference.collection)
1481        {
1482            return Err(StateFailure::new(
1483                "AUTHORIZATION_DENIED",
1484                "referenced collection is not readable",
1485            ));
1486        }
1487        if reference
1488            .projection
1489            .iter()
1490            .any(|field| !target.fields.iter().any(|v| &v.name == field))
1491        {
1492            return Err(StateFailure::new(
1493                "UNKNOWN_FIELD",
1494                "reference projection contains an unknown field",
1495            ));
1496        }
1497        if let Some(allowed) = context
1498            .authorization
1499            .field_projections
1500            .get(&reference.collection)
1501        {
1502            if reference
1503                .projection
1504                .iter()
1505                .any(|field| !allowed.contains(field))
1506            {
1507                return Err(StateFailure::new(
1508                    "UNAUTHORIZED_PROJECTION",
1509                    "reference projection contains a protected field",
1510                ));
1511            }
1512        }
1513    }
1514
1515    let policy_snapshot = if matches!(read_policy, Some(PolicySubject::Member)) {
1516        member_policy_snapshot_rows(_db, schema, &context.state_namespace)?
1517    } else {
1518        Vec::new()
1519    };
1520
1521    // Detect unfiltered COUNT(*) for maintained cardinality optimization
1522    let is_unfiltered_count = query.filter.is_none()
1523        && query.group_by.is_empty()
1524        && query
1525            .aggregates
1526            .iter()
1527            .any(|agg| matches!(agg.function, AggregateFunction::Count));
1528
1529    // If unfiltered COUNT(*), use maintained cardinality instead of scanning rows
1530    // NOTE: Skip this optimization if record-level authorization is in effect,
1531    // since maintained cardinality doesn't account for per-record filtering
1532    if is_unfiltered_count && read_policy.is_none() {
1533        let cardinality = _db.collection_cardinality(&capability).unwrap_or(0);
1534
1535        let count_result = serde_json::json!(cardinality);
1536        let query_hash = query_hash(query)?;
1537        let execution_ms = query_start.elapsed().as_secs_f64() * 1000.0;
1538
1539        return Ok(QueryResult {
1540            query_id: format!("query:{}:{}", context.state_version, &query_hash[7..23]),
1541            query_hash,
1542            application_revision: context.revision_id.clone(),
1543            schema_version: context.schema_version,
1544            state_version: context.state_version,
1545            causal_cursor: std::collections::BTreeMap::new(),
1546            records: vec![],
1547            aggregates: vec![count_result],
1548            next_cursor: None,
1549            plan: QueryPlan {
1550                implementation: "maintained_count".into(),
1551                collection: query.collection.clone(),
1552                index: None,
1553                scan_fallback: false,
1554                access_method: Some("maintained_count".to_string()),
1555                estimated_rows: Some(1),
1556                fallback_reason: None,
1557                actual_rows_scanned: Some(0),
1558                actual_rows_returned: Some(1),
1559                matching_records: Some(1),
1560                unrelated_records_examined: Some(0),
1561                pagination_position: Some(0),
1562                execution_ms: Some(execution_ms),
1563                timing: Some(ExecutionTiming {
1564                    planning_ms: None,
1565                    index_lookup_ms: None,
1566                    filtering_ms: None,
1567                    serialization_ms: None,
1568                    total_ms: Some(execution_ms),
1569                }),
1570                index_metrics: Some(IndexMetrics {
1571                    index_used: None,
1572                    index_hits: 0,
1573                    index_misses: 0,
1574                    fallback_used: false,
1575                    fallback_reason: None,
1576                }),
1577            },
1578            operation_type: Some(OperationType::Query),
1579        });
1580    }
1581
1582    let (planned_index, index_name, index_predicate_field_and_value) =
1583        if let Some(filter) = &query.filter {
1584            if let Some((field, value)) = extract_indexed_equality(filter, definition) {
1585                let index_name = definition
1586                    .indexes
1587                    .iter()
1588                    .find(|idx| idx.fields.contains(&field))
1589                    .map(|idx| idx.name.clone());
1590
1591                if index_name.is_some() {
1592                    (true, index_name, Some((field, value)))
1593                } else {
1594                    (false, None, None)
1595                }
1596            } else {
1597                (false, None, None)
1598            }
1599        } else {
1600            (false, None, None)
1601        };
1602    let used_index = planned_index && context.live_query;
1603    // Substrate collections declare no indexes and already live inside the
1604    // application's tenant-scoped namespace; their bounded queries are exempt.
1605    if context.live_query
1606        && !substrate
1607        && query.filter.as_ref().is_some_and(contains_managed_scope)
1608        && !used_index
1609    {
1610        return Err(StateFailure::new(
1611            "INDEX_UNAVAILABLE",
1612            "managed tenant scope requires a schema-declared equality index",
1613        ));
1614    }
1615
1616    let planning_ms = query_start.elapsed().as_secs_f64() * 1000.0;
1617    let filtering_start = Instant::now();
1618
1619    // Execute against authoritative rows. A schema-declared equality index is
1620    // derived for this exact namespaced collection and must answer the query;
1621    // the managed path never silently reinterprets an indexed scope as a scan.
1622    let execution_before = crate::query_execution_diagnostics::counters();
1623    let index_lookup_start = Instant::now();
1624    let selected_rows = if !context.live_query {
1625        context
1626            .snapshot_rows
1627            .iter()
1628            .filter(|row| {
1629                row.capability == capability
1630                    && !row.deleted
1631                    && query
1632                        .filter
1633                        .as_ref()
1634                        .is_none_or(|filter| matches(&query_value(row), filter))
1635            })
1636            .cloned()
1637            .collect()
1638    } else if let Some((field, search_value)) = &index_predicate_field_and_value {
1639        _db.create_equality_index(&capability, field)
1640            .map_err(StateFailure::storage)?;
1641        _db.query_collection_by_equality(&capability, &[(field.as_str(), search_value)], |row| {
1642            !row.deleted
1643                && query
1644                    .filter
1645                    .as_ref()
1646                    .is_none_or(|filter| matches(&query_value(row), filter))
1647        })
1648        .map_err(StateFailure::storage)?
1649        .ok_or_else(|| {
1650            StateFailure::new(
1651                "INDEX_UNAVAILABLE",
1652                "the schema-declared scoped equality index is unavailable",
1653            )
1654        })?
1655    } else {
1656        _db.query_collection(&capability, None, |row| {
1657            !row.deleted
1658                && query
1659                    .filter
1660                    .as_ref()
1661                    .is_none_or(|filter| matches(&query_value(row), filter))
1662        })
1663        .map_err(StateFailure::storage)?
1664    };
1665    let index_lookup_ms = index_lookup_start.elapsed().as_secs_f64() * 1000.0;
1666    let execution_after = crate::query_execution_diagnostics::counters();
1667    let candidates_examined = if used_index {
1668        // The equality executor clones only predicate-matching candidates. Its
1669        // process counters remain useful operationally but cannot be differenced
1670        // per request under concurrency, so the response reports the isolated
1671        // result of this execution.
1672        selected_rows.len()
1673    } else {
1674        execution_after
1675            .scan_records_visited
1676            .saturating_sub(execution_before.scan_records_visited) as usize
1677    };
1678    let _predicate_evaluated = execution_after
1679        .records_predicate_evaluated
1680        .saturating_sub(execution_before.records_predicate_evaluated)
1681        as usize;
1682    let mut values = selected_rows.iter().map(query_value).collect::<Vec<_>>();
1683
1684    if let Some(policy) = &read_policy {
1685        let actor =
1686            if context.authorization.subject.is_empty() || context.authorization.subject == ":" {
1687                None
1688            } else {
1689                context
1690                    .authorization
1691                    .subject
1692                    .split(':')
1693                    .nth(1)
1694                    .map(|id| Actor::new(id))
1695            };
1696
1697        let auth_state = matches!(policy, &PolicySubject::Member).then(|| {
1698            AuthorizationState::new(
1699                Arc::new(schema.clone()),
1700                context.state_namespace.clone(),
1701                Arc::new(policy_snapshot.clone()),
1702            )
1703        });
1704
1705        values.retain(|record_value| {
1706            if let Some(record_id) = record_value.get("_id").and_then(Value::as_str) {
1707                let mut record_context = RecordAuthorizationContext {
1708                    actor: actor.clone(),
1709                    collection: query.collection.clone(),
1710                    record_id: record_id.to_string(),
1711                    record_value: record_value.clone(),
1712                    authorization_state: None,
1713                };
1714                if let Some(state) = &auth_state {
1715                    record_context.authorization_state = Some(state.clone());
1716                }
1717
1718                PolicyEvaluator::evaluate_record(policy.clone(), &record_context).is_ok()
1719            } else {
1720                true
1721            }
1722        });
1723    }
1724
1725    let reference_rows =
1726        load_reference_rows(_db, &context.state_namespace, &values, &query.references)?;
1727    for value in &mut values {
1728        for reference in &query.references {
1729            let id = value.get(&reference.field).and_then(Value::as_str);
1730            let related = id
1731                .and_then(|id| {
1732                    reference_rows
1733                        .get(&(reference.collection.clone(), id.to_string()))
1734                        .filter(|value| !value.is_null())
1735                })
1736                .map(|value| projected(value, &reference.projection))
1737                .unwrap_or(Value::Null);
1738            if let Some(object) = value.as_object_mut() {
1739                object.insert(reference.as_name.clone(), related);
1740            }
1741        }
1742    }
1743    values.sort_by(|a, b| {
1744        for order in &query.order_by {
1745            let cmp = a
1746                .get(&order.field)
1747                .zip(b.get(&order.field))
1748                .and_then(|(x, y)| compare_value(x, y))
1749                .unwrap_or(Ordering::Equal);
1750            if cmp != Ordering::Equal {
1751                return if matches!(order.direction, SortDirection::Desc) {
1752                    cmp.reverse()
1753                } else {
1754                    cmp
1755                };
1756            }
1757        }
1758        compare_value(
1759            a.get("_id").unwrap_or(&Value::Null),
1760            b.get("_id").unwrap_or(&Value::Null),
1761        )
1762        .unwrap_or(Ordering::Equal)
1763    });
1764    let pagination_hash = pagination_query_hash(query)?;
1765    let start = if let Some(cursor) = &query.cursor {
1766        decode_query_cursor(cursor, &pagination_hash)?
1767    } else {
1768        query.offset
1769    };
1770    let limit = query.limit.unwrap_or(100).min(1000);
1771    let end = (start + limit).min(values.len());
1772    let page = if start <= values.len() {
1773        values[start..end].to_vec()
1774    } else {
1775        vec![]
1776    };
1777    let filtering_ms = filtering_start.elapsed().as_secs_f64() * 1000.0;
1778    let aggregates = aggregate(&values, &query.group_by, &query.aggregates)?;
1779    let hash = pagination_hash;
1780
1781    let (execution_strategy, scan_fallback_flag, access_method) = if used_index {
1782        ("index_lookup".into(), false, Some("index".to_string()))
1783    } else {
1784        (
1785            "full_table_scan".into(),
1786            true,
1787            Some("sequential_scan".to_string()),
1788        )
1789    };
1790
1791    let rows_scanned = candidates_examined;
1792
1793    let index_metrics = Some(IndexMetrics {
1794        index_used: index_name.clone(),
1795        index_hits: if used_index { candidates_examined } else { 0 },
1796        index_misses: 0,
1797        fallback_used: scan_fallback_flag,
1798        fallback_reason: if scan_fallback_flag {
1799            Some("no_applicable_index".to_string())
1800        } else {
1801            None
1802        },
1803    });
1804
1805    let serialization_start = Instant::now();
1806    let serialized_records: Vec<Value> = page
1807        .iter()
1808        .map(|v| projected(v, &effective_projection))
1809        .collect();
1810    let serialization_ms = serialization_start.elapsed().as_secs_f64() * 1000.0;
1811
1812    let total_ms = query_start.elapsed().as_secs_f64() * 1000.0;
1813    let returned_count = serialized_records.len();
1814    let next_cursor = (end < values.len()).then(|| encode_query_cursor(&hash, end));
1815
1816    let timing = ExecutionTiming {
1817        planning_ms: Some(planning_ms),
1818        index_lookup_ms: used_index.then_some(index_lookup_ms),
1819        filtering_ms: Some(filtering_ms),
1820        serialization_ms: Some(serialization_ms),
1821        total_ms: Some(total_ms),
1822    };
1823
1824    Ok(QueryResult {
1825        query_id: format!("query:{}:{}", context.state_version, &hash[7..23]),
1826        query_hash: hash,
1827        application_revision: context.revision_id.clone(),
1828        schema_version: context.schema_version,
1829        state_version: context.state_version,
1830        causal_cursor: context.causal_cursor.clone(),
1831        records: serialized_records,
1832        aggregates,
1833        next_cursor,
1834        plan: QueryPlan {
1835            implementation: execution_strategy,
1836            collection: query.collection.clone(),
1837            index: index_name,
1838            scan_fallback: scan_fallback_flag,
1839            access_method,
1840            estimated_rows: Some(values.len()),
1841            fallback_reason: if scan_fallback_flag {
1842                Some("no_applicable_index".to_string())
1843            } else {
1844                None
1845            },
1846            actual_rows_scanned: Some(rows_scanned),
1847            actual_rows_returned: Some(returned_count),
1848            matching_records: Some(values.len()),
1849            unrelated_records_examined: Some(0),
1850            pagination_position: Some(start),
1851            execution_ms: Some(total_ms),
1852            timing: Some(timing),
1853            index_metrics,
1854        },
1855        operation_type: Some(OperationType::Query),
1856    })
1857}
1858fn aggregate(
1859    values: &[Value],
1860    groups: &[String],
1861    defs: &[AggregateDefinition],
1862) -> Result<Vec<Value>, StateFailure> {
1863    if defs.is_empty() {
1864        return Ok(vec![]);
1865    }
1866    let mut buckets: BTreeMap<String, (Map<String, Value>, Vec<&Value>)> = BTreeMap::new();
1867    for value in values {
1868        let mut key = Map::new();
1869        for field in groups {
1870            key.insert(
1871                field.clone(),
1872                value.get(field).cloned().unwrap_or(Value::Null),
1873            );
1874        }
1875        let encoded = serde_json::to_string(&key).map_err(StateFailure::storage)?;
1876        buckets
1877            .entry(encoded)
1878            .or_insert((key, vec![]))
1879            .1
1880            .push(value);
1881    }
1882    let mut out = vec![];
1883    for (_, (group, rows)) in buckets {
1884        let mut result = group;
1885        for def in defs {
1886            let nums = def
1887                .field
1888                .as_ref()
1889                .map(|field| {
1890                    rows.iter()
1891                        .filter_map(|v| v.get(field).and_then(Value::as_f64))
1892                        .collect::<Vec<_>>()
1893                })
1894                .unwrap_or_default();
1895            let value = match def.function {
1896                AggregateFunction::Count => Value::from(rows.len() as u64),
1897                AggregateFunction::Sum => Value::from(nums.iter().sum::<f64>()),
1898                AggregateFunction::Avg => {
1899                    if nums.is_empty() {
1900                        Value::Null
1901                    } else {
1902                        Value::from(nums.iter().sum::<f64>() / nums.len() as f64)
1903                    }
1904                }
1905                AggregateFunction::Min => nums
1906                    .into_iter()
1907                    .reduce(f64::min)
1908                    .map(Value::from)
1909                    .unwrap_or(Value::Null),
1910                AggregateFunction::Max => nums
1911                    .into_iter()
1912                    .reduce(f64::max)
1913                    .map(Value::from)
1914                    .unwrap_or(Value::Null),
1915            };
1916            result.insert(def.name.clone(), value);
1917        }
1918        out.push(Value::Object(result));
1919    }
1920    Ok(out)
1921}
1922
1923#[derive(Debug, Clone, Serialize, Deserialize)]
1924#[serde(rename_all = "snake_case")]
1925pub enum TransactionOperationKind {
1926    /// Create; refused with `PRECONDITION_FAILED` (`present`) when the record exists.
1927    Insert,
1928    /// Merge `value` into the existing record; refused when it does not exist.
1929    Update,
1930    Delete,
1931    /// Create or replace: the record becomes exactly `value`. The canonical form of an
1932    /// unconditional or version-guarded collection write, which creates an absent record
1933    /// and replaces an existing one on every runtime (docs/reference/public-api.md,
1934    /// "Collection write semantics"). Guards travel as preconditions, as for the others.
1935    Put,
1936}
1937#[derive(Debug, Clone, Serialize, Deserialize)]
1938pub struct TransactionOperation {
1939    pub kind: TransactionOperationKind,
1940    pub collection: String,
1941    pub id: String,
1942    #[serde(default)]
1943    pub value: Value,
1944    #[serde(default)]
1945    pub if_version: Option<u64>,
1946}
1947#[derive(Debug, Clone, Serialize, Deserialize)]
1948#[serde(tag = "kind", rename_all = "snake_case")]
1949pub enum TransactionPrecondition {
1950    CollectionVersion {
1951        collection: String,
1952        version: u64,
1953    },
1954    StateVersion {
1955        version: u64,
1956    },
1957    ApplicationRevision {
1958        revision_id: String,
1959    },
1960    CausalCursor {
1961        cursor: BTreeMap<String, u64>,
1962    },
1963    /// What must be true of one record for the transaction to commit.
1964    ///
1965    /// Distinct from every variant above, and evaluated in a different place
1966    /// for a reason: those compare counters that can be read before the commit
1967    /// lock is taken, while this one compares the fields a caller reads back —
1968    /// the document's `__version`, its authority epoch, its lease — and must
1969    /// therefore be evaluated *inside* the same lock as the writes. Checking it
1970    /// against the pre-commit snapshot would be a client-side check wearing the
1971    /// authority's name: between the read and the write, another writer
1972    /// commits.
1973    ///
1974    /// It is also what makes `putIfAbsent` and `updateIfVersion` mean the same
1975    /// thing against a managed authority as against the file runtime. Mapping
1976    /// a caller's `expectedVersion` onto `if_version` would not: that fences
1977    /// the row's internal storage sequence, which is a different number that
1978    /// happens to share a name.
1979    Record {
1980        collection: String,
1981        id: String,
1982        #[serde(default)]
1983        require_absent: bool,
1984        #[serde(default)]
1985        expected_version: Option<u64>,
1986        #[serde(default)]
1987        expected_epoch: Option<u64>,
1988        #[serde(default)]
1989        expected_lease_id: Option<String>,
1990    },
1991}
1992#[derive(Debug, Clone, Serialize, Deserialize)]
1993pub struct TransactionRequest {
1994    #[serde(default)]
1995    pub transaction_id: Option<String>,
1996    pub tenant_id: String,
1997    pub application_id: String,
1998    pub revision_id: String,
1999    pub schema_version: u64,
2000    #[serde(default)]
2001    pub state_namespace: Option<String>,
2002    #[serde(default)]
2003    /// Optimistic authority revision. A mismatched revision is rejected.
2004    #[serde(rename = "base_revision")]
2005    pub causal_parent: Option<u64>,
2006    pub authorization: AuthorizationContext,
2007    pub operations: Vec<TransactionOperation>,
2008    #[serde(default)]
2009    pub preconditions: Vec<TransactionPrecondition>,
2010    /// Read bases this authority issued for this state namespace
2011    /// (`capture_read_basis`), re-evaluated inside the commit lock.
2012    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2013    pub fences: Vec<crate::ReadBasis>,
2014}
2015
2016/// Collections the composition substrate keeps inside an application's state
2017/// namespace. They are not part of the application contract: their records
2018/// are validated by the substrate, and reading or writing them requires the
2019/// dedicated `composition:read` / `composition:write` capabilities rather
2020/// than `state:*`. Every other `_feltdb` name remains unwritable here.
2021pub const SUBSTRATE_COLLECTIONS: &[&str] = &[
2022    "_feltdb.CompositionContract",
2023    "_feltdb.Composition",
2024    "_feltdb.CompositionVersion",
2025    "_feltdb.CompositionTopology",
2026    "_feltdb.StateOwnership",
2027    "_feltdb.StateDependency",
2028    "_feltdb.StateChange",
2029    "_feltdb.StateAttribution",
2030    "_feltdb.StateObservation",
2031    "_feltdb.StateProposal",
2032    "_feltdb.StateImpact",
2033    "_feltdb.Intent",
2034    "_feltdb.IntentEvent",
2035    "_feltdb.ProposalEvent",
2036];
2037
2038pub fn is_substrate_collection(name: &str) -> bool {
2039    SUBSTRATE_COLLECTIONS.contains(&name)
2040}
2041
2042/// Capture a read basis over an application state namespace.
2043///
2044/// Every collection the read set names must be one the caller may read in
2045/// full: an application collection readable under `state:read` whose read
2046/// policy does not depend on the record, or a substrate collection under
2047/// `composition:read`. A token is a digest over every row it covers, so a
2048/// basis over rows the caller could not read would disclose them; such a
2049/// read set is refused, never digested.
2050pub fn capture_read_basis(
2051    db: &FeltDb,
2052    schema: &StateSchema,
2053    state_namespace: &str,
2054    reads: &crate::ReadSet,
2055    readable: &dyn Fn(&str) -> bool,
2056) -> Result<crate::ReadBasis, StateFailure> {
2057    let reads = reads
2058        .normalised()
2059        .map_err(|error| StateFailure::new("VALIDATION_FAILED", &error.to_string()))?;
2060    authorize_read_set(schema, &reads, readable)?;
2061    db.read_basis_in(
2062        &reads,
2063        &crate::ReadLayout::Namespace(state_namespace.to_string()),
2064    )
2065    .map_err(StateFailure::storage)
2066}
2067
2068/// The refusal for a read basis over a collection the caller cannot read in full.
2069pub fn read_basis_denied(collection: &str) -> StateFailure {
2070    let mut failure = StateFailure::new(
2071        "AUTHORIZATION_DENIED",
2072        "a read basis may only cover collections the caller can read in full",
2073    );
2074    failure.resource = Some(collection.to_string());
2075    failure
2076}
2077
2078fn authorize_read_set(
2079    schema: &StateSchema,
2080    reads: &crate::ReadSet,
2081    readable: &dyn Fn(&str) -> bool,
2082) -> Result<(), StateFailure> {
2083    let collections = reads
2084        .records
2085        .iter()
2086        .map(|record| record.collection.as_str())
2087        .chain(
2088            reads
2089                .predicates
2090                .iter()
2091                .map(|predicate| predicate.collection.as_str()),
2092        );
2093    for name in collections {
2094        if !is_substrate_collection(name) {
2095            collection(schema, name)?;
2096        }
2097        if !readable(name) {
2098            return Err(read_basis_denied(name));
2099        }
2100    }
2101    Ok(())
2102}
2103#[derive(Debug, Clone, Serialize, Deserialize)]
2104pub struct TransactionAudit {
2105    pub transaction_id: String,
2106    pub subject: String,
2107    pub tenant: String,
2108    pub application: String,
2109    pub revision: String,
2110    pub schema_version: u64,
2111    pub state_before: u64,
2112    pub state_after: u64,
2113    pub operations: usize,
2114    pub authorization_decisions: Vec<String>,
2115    pub causal_parent: Option<u64>,
2116    pub result: String,
2117}
2118#[derive(Debug, Clone, Serialize, Deserialize)]
2119pub struct TransactionResult {
2120    pub transaction_id: String,
2121    pub base_revision: u64,
2122    pub commit_revision: u64,
2123    pub status: crate::TransactionStatus,
2124    pub state_before: u64,
2125    pub state_after: u64,
2126    pub causal_cursor: BTreeMap<String, u64>,
2127    pub duplicate: bool,
2128    pub audit: TransactionAudit,
2129}
2130
2131fn valid_type(value: &Value, field: &FieldSchema) -> bool {
2132    if value.is_null() {
2133        return field.nullable;
2134    }
2135    match &field.field_type {
2136        FieldType::Primitive { primitive } => match primitive {
2137            PrimitiveType::String => value.is_string(),
2138            PrimitiveType::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
2139            PrimitiveType::Number => value.is_number(),
2140            PrimitiveType::Boolean => value.is_boolean(),
2141            PrimitiveType::Json => true,
2142            PrimitiveType::Timestamp => value.as_str().is_some_and(valid_timestamp),
2143            PrimitiveType::Date => value.as_str().is_some_and(valid_date),
2144            PrimitiveType::Time => value.as_str().is_some_and(valid_time),
2145            PrimitiveType::Uuid => value.as_str().is_some_and(valid_uuid),
2146            PrimitiveType::Decimal => value.as_str().is_some_and(valid_decimal),
2147            PrimitiveType::Bigint => value.as_str().is_some_and(valid_integer_string),
2148            PrimitiveType::Email => value.as_str().is_some_and(valid_email),
2149            PrimitiveType::Url => value.as_str().is_some_and(valid_url),
2150            PrimitiveType::Phone => value.as_str().is_some_and(valid_phone),
2151            PrimitiveType::Binary => value.as_str().is_some_and(valid_base64),
2152            PrimitiveType::Money => valid_money(value),
2153            PrimitiveType::File => valid_file(value),
2154            PrimitiveType::GeoPoint => valid_geo(value),
2155            PrimitiveType::Object => value.is_object(),
2156        },
2157        FieldType::Enum { values } => value
2158            .as_str()
2159            .is_some_and(|v| values.iter().any(|x| x == v)),
2160        FieldType::Reference { .. } => value.is_string(),
2161        FieldType::Array { items } => value.as_array().is_some_and(|values| {
2162            values.iter().all(|value| {
2163                valid_type(
2164                    value,
2165                    &FieldSchema {
2166                        name: "item".into(),
2167                        field_type: (**items).clone(),
2168                        nullable: false,
2169                        required: true,
2170                        default: None,
2171                        constraints: Default::default(),
2172                        computed: None,
2173                    },
2174                )
2175            })
2176        }),
2177        FieldType::Map { values } => value.as_object().is_some_and(|entries| {
2178            entries
2179                .values()
2180                .all(|value| valid_nested_type(value, values))
2181        }),
2182        FieldType::Vector { dimensions } => value.as_array().is_some_and(|values| {
2183            dimensions.is_none_or(|expected| values.len() == expected)
2184                && values.iter().all(Value::is_number)
2185        }),
2186    }
2187}
2188
2189fn valid_nested_type(value: &Value, field_type: &FieldType) -> bool {
2190    valid_type(
2191        value,
2192        &FieldSchema {
2193            name: "value".into(),
2194            field_type: field_type.clone(),
2195            nullable: false,
2196            required: true,
2197            default: None,
2198            constraints: Default::default(),
2199            computed: None,
2200        },
2201    )
2202}
2203fn valid_integer_string(value: &str) -> bool {
2204    !value.is_empty()
2205        && value
2206            .strip_prefix('-')
2207            .unwrap_or(value)
2208            .chars()
2209            .all(|c| c.is_ascii_digit())
2210}
2211fn valid_decimal(value: &str) -> bool {
2212    let unsigned = value.strip_prefix('-').unwrap_or(value);
2213    let mut parts = unsigned.split('.');
2214    parts
2215        .next()
2216        .is_some_and(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
2217        && parts
2218            .next()
2219            .is_none_or(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
2220        && parts.next().is_none()
2221}
2222fn valid_date(value: &str) -> bool {
2223    value.len() == 10
2224        && value.as_bytes()[4] == b'-'
2225        && value.as_bytes()[7] == b'-'
2226        && value[..4].parse::<u16>().is_ok()
2227        && value[5..7]
2228            .parse::<u8>()
2229            .is_ok_and(|v| (1..=12).contains(&v))
2230        && value[8..]
2231            .parse::<u8>()
2232            .is_ok_and(|v| (1..=31).contains(&v))
2233}
2234fn valid_time(value: &str) -> bool {
2235    value.len() >= 5
2236        && value.as_bytes().get(2) == Some(&b':')
2237        && value[..2].parse::<u8>().is_ok_and(|v| v < 24)
2238        && value[3..5].parse::<u8>().is_ok_and(|v| v < 60)
2239        && (value.len() == 5
2240            || value.as_bytes().get(5) == Some(&b':')
2241                && value
2242                    .get(6..8)
2243                    .is_some_and(|v| v.parse::<u8>().is_ok_and(|s| s < 60)))
2244}
2245fn valid_timestamp(value: &str) -> bool {
2246    value.get(..10).is_some_and(valid_date)
2247        && value
2248            .as_bytes()
2249            .get(10)
2250            .is_some_and(|v| *v == b'T' || *v == b' ')
2251        && value.get(11..).is_some_and(valid_time)
2252}
2253fn valid_uuid(value: &str) -> bool {
2254    value.len() == 36
2255        && value.chars().enumerate().all(|(i, c)| {
2256            matches!(i, 8 | 13 | 18 | 23) && c == '-'
2257                || !matches!(i, 8 | 13 | 18 | 23) && c.is_ascii_hexdigit()
2258        })
2259}
2260fn valid_email(value: &str) -> bool {
2261    let mut parts = value.split('@');
2262    parts.next().is_some_and(|v| !v.is_empty())
2263        && parts
2264            .next()
2265            .is_some_and(|v| v.contains('.') && !v.starts_with('.') && !v.ends_with('.'))
2266        && parts.next().is_none()
2267}
2268fn valid_url(value: &str) -> bool {
2269    (value.starts_with("https://") || value.starts_with("http://"))
2270        && value.split("//").nth(1).is_some_and(|v| !v.is_empty())
2271}
2272fn valid_phone(value: &str) -> bool {
2273    let digits = value.chars().filter(|c| c.is_ascii_digit()).count();
2274    (7..=15).contains(&digits)
2275        && value
2276            .chars()
2277            .all(|c| c.is_ascii_digit() || "+-(). ".contains(c))
2278}
2279fn valid_base64(value: &str) -> bool {
2280    !value.is_empty()
2281        && value.len() % 4 == 0
2282        && value
2283            .chars()
2284            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '='))
2285}
2286fn valid_money(value: &Value) -> bool {
2287    value.as_object().is_some_and(|v| {
2288        v.get("amount")
2289            .and_then(Value::as_str)
2290            .is_some_and(valid_decimal)
2291            && v.get("currency")
2292                .and_then(Value::as_str)
2293                .is_some_and(|c| c.len() == 3 && c.chars().all(|x| x.is_ascii_uppercase()))
2294    })
2295}
2296fn valid_file(value: &Value) -> bool {
2297    value.as_object().is_some_and(|v| {
2298        v.get("name").is_some_and(Value::is_string)
2299            && v.get("type").is_some_and(Value::is_string)
2300            && v.get("size").and_then(Value::as_u64).is_some()
2301            && (v.get("url").is_some_and(Value::is_string)
2302                || v.get("ref").is_some_and(Value::is_string))
2303    })
2304}
2305fn valid_geo(value: &Value) -> bool {
2306    value.as_object().is_some_and(|v| {
2307        v.get("latitude")
2308            .and_then(Value::as_f64)
2309            .is_some_and(|x| (-90.0..=90.0).contains(&x))
2310            && v.get("longitude")
2311                .and_then(Value::as_f64)
2312                .is_some_and(|x| (-180.0..=180.0).contains(&x))
2313    })
2314}
2315fn validate_record(collection: &CollectionSchema, value: &mut Value) -> Result<(), StateFailure> {
2316    let object = value
2317        .as_object_mut()
2318        .ok_or_else(|| StateFailure::new("VALIDATION_FAILED", "record must be an object"))?;
2319    for field in &collection.fields {
2320        if let Some(expression) = &field.computed {
2321            let computed = if let Some(source) = expression.strip_prefix("copy:") {
2322                object.get(source.trim()).cloned().unwrap_or(Value::Null)
2323            } else if let Some(sources) = expression.strip_prefix("concat:") {
2324                Value::String(
2325                    sources
2326                        .split(',')
2327                        .filter_map(|source| object.get(source.trim()).and_then(Value::as_str))
2328                        .collect::<String>(),
2329                )
2330            } else {
2331                return Err(StateFailure::new(
2332                    "VALIDATION_FAILED",
2333                    "unsupported computed expression",
2334                ));
2335            };
2336            object.insert(field.name.clone(), computed);
2337        }
2338    }
2339    for field in &collection.fields {
2340        if !object.contains_key(&field.name) {
2341            if let Some(default) = &field.default {
2342                object.insert(field.name.clone(), default.clone());
2343            } else if field.required && field.computed.is_none() {
2344                return Err(StateFailure::new(
2345                    "VALIDATION_FAILED",
2346                    &format!("missing required field {}", field.name),
2347                ));
2348            }
2349        }
2350        if let Some(value) = object.get(&field.name) {
2351            if !valid_type(value, field) {
2352                return Err(StateFailure::new(
2353                    "VALIDATION_FAILED",
2354                    &format!("invalid value for {}", field.name),
2355                ));
2356            }
2357            if let Some(text) = value.as_str() {
2358                if field.constraints.min_length.is_some_and(|n| text.len() < n)
2359                    || field.constraints.max_length.is_some_and(|n| text.len() > n)
2360                {
2361                    return Err(StateFailure::new(
2362                        "VALIDATION_FAILED",
2363                        &format!("length constraint failed for {}", field.name),
2364                    ));
2365                }
2366            }
2367            if let Some(number) = value.as_f64() {
2368                if field.constraints.minimum.is_some_and(|n| number < n)
2369                    || field.constraints.maximum.is_some_and(|n| number > n)
2370                {
2371                    return Err(StateFailure::new(
2372                        "VALIDATION_FAILED",
2373                        &format!("numeric constraint failed for {}", field.name),
2374                    ));
2375                }
2376            }
2377        }
2378    }
2379    // `__version` and `id` are record identity, carried by every client
2380    // record whether or not the contract declares them; a declared `id` is
2381    // validated by its declaration above.
2382    if object.keys().any(|name| {
2383        name != "__version" && name != "id" && !collection.fields.iter().any(|f| &f.name == name)
2384    }) {
2385        return Err(StateFailure::new(
2386            "VALIDATION_FAILED",
2387            "record contains an unknown field",
2388        ));
2389    }
2390    Ok(())
2391}
2392pub fn execute_transaction(
2393    db: &FeltDb,
2394    schema: &StateSchema,
2395    request: &TransactionRequest,
2396    write_policy: Option<PolicySubject>,
2397) -> Result<TransactionResult, StateFailure> {
2398    execute_transaction_with_policies(db, schema, request, write_policy.as_ref(), None)
2399}
2400pub fn execute_transaction_with_collection_policies(
2401    db: &FeltDb,
2402    schema: &StateSchema,
2403    request: &TransactionRequest,
2404    write_policies: &BTreeMap<String, PolicySubject>,
2405) -> Result<TransactionResult, StateFailure> {
2406    execute_transaction_with_policies(db, schema, request, None, Some(write_policies))
2407}
2408fn execute_transaction_with_policies(
2409    db: &FeltDb,
2410    schema: &StateSchema,
2411    request: &TransactionRequest,
2412    uniform_write_policy: Option<&PolicySubject>,
2413    collection_write_policies: Option<&BTreeMap<String, PolicySubject>>,
2414) -> Result<TransactionResult, StateFailure> {
2415    if request.application_id != schema.application_id
2416        || request.revision_id != schema.revision_id
2417        || request.schema_version != schema.schema_version
2418    {
2419        return Err(StateFailure::new(
2420            "SCHEMA_MISMATCH",
2421            "transaction does not match state schema",
2422        ));
2423    }
2424    if request.authorization.subject.is_empty()
2425        || request.authorization.tenant_id != request.tenant_id
2426        || request.authorization.application_id != request.application_id
2427        || request.authorization.revision_id != request.revision_id
2428    {
2429        return Err(StateFailure::new(
2430            "AUTHORIZATION_DENIED",
2431            "transaction authorization context is invalid",
2432        ));
2433    }
2434    if !request.authorization.can("state:write") {
2435        return Err(StateFailure::new(
2436            "AUTHORIZATION_DENIED",
2437            "state:write is required",
2438        ));
2439    }
2440    let canonical = if request.fences.is_empty() {
2441        serde_json::to_vec(&(
2442            request.tenant_id.as_str(),
2443            request.application_id.as_str(),
2444            request.revision_id.as_str(),
2445            request.schema_version,
2446            request.state_namespace.as_deref(),
2447            request.causal_parent,
2448            &request.authorization.subject,
2449            &request.preconditions,
2450            &request.operations,
2451        ))
2452    } else {
2453        // Fences are part of what the transaction means, so they bind its
2454        // identity. Unfenced transactions hash exactly as they always have.
2455        serde_json::to_vec(&(
2456            request.tenant_id.as_str(),
2457            request.application_id.as_str(),
2458            request.revision_id.as_str(),
2459            request.schema_version,
2460            request.state_namespace.as_deref(),
2461            request.causal_parent,
2462            &request.authorization.subject,
2463            &request.preconditions,
2464            &request.operations,
2465            &request.fences,
2466        ))
2467    }
2468    .map_err(StateFailure::storage)?;
2469    let transaction_id = request
2470        .transaction_id
2471        .clone()
2472        .unwrap_or_else(|| format!("tx_{:x}", Sha256::digest(&canonical)));
2473    // Client transaction identities are application-local. The managed store
2474    // serves many applications from one durable log, so using the bare client
2475    // id as the substrate id lets one application collide with another.
2476    // Hash the authorization boundary into the private storage identity while
2477    // preserving the caller's id in every public result and audit record.
2478    let storage_transaction_id = format!(
2479        "txscope_{:x}_{}",
2480        Sha256::digest(
2481            format!(
2482                "{}\0{}\0{}",
2483                request.tenant_id,
2484                request.application_id,
2485                request
2486                    .state_namespace
2487                    .as_deref()
2488                    .unwrap_or(&request.application_id)
2489            )
2490            .as_bytes()
2491        ),
2492        transaction_id
2493    );
2494    let payload_hash = format!("sha256:{:x}", Sha256::digest(&canonical));
2495    // A retried id is answered before anything is evaluated against current
2496    // state, with the original commit: its revisions and positions, not the
2497    // store's current ones. The same answer the commit lock gives a retry that
2498    // races past this check.
2499    if let Some(result) = committed_result(
2500        db,
2501        request,
2502        &transaction_id,
2503        &storage_transaction_id,
2504        &payload_hash,
2505    )? {
2506        return Ok(result);
2507    }
2508    match commit_new_transaction(
2509        db,
2510        schema,
2511        request,
2512        uniform_write_policy,
2513        collection_write_policies,
2514        &transaction_id,
2515        &storage_transaction_id,
2516        &payload_hash,
2517    ) {
2518        Ok(result) => Ok(result),
2519        // The request was evaluated against state read before the commit
2520        // lock. An identical request that committed in between makes this one
2521        // its duplicate, and the duplicate -- the original commit -- is the
2522        // answer, not the refusal that later state produced (a create that
2523        // now "exists", a fence that now "changed").
2524        Err(failure) => match committed_result(
2525            db,
2526            request,
2527            &transaction_id,
2528            &storage_transaction_id,
2529            &payload_hash,
2530        )? {
2531            Some(result) => Ok(result),
2532            None => Err(failure),
2533        },
2534    }
2535}
2536
2537/// A retried id, answered with the original commit: its revisions and
2538/// positions, never the store's current ones. `IDEMPOTENCY_CONFLICT` when the id
2539/// committed with a different payload; `None` when it has not committed.
2540fn committed_result(
2541    db: &FeltDb,
2542    request: &TransactionRequest,
2543    transaction_id: &str,
2544    storage_transaction_id: &str,
2545    payload_hash: &str,
2546) -> Result<Option<TransactionResult>, StateFailure> {
2547    let Some(original) = db
2548        .committed_transaction(storage_transaction_id, Some(payload_hash))
2549        .map_err(|error| match error {
2550            crate::FlowError::TransactionIdPayloadMismatch { .. } => idempotency_conflict(),
2551            other => StateFailure::storage(other),
2552        })?
2553    else {
2554        return Ok(None);
2555    };
2556    let cursor = db
2557        .operation_versions()
2558        .map_err(StateFailure::storage)?
2559        .into_iter()
2560        .collect();
2561    let audit = TransactionAudit {
2562        transaction_id: transaction_id.to_string(),
2563        subject: request.authorization.subject.clone(),
2564        tenant: request.tenant_id.clone(),
2565        application: request.application_id.clone(),
2566        revision: request.revision_id.clone(),
2567        schema_version: request.schema_version,
2568        state_before: original.state_before,
2569        state_after: original.state_after,
2570        operations: request.operations.len(),
2571        authorization_decisions: vec!["state:write allowed".into()],
2572        causal_parent: request.causal_parent,
2573        result: "duplicate".into(),
2574    };
2575    Ok(Some(TransactionResult {
2576        transaction_id: transaction_id.to_string(),
2577        base_revision: original.base_revision,
2578        commit_revision: original.commit_revision,
2579        status: original.status,
2580        state_before: original.state_before,
2581        state_after: original.state_after,
2582        causal_cursor: cursor,
2583        duplicate: true,
2584        audit,
2585    }))
2586}
2587
2588/// Evaluate and commit a transaction id that has not committed.
2589#[allow(clippy::too_many_arguments)]
2590fn commit_new_transaction(
2591    db: &FeltDb,
2592    schema: &StateSchema,
2593    request: &TransactionRequest,
2594    uniform_write_policy: Option<&PolicySubject>,
2595    collection_write_policies: Option<&BTreeMap<String, PolicySubject>>,
2596    transaction_id: &str,
2597    storage_transaction_id: &str,
2598    payload_hash: &str,
2599) -> Result<TransactionResult, StateFailure> {
2600    let transaction_id = transaction_id.to_string();
2601    let storage_transaction_id = storage_transaction_id.to_string();
2602    let payload_hash = payload_hash.to_string();
2603    let namespace = request
2604        .state_namespace
2605        .as_deref()
2606        .unwrap_or(&request.application_id)
2607        .to_string();
2608    let snapshot = transaction_snapshot_rows(
2609        db,
2610        schema,
2611        request,
2612        uniform_write_policy,
2613        collection_write_policies,
2614    )?;
2615    // Record preconditions are collected here and evaluated by the store,
2616    // inside the same lock as the writes. Evaluating them against `snapshot`
2617    // alongside the counters below would reintroduce exactly the gap they
2618    // exist to close.
2619    let mut record_preconditions: Vec<RecordPrecondition> = Vec::new();
2620    for condition in &request.preconditions {
2621        let (resource, expected, actual, passed) = match condition {
2622            TransactionPrecondition::Record {
2623                collection,
2624                id,
2625                require_absent,
2626                expected_version,
2627                expected_epoch,
2628                expected_lease_id,
2629            } => {
2630                let precondition = RecordPrecondition {
2631                    capability: format!("{namespace}:{collection}"),
2632                    key: id.clone(),
2633                    require_absent: *require_absent,
2634                    expected_version: *expected_version,
2635                    expected_epoch: *expected_epoch,
2636                    expected_lease_id: expected_lease_id.clone(),
2637                };
2638                if precondition.is_empty() {
2639                    // Refused rather than ignored: a precondition that
2640                    // constrains nothing reads as protection and provides none.
2641                    let mut failure = StateFailure::new(
2642                        "VALIDATION_FAILED",
2643                        &format!("precondition on {collection}:{id} constrains nothing"),
2644                    );
2645                    failure.resource = Some(format!("{collection}:{id}"));
2646                    return Err(failure);
2647                }
2648                record_preconditions.push(precondition);
2649                continue;
2650            }
2651            TransactionPrecondition::CollectionVersion {
2652                collection,
2653                version,
2654            } => {
2655                let capability = format!(
2656                    "{}:{}",
2657                    request
2658                        .state_namespace
2659                        .as_deref()
2660                        .unwrap_or(&request.application_id),
2661                    collection
2662                );
2663                let actual = snapshot
2664                    .iter()
2665                    .filter(|row| row.capability == capability)
2666                    .filter_map(|row| row.operation.as_ref().map(|op| op.sequence))
2667                    .max()
2668                    .unwrap_or(0);
2669                (
2670                    collection.clone(),
2671                    Value::from(*version),
2672                    Value::from(actual),
2673                    actual == *version,
2674                )
2675            }
2676            TransactionPrecondition::StateVersion { version } => {
2677                let actual = db.sequence().map_err(StateFailure::storage)?;
2678                (
2679                    "state".into(),
2680                    Value::from(*version),
2681                    Value::from(actual),
2682                    actual == *version,
2683                )
2684            }
2685            TransactionPrecondition::ApplicationRevision { revision_id } => (
2686                ("application_revision").into(),
2687                Value::String(revision_id.clone()),
2688                Value::String(schema.revision_id.clone()),
2689                revision_id == &schema.revision_id,
2690            ),
2691            TransactionPrecondition::CausalCursor { cursor } => {
2692                let actual: BTreeMap<_, _> = db
2693                    .operation_versions()
2694                    .map_err(StateFailure::storage)?
2695                    .into_iter()
2696                    .collect();
2697                (
2698                    "causal_cursor".into(),
2699                    serde_json::to_value(cursor).map_err(StateFailure::storage)?,
2700                    serde_json::to_value(&actual).map_err(StateFailure::storage)?,
2701                    cursor == &actual,
2702                )
2703            }
2704        };
2705        if !passed {
2706            let mut failure =
2707                StateFailure::new("PRECONDITION_FAILED", "transaction precondition failed");
2708            failure.resource = Some(resource);
2709            failure.expected = Some(expected);
2710            failure.actual = Some(actual);
2711            failure.transaction = Some(transaction_id.clone());
2712            failure.causal_position = db.sequence().ok();
2713            return Err(failure);
2714        }
2715    }
2716    let mut mutations = vec![];
2717    let mut preconditions = vec![];
2718    let mut overlay: BTreeMap<(String, String), Option<Value>> = BTreeMap::new();
2719    for operation in &request.operations {
2720        if !request.authorization.writable_collections.is_empty()
2721            && !request
2722                .authorization
2723                .writable_collections
2724                .contains(&operation.collection)
2725        {
2726            return Err(StateFailure::new(
2727                "AUTHORIZATION_DENIED",
2728                "collection is not writable",
2729            ));
2730        }
2731        // Substrate collections are validated by the substrate that owns them,
2732        // and written only under the dedicated capability.
2733        let substrate = is_substrate_collection(&operation.collection);
2734        if substrate && !request.authorization.can("composition:write") {
2735            let mut failure = StateFailure::new(
2736                "AUTHORIZATION_DENIED",
2737                "composition:write is required for substrate collections",
2738            );
2739            failure.resource = Some(operation.collection.clone());
2740            return Err(failure);
2741        }
2742        let definition = if substrate {
2743            None
2744        } else {
2745            Some(collection(schema, &operation.collection)?)
2746        };
2747        let capability = format!(
2748            "{}:{}",
2749            request
2750                .state_namespace
2751                .as_deref()
2752                .unwrap_or(&request.application_id),
2753            operation.collection
2754        );
2755        let key = operation.id.clone();
2756        let persisted = snapshot
2757            .iter()
2758            .find(|row| row.capability == capability && row.key == key && !row.deleted);
2759        let existing_value = overlay
2760            .get(&(capability.clone(), key.clone()))
2761            .cloned()
2762            .unwrap_or_else(|| persisted.map(|row| Some(row.value.clone())).unwrap_or(None));
2763
2764        // Per-operation record authorization
2765        let write_policy = collection_write_policies
2766            .and_then(|policies| policies.get(&operation.collection))
2767            .or(uniform_write_policy)
2768            .filter(|_| !substrate);
2769        if let Some(policy) = write_policy {
2770            let actor = if request.authorization.subject.is_empty()
2771                || request.authorization.subject == ":"
2772            {
2773                None
2774            } else {
2775                request
2776                    .authorization
2777                    .subject
2778                    .split(':')
2779                    .nth(1)
2780                    .map(|id| Actor::new(id))
2781            };
2782
2783            // Select authorization record based on operation type
2784            let auth_record_value = match operation.kind {
2785                TransactionOperationKind::Insert => {
2786                    // For create: authorize against proposed record
2787                    operation.value.clone()
2788                }
2789                TransactionOperationKind::Update | TransactionOperationKind::Delete => {
2790                    // For update/delete: authorize against EXISTING record (before changes)
2791                    // This prevents authorization bypass via ownership/org changes
2792                    existing_value
2793                        .clone()
2794                        .ok_or_else(|| StateFailure::new("CONFLICT", "record does not exist"))?
2795                }
2796                TransactionOperationKind::Put => {
2797                    // A replace is authorized against the record it replaces, like an
2798                    // update; a create against the proposed record, like an insert.
2799                    existing_value
2800                        .clone()
2801                        .unwrap_or_else(|| operation.value.clone())
2802                }
2803            };
2804
2805            // Create authorization state for policies that need to verify relationships
2806            let auth_state = AuthorizationState::new(
2807                Arc::new(schema.clone()),
2808                request
2809                    .state_namespace
2810                    .as_deref()
2811                    .unwrap_or(&request.application_id)
2812                    .to_string(),
2813                Arc::new(snapshot.clone()),
2814            );
2815
2816            let record_context = RecordAuthorizationContext {
2817                actor,
2818                collection: operation.collection.clone(),
2819                record_id: key.clone(),
2820                record_value: auth_record_value,
2821                authorization_state: Some(auth_state),
2822            };
2823
2824            PolicyEvaluator::evaluate_record(policy.clone(), &record_context).map_err(|_| {
2825                StateFailure::new(
2826                    "AUTHORIZATION_DENIED",
2827                    &format!(
2828                        "record authorization failed for operation on {}:{}",
2829                        operation.collection, key
2830                    ),
2831                )
2832            })?;
2833        }
2834
2835        let value = match operation.kind {
2836            TransactionOperationKind::Insert => {
2837                if existing_value.is_some() {
2838                    // A create is conditional on absence: the same structured
2839                    // refusal a `requireAbsent` create gets on every route.
2840                    let refusal = crate::PreconditionFailure::Present {
2841                        collection: capability.clone(),
2842                        key: key.clone(),
2843                    };
2844                    let mut failure =
2845                        StateFailure::new("PRECONDITION_FAILED", &refusal.to_string());
2846                    failure.transaction = Some(transaction_id.clone());
2847                    failure.resource = Some(format!("{}:{}", operation.collection, key));
2848                    failure.failure =
2849                        Some(refusal.public_json(&crate::ReadLayout::Namespace(namespace.clone())));
2850                    return Err(failure);
2851                }
2852                let mut value = operation.value.clone();
2853                match definition {
2854                    Some(definition) => validate_record(definition, &mut value)?,
2855                    None if value.is_object() => {}
2856                    None => {
2857                        return Err(StateFailure::new(
2858                            "VALIDATION_FAILED",
2859                            "record must be an object",
2860                        ));
2861                    }
2862                }
2863                Some(value)
2864            }
2865            TransactionOperationKind::Update => {
2866                let current = existing_value
2867                    .ok_or_else(|| StateFailure::new("CONFLICT", "record does not exist"))?;
2868                let mut value = current;
2869                let patch = operation.value.as_object().ok_or_else(|| {
2870                    StateFailure::new("VALIDATION_FAILED", "update must be an object")
2871                })?;
2872                for (k, v) in patch {
2873                    value
2874                        .as_object_mut()
2875                        .expect("existing object")
2876                        .insert(k.clone(), v.clone());
2877                }
2878                if let Some(definition) = definition {
2879                    validate_record(definition, &mut value)?;
2880                }
2881                Some(value)
2882            }
2883            TransactionOperationKind::Delete => {
2884                if existing_value.is_none() {
2885                    return Err(StateFailure::new("CONFLICT", "record does not exist"));
2886                }
2887                None
2888            }
2889            TransactionOperationKind::Put => {
2890                let mut value = operation.value.clone();
2891                match definition {
2892                    Some(definition) => validate_record(definition, &mut value)?,
2893                    None if value.is_object() => {}
2894                    None => {
2895                        return Err(StateFailure::new(
2896                            "VALIDATION_FAILED",
2897                            "record must be an object",
2898                        ));
2899                    }
2900                }
2901                Some(value)
2902            }
2903        };
2904        preconditions.push(AtomicPrecondition {
2905            capability: capability.clone(),
2906            key: key.clone(),
2907            expected_version: operation.if_version.or_else(|| {
2908                persisted
2909                    .and_then(|row| row.operation.as_ref())
2910                    .map(|op| op.sequence)
2911            }),
2912        });
2913        overlay.insert((capability.clone(), key.clone()), value.clone());
2914        mutations.push(AtomicMutation {
2915            capability,
2916            key,
2917            value,
2918        });
2919    }
2920    for definition in &schema.collections {
2921        let capability = format!(
2922            "{}:{}",
2923            request
2924                .state_namespace
2925                .as_deref()
2926                .unwrap_or(&request.application_id),
2927            definition.name
2928        );
2929        let mut referenced_records = BTreeMap::new();
2930        for ((cap, key), value) in overlay
2931            .iter()
2932            .filter(|((cap, _), value)| cap == &capability && value.is_some())
2933        {
2934            let value = value.as_ref().expect("filtered");
2935            for field in &definition.fields {
2936                if let FieldType::Reference { collection: target } = &field.field_type {
2937                    if let Some(reference) = value.get(&field.name).and_then(Value::as_str) {
2938                        let target_capability = format!(
2939                            "{}:{}",
2940                            request
2941                                .state_namespace
2942                                .as_deref()
2943                                .unwrap_or(&request.application_id),
2944                            target
2945                        );
2946                        let present = if let Some(value) =
2947                            overlay.get(&(target_capability.clone(), reference.into()))
2948                        {
2949                            value.is_some()
2950                        } else if snapshot.iter().any(|row| {
2951                            row.capability == target_capability
2952                                && row.key == reference
2953                                && !row.deleted
2954                        }) {
2955                            true
2956                        } else {
2957                            let cache_key = (target_capability.clone(), reference.to_string());
2958                            if let Some(present) = referenced_records.get(&cache_key) {
2959                                *present
2960                            } else {
2961                                let present = db
2962                                    .get_collection_record(&target_capability, reference)
2963                                    .map_err(StateFailure::storage)?
2964                                    .is_some();
2965                                referenced_records.insert(cache_key, present);
2966                                present
2967                            }
2968                        };
2969                        if !present {
2970                            let mut failure = StateFailure::new(
2971                                "VALIDATION_FAILED",
2972                                &format!(
2973                                    "reference {}.{} points to a missing record",
2974                                    definition.name, field.name
2975                                ),
2976                            );
2977                            failure.resource = Some(format!("{cap}:{key}"));
2978                            return Err(failure);
2979                        }
2980                    }
2981                }
2982            }
2983        }
2984        for index in definition.indexes.iter().filter(|index| index.unique) {
2985            let mut seen = BTreeSet::new();
2986            let existing = snapshot
2987                .iter()
2988                .filter(|row| row.capability == capability && !row.deleted)
2989                .filter(|row| !overlay.contains_key(&(capability.clone(), row.key.clone())))
2990                .map(|row| &row.value)
2991                .chain(
2992                    overlay
2993                        .iter()
2994                        .filter(|((cap, _), _)| cap == &capability)
2995                        .filter_map(|(_, value)| value.as_ref()),
2996                );
2997            for value in existing {
2998                let tuple = index
2999                    .fields
3000                    .iter()
3001                    .map(|field| value.get(field).cloned().unwrap_or(Value::Null))
3002                    .collect::<Vec<_>>();
3003                if index.sparse && tuple.iter().any(Value::is_null) {
3004                    continue;
3005                }
3006                let encoded = serde_json::to_string(&tuple).map_err(StateFailure::storage)?;
3007                if !seen.insert(encoded) {
3008                    return Err(StateFailure::new(
3009                        "CONFLICT",
3010                        &format!("unique index {} violated", index.name),
3011                    ));
3012                }
3013            }
3014        }
3015    }
3016    let commit = db
3017        .apply_atomic_transaction_fenced_in(
3018            &storage_transaction_id,
3019            Some(&payload_hash),
3020            request.causal_parent,
3021            &preconditions,
3022            &record_preconditions,
3023            &crate::ReadLayout::Namespace(namespace.clone()),
3024            &request.fences,
3025            &mutations,
3026            Some(serde_json::json!({"transaction_id":transaction_id.clone(),"subject":request.authorization.subject.clone(),"tenant":request.tenant_id.clone(),"application":request.application_id.clone(),"revision":request.revision_id.clone(),"schema_version":request.schema_version,"operations":request.operations.len(),"authorization_decisions":["state:write allowed"],"causal_parent":request.causal_parent,"result":"committed"})),
3027        )
3028        .map_err(|error| {
3029            // A record precondition that did not hold is a lost race, and it
3030            // says which predicate failed and what the authority holds. That
3031            // detail is the difference between "re-read and decide again" and
3032            // "this deployment is broken", so it is carried through as
3033            // structure rather than flattened into the message.
3034            if let FlowError::PreconditionFailed(refusal) = &error {
3035                let layout = crate::ReadLayout::Namespace(namespace.clone());
3036                let public = refusal.public_json(&layout);
3037                let mut failure =
3038                    StateFailure::new("PRECONDITION_FAILED", &error.to_string());
3039                failure.transaction = Some(transaction_id.clone());
3040                failure.causal_position = db.sequence().ok();
3041                failure.resource = public
3042                    .get("key")
3043                    .and_then(Value::as_str)
3044                    .map(str::to_owned)
3045                    .or_else(|| Some(refusal.key().to_string()));
3046                failure.failure = Some(public);
3047                if let PreconditionFailure::Version {
3048                    expected, actual, ..
3049                } = refusal.as_ref()
3050                {
3051                    failure.expected = Some(Value::from(*expected));
3052                    failure.actual = Some(Value::from(*actual));
3053                }
3054                return failure;
3055            }
3056            if let FlowError::RevisionConflict { expected, actual } = &error {
3057                let mut failure =
3058                    StateFailure::new("PRECONDITION_FAILED", &error.to_string());
3059                failure.transaction = Some(transaction_id.clone());
3060                failure.causal_position = db.sequence().ok();
3061                failure.resource = Some("authority_revision".into());
3062                failure.expected = Some(Value::from(*expected));
3063                failure.actual = Some(Value::from(*actual));
3064                return failure;
3065            }
3066            if let FlowError::TransactionIdPayloadMismatch { .. } = &error {
3067                // Two different payloads raced past the check above under one
3068                // id; the commit lock admitted the first. Same refusal.
3069                let mut failure = idempotency_conflict();
3070                failure.transaction = Some(transaction_id.clone());
3071                return failure;
3072            }
3073            if let FlowError::Io(_) | FlowError::Serde(_) = &error {
3074                // The commit could not be made durable. Never a lost race and
3075                // never a success: the outcome must be treated as unknown and
3076                // reconciled by retrying with the same transaction id.
3077                let mut failure = StateFailure::new("DURABILITY_FAILURE", &error.to_string());
3078                failure.transaction = Some(transaction_id.clone());
3079                return failure;
3080            }
3081            let text = error.to_string();
3082            if text.contains("INVALID_READ_SET:") {
3083                // A malformed or foreign basis is a caller error, never a lost race.
3084                let mut failure = StateFailure::new("VALIDATION_FAILED", &text);
3085                failure.transaction = Some(transaction_id.clone());
3086                return failure;
3087            }
3088            if text.contains("PRECONDITION_FAILED") {
3089                let mut failure = StateFailure::new("PRECONDITION_FAILED", &text);
3090                failure.transaction = Some(transaction_id.clone());
3091                failure.causal_position = db.sequence().ok();
3092                if let Some(condition) = preconditions.first() {
3093                    failure.resource = Some(format!("{}:{}", condition.capability, condition.key));
3094                    failure.expected = Some(
3095                        condition
3096                            .expected_version
3097                            .map(Value::from)
3098                            .unwrap_or(Value::Null),
3099                    );
3100                    failure.actual = Some(
3101                        snapshot
3102                            .iter()
3103                            .find(|row| {
3104                                row.capability == condition.capability && row.key == condition.key
3105                            })
3106                            .and_then(|row| row.operation.as_ref())
3107                            .map(|op| Value::from(op.sequence))
3108                            .unwrap_or(Value::Null),
3109                    );
3110                }
3111                failure
3112            } else {
3113                StateFailure::storage(error)
3114            }
3115        })?;
3116    let cursor = db
3117        .operation_versions()
3118        .map_err(StateFailure::storage)?
3119        .into_iter()
3120        .collect();
3121    let audit = TransactionAudit {
3122        transaction_id: transaction_id.clone(),
3123        subject: request.authorization.subject.clone(),
3124        tenant: request.tenant_id.clone(),
3125        application: request.application_id.clone(),
3126        revision: request.revision_id.clone(),
3127        schema_version: request.schema_version,
3128        state_before: commit.state_before,
3129        state_after: commit.state_after,
3130        operations: request.operations.len(),
3131        authorization_decisions: vec!["state:write allowed".into()],
3132        causal_parent: request.causal_parent,
3133        result: if commit.duplicate {
3134            "duplicate"
3135        } else {
3136            "committed"
3137        }
3138        .into(),
3139    };
3140    Ok(TransactionResult {
3141        transaction_id,
3142        base_revision: commit.base_revision,
3143        commit_revision: commit.commit_revision,
3144        status: commit.status,
3145        state_before: commit.state_before,
3146        state_after: commit.state_after,
3147        causal_cursor: cursor,
3148        duplicate: commit.duplicate,
3149        audit,
3150    })
3151}
3152
3153#[cfg(test)]
3154mod tests {
3155    use super::*;
3156    #[test]
3157    fn extended_field_types_have_canonical_validation() {
3158        let valid = [
3159            (
3160                "uuid",
3161                serde_json::json!("550e8400-e29b-41d4-a716-446655440000"),
3162            ),
3163            ("decimal", serde_json::json!("-1234567890.0123456789")),
3164            ("bigint", serde_json::json!("9223372036854775808123")),
3165            ("date", serde_json::json!("2026-08-21")),
3166            ("time", serde_json::json!("13:45:30")),
3167            ("datetime", serde_json::json!("2026-08-21T13:45:30Z")),
3168            ("email", serde_json::json!("person@example.com")),
3169            ("url", serde_json::json!("https://example.com/path")),
3170            ("phone", serde_json::json!("+1 (212) 555-0100")),
3171            ("binary", serde_json::json!("SGVsbG8=")),
3172            (
3173                "money",
3174                serde_json::json!({"amount":"19.99","currency":"USD"}),
3175            ),
3176            (
3177                "file",
3178                serde_json::json!({"name":"report.pdf","type":"application/pdf","size":42,"ref":"sha256:abc"}),
3179            ),
3180            (
3181                "geo",
3182                serde_json::json!({"latitude":40.7,"longitude":-74.0}),
3183            ),
3184            ("object", serde_json::json!({"enabled":true})),
3185            (
3186                "array<uuid>",
3187                serde_json::json!(["550e8400-e29b-41d4-a716-446655440000"]),
3188            ),
3189            ("map<integer>", serde_json::json!({"one":1,"two":2})),
3190            ("vector<3>", serde_json::json!([0.1, 0.2, 0.3])),
3191        ];
3192        for (kind, value) in valid {
3193            assert!(
3194                valid_nested_type(&value, &parse_manifest_type(kind)),
3195                "{kind} rejected {value}"
3196            );
3197        }
3198        assert!(!valid_nested_type(
3199            &serde_json::json!("12.3x"),
3200            &parse_manifest_type("decimal")
3201        ));
3202        assert!(!valid_nested_type(
3203            &serde_json::json!([1, 2]),
3204            &parse_manifest_type("vector<3>")
3205        ));
3206        assert!(!valid_nested_type(
3207            &serde_json::json!({"latitude":100,"longitude":0}),
3208            &parse_manifest_type("geo")
3209        ));
3210    }
3211    fn schema() -> StateSchema {
3212        StateSchema {
3213            contract_version: 1,
3214            schema_version: 1,
3215            application_id: "app".into(),
3216            revision_id: "rev".into(),
3217            collections: vec![CollectionSchema {
3218                name: "incidents".into(),
3219                version: 1,
3220                fields: vec![
3221                    FieldSchema {
3222                        name: "title".into(),
3223                        field_type: FieldType::Primitive {
3224                            primitive: PrimitiveType::String,
3225                        },
3226                        nullable: false,
3227                        required: true,
3228                        default: None,
3229                        constraints: Default::default(),
3230                        computed: None,
3231                    },
3232                    FieldSchema {
3233                        name: "status".into(),
3234                        field_type: FieldType::Enum {
3235                            values: vec!["open".into(), "closed".into()],
3236                        },
3237                        nullable: false,
3238                        required: false,
3239                        default: Some(Value::String("open".into())),
3240                        constraints: Default::default(),
3241                        computed: None,
3242                    },
3243                ],
3244                indexes: vec![SchemaIndex {
3245                    name: "status".into(),
3246                    fields: vec!["status".into()],
3247                    unique: false,
3248                    sparse: false,
3249                    version: 1,
3250                }],
3251            }],
3252        }
3253    }
3254    fn scoped_schema() -> StateSchema {
3255        let mut schema = schema();
3256        schema.collections[0].fields[1].field_type = FieldType::Primitive {
3257            primitive: PrimitiveType::String,
3258        };
3259        schema
3260    }
3261    fn auth() -> AuthorizationContext {
3262        AuthorizationContext {
3263            subject: "user:1".into(),
3264            tenant_id: "tenant".into(),
3265            application_id: "app".into(),
3266            revision_id: "rev".into(),
3267            capabilities: ["state:read".into(), "state:write".into()].into(),
3268            readable_collections: Default::default(),
3269            writable_collections: Default::default(),
3270            field_projections: Default::default(),
3271        }
3272    }
3273    fn schema_with_owner() -> StateSchema {
3274        StateSchema {
3275            contract_version: 1,
3276            schema_version: 1,
3277            application_id: "app".into(),
3278            revision_id: "rev".into(),
3279            collections: vec![CollectionSchema {
3280                name: "incidents".into(),
3281                version: 1,
3282                fields: vec![
3283                    FieldSchema {
3284                        name: "title".into(),
3285                        field_type: FieldType::Primitive {
3286                            primitive: PrimitiveType::String,
3287                        },
3288                        nullable: false,
3289                        required: true,
3290                        default: None,
3291                        constraints: Default::default(),
3292                        computed: None,
3293                    },
3294                    FieldSchema {
3295                        name: "owner_id".into(),
3296                        field_type: FieldType::Primitive {
3297                            primitive: PrimitiveType::String,
3298                        },
3299                        nullable: false,
3300                        required: false,
3301                        default: None,
3302                        constraints: Default::default(),
3303                        computed: None,
3304                    },
3305                ],
3306                indexes: vec![],
3307            }],
3308        }
3309    }
3310
3311    fn db(name: &str) -> FeltDb {
3312        FeltDb::open(
3313            std::env::temp_dir().join(format!("state-contract-{name}-{}.log", crate::now_ms())),
3314        )
3315        .unwrap()
3316    }
3317    #[test]
3318    fn schema_hash_is_deterministic_and_invalid_schema_fails() {
3319        let a = schema();
3320        let mut b = a.clone();
3321        b.collections[0].fields.reverse();
3322        assert_eq!(schema_hash(&a).unwrap(), schema_hash(&b).unwrap());
3323        let mut invalid = a;
3324        invalid.collections[0].fields[0].required = true;
3325        invalid.collections[0].fields[0].nullable = true;
3326        assert!(!validate_schema(&invalid).valid);
3327    }
3328    #[test]
3329    fn compatibility_classifies_required_and_type_changes() {
3330        let a = schema();
3331        let mut b = a.clone();
3332        b.schema_version = 2;
3333        b.collections[0].fields.push(FieldSchema {
3334            name: "owner".into(),
3335            field_type: FieldType::Primitive {
3336                primitive: PrimitiveType::String,
3337            },
3338            nullable: false,
3339            required: true,
3340            default: None,
3341            constraints: Default::default(),
3342            computed: None,
3343        });
3344        assert_eq!(
3345            compare_schemas(&a, &b).classification,
3346            SchemaCompatibility::Destructive
3347        );
3348        b.collections[0].fields[0].field_type = FieldType::Primitive {
3349            primitive: PrimitiveType::Integer,
3350        };
3351        assert_eq!(
3352            compare_schemas(&a, &b).classification,
3353            SchemaCompatibility::Incompatible
3354        );
3355    }
3356    #[test]
3357    fn transaction_is_atomic_and_preconditions_are_structured() {
3358        let db = db("tx");
3359        let schema = schema();
3360        let request = TransactionRequest {
3361            transaction_id: None,
3362            tenant_id: "tenant".into(),
3363            application_id: "app".into(),
3364            revision_id: "rev".into(),
3365            schema_version: 1,
3366            state_namespace: None,
3367            causal_parent: Some(0),
3368            authorization: auth(),
3369            preconditions: vec![],
3370            operations: vec![
3371                TransactionOperation {
3372                    kind: TransactionOperationKind::Insert,
3373                    collection: "incidents".into(),
3374                    id: "one".into(),
3375                    value: serde_json::json!({"title":"First"}),
3376                    if_version: None,
3377                },
3378                TransactionOperation {
3379                    kind: TransactionOperationKind::Insert,
3380                    collection: "incidents".into(),
3381                    id: "two".into(),
3382                    value: serde_json::json!({"title":"Second"}),
3383                    if_version: None,
3384                },
3385            ],
3386            fences: vec![],
3387        };
3388        let result = execute_transaction(&db, &schema, &request, None).unwrap();
3389        assert_eq!(result.state_after, 2);
3390        assert_eq!(db.export_snapshot().unwrap().rows.len(), 2);
3391        let mut stale = request;
3392        stale.transaction_id = Some("stale".into());
3393        stale.causal_parent = Some(0);
3394        // A create of a record that exists is refused as a structured presence
3395        // precondition, as a `requireAbsent` create is on every other route.
3396        let refused = execute_transaction(&db, &schema, &stale, None).unwrap_err();
3397        assert_eq!(refused.code, "PRECONDITION_FAILED");
3398        let failure = refused.failure.expect("structured failure");
3399        assert_eq!(failure["predicate"], "present");
3400        assert_eq!(failure["collection"], "incidents");
3401        assert_eq!(failure["id"], "one");
3402        assert_eq!(failure["key"], "incidents:one");
3403        assert_eq!(
3404            db.export_snapshot().unwrap().rows.len(),
3405            2,
3406            "nothing written"
3407        );
3408    }
3409    /// A conditional write through the canonical contract has to mean the same
3410    /// thing it means everywhere else in FeltDB, or `putIfAbsent` and
3411    /// `updateIfVersion` become file-runtime privileges.
3412    ///
3413    /// The predicate under test is the document's `__version` -- the number a
3414    /// caller reads back -- not the row's internal storage sequence. Those are
3415    /// two different numbers that happen to share a name, and a fence on the
3416    /// wrong one is worse than no fence, because it reads as protection.
3417    #[test]
3418    fn a_record_precondition_fences_the_document_version() {
3419        let db = db("record-precondition");
3420        let schema = schema();
3421        let write = |id: &str,
3422                     transaction: &str,
3423                     title: &str,
3424                     preconditions: Vec<TransactionPrecondition>,
3425                     kind: TransactionOperationKind| {
3426            execute_transaction(
3427                &db,
3428                &schema,
3429                &TransactionRequest {
3430                    transaction_id: Some(transaction.into()),
3431                    tenant_id: "tenant".into(),
3432                    application_id: "app".into(),
3433                    revision_id: "rev".into(),
3434                    schema_version: 1,
3435                    state_namespace: None,
3436                    causal_parent: None,
3437                    authorization: auth(),
3438                    preconditions,
3439                    operations: vec![TransactionOperation {
3440                        kind,
3441                        collection: "incidents".into(),
3442                        id: id.into(),
3443                        value: serde_json::json!({ "title": title }),
3444                        if_version: None,
3445                    }],
3446                    fences: vec![],
3447                },
3448                None,
3449            )
3450        };
3451        let fence = |version: Option<u64>, absent: bool| {
3452            vec![TransactionPrecondition::Record {
3453                collection: "incidents".into(),
3454                id: "i1".into(),
3455                require_absent: absent,
3456                expected_version: version,
3457                expected_epoch: None,
3458                expected_lease_id: None,
3459            }]
3460        };
3461        let version = || {
3462            db.state_rows()
3463                .unwrap()
3464                .into_iter()
3465                .find(|row| row.capability == "app:incidents" && row.key == "i1" && !row.deleted)
3466                .and_then(|row| row.value.get("__version").and_then(Value::as_u64))
3467        };
3468
3469        // A conditional create is a create at version 1.
3470        write(
3471            "i1",
3472            "create",
3473            "First",
3474            fence(None, true),
3475            TransactionOperationKind::Insert,
3476        )
3477        .expect("the record is absent, so the create commits");
3478        assert_eq!(version(), Some(1));
3479
3480        // The same create again is a lost race. Whichever check catches it,
3481        // the answer is a conflict and nothing was written.
3482        let refused = write(
3483            "i1",
3484            "create-again",
3485            "Second",
3486            fence(None, true),
3487            TransactionOperationKind::Insert,
3488        )
3489        .expect_err("the record now exists");
3490        assert!(
3491            refused.code == "CONFLICT" || refused.code == "PRECONDITION_FAILED",
3492            "a lost creation race is a conflict, got {}",
3493            refused.code,
3494        );
3495        assert_eq!(version(), Some(1), "a refused transaction writes nothing");
3496
3497        // A guard on a record the transaction does not write is evaluated
3498        // inside the commit lock, and says which predicate failed.
3499        let guarded = write(
3500            "i2",
3501            "guarded",
3502            "Other",
3503            fence(None, true),
3504            TransactionOperationKind::Insert,
3505        )
3506        .expect_err("i1 exists, so the guard fails and i2 is never written");
3507        assert_eq!(guarded.code, "PRECONDITION_FAILED");
3508        assert_eq!(
3509            guarded
3510                .failure
3511                .as_ref()
3512                .and_then(|value| value.get("predicate").and_then(Value::as_str)),
3513            Some("present"),
3514        );
3515        assert!(
3516            db.state_rows()
3517                .unwrap()
3518                .iter()
3519                .all(|row| row.key != "i2" || row.deleted),
3520            "a refused guard leaves the staged write unwritten",
3521        );
3522
3523        // A stale fence conflicts and names what the authority actually holds.
3524        let stale = write(
3525            "i1",
3526            "stale",
3527            "Third",
3528            fence(Some(7), false),
3529            TransactionOperationKind::Update,
3530        )
3531        .expect_err("version 7 is not the version on record");
3532        assert_eq!(stale.code, "PRECONDITION_FAILED");
3533        assert_eq!(stale.expected, Some(Value::from(7)));
3534        assert_eq!(stale.actual, Some(Value::from(1)));
3535        assert_eq!(version(), Some(1));
3536
3537        // The fenced write commits and the authority owns the next version.
3538        write(
3539            "i1",
3540            "fenced",
3541            "Fourth",
3542            fence(Some(1), false),
3543            TransactionOperationKind::Update,
3544        )
3545        .expect("version 1 is the version on record");
3546        assert_eq!(version(), Some(2));
3547
3548        // Replaying a committed transaction id applies nothing, so it cannot
3549        // advance the version a second time.
3550        write(
3551            "i1",
3552            "fenced",
3553            "Fourth",
3554            fence(Some(1), false),
3555            TransactionOperationKind::Update,
3556        )
3557        .expect("a duplicate transaction id is not a failure");
3558        assert_eq!(version(), Some(2));
3559    }
3560
3561    /// A precondition that constrains nothing is refused rather than accepted,
3562    /// because one that is silently ignored reads as protection and provides
3563    /// none.
3564    #[test]
3565    fn an_empty_record_precondition_is_refused() {
3566        let db = db("empty-precondition");
3567        let schema = schema();
3568        let failure = execute_transaction(
3569            &db,
3570            &schema,
3571            &TransactionRequest {
3572                transaction_id: Some("empty".into()),
3573                tenant_id: "tenant".into(),
3574                application_id: "app".into(),
3575                revision_id: "rev".into(),
3576                schema_version: 1,
3577                state_namespace: None,
3578                causal_parent: None,
3579                authorization: auth(),
3580                preconditions: vec![TransactionPrecondition::Record {
3581                    collection: "incidents".into(),
3582                    id: "i1".into(),
3583                    require_absent: false,
3584                    expected_version: None,
3585                    expected_epoch: None,
3586                    expected_lease_id: None,
3587                }],
3588                operations: vec![TransactionOperation {
3589                    kind: TransactionOperationKind::Insert,
3590                    collection: "incidents".into(),
3591                    id: "i1".into(),
3592                    value: serde_json::json!({ "title": "First" }),
3593                    if_version: None,
3594                }],
3595                fences: vec![],
3596            },
3597            None,
3598        )
3599        .expect_err("a precondition that constrains nothing is a caller error");
3600        assert_eq!(failure.code, "VALIDATION_FAILED");
3601        assert!(db.state_rows().unwrap().iter().all(|row| row.deleted));
3602    }
3603
3604    #[test]
3605    fn validation_failure_rolls_back_every_operation() {
3606        let db = db("rollback");
3607        let schema = schema();
3608        let request = TransactionRequest {
3609            transaction_id: None,
3610            tenant_id: "tenant".into(),
3611            application_id: "app".into(),
3612            revision_id: "rev".into(),
3613            schema_version: 1,
3614            state_namespace: None,
3615            causal_parent: None,
3616            authorization: auth(),
3617            preconditions: vec![],
3618            operations: vec![
3619                TransactionOperation {
3620                    kind: TransactionOperationKind::Insert,
3621                    collection: "incidents".into(),
3622                    id: "one".into(),
3623                    value: serde_json::json!({"title":"ok"}),
3624                    if_version: None,
3625                },
3626                TransactionOperation {
3627                    kind: TransactionOperationKind::Insert,
3628                    collection: "incidents".into(),
3629                    id: "two".into(),
3630                    value: serde_json::json!({"status":"open"}),
3631                    if_version: None,
3632                },
3633            ],
3634            fences: vec![],
3635        };
3636        assert_eq!(
3637            execute_transaction(&db, &schema, &request, None)
3638                .unwrap_err()
3639                .code,
3640            "VALIDATION_FAILED"
3641        );
3642        assert!(db.export_snapshot().unwrap().rows.is_empty());
3643    }
3644    #[test]
3645    fn query_filters_orders_projects_and_aggregates() {
3646        let db = db("query");
3647        let schema = schema();
3648        for (id, title, status) in [("1", "B", "open"), ("2", "A", "open"), ("3", "C", "closed")] {
3649            execute_transaction(
3650                &db,
3651                &schema,
3652                &TransactionRequest {
3653                    transaction_id: None,
3654                    tenant_id: "tenant".into(),
3655                    application_id: "app".into(),
3656                    revision_id: "rev".into(),
3657                    schema_version: 1,
3658                    state_namespace: None,
3659                    causal_parent: None,
3660                    authorization: auth(),
3661                    preconditions: vec![],
3662                    operations: vec![TransactionOperation {
3663                        kind: TransactionOperationKind::Insert,
3664                        collection: "incidents".into(),
3665                        id: id.into(),
3666                        value: serde_json::json!({"title":title,"status":status}),
3667                        if_version: None,
3668                    }],
3669                    fences: vec![],
3670                },
3671                None,
3672            )
3673            .unwrap();
3674        }
3675        let context = begin_read(&db, &schema, "app", auth()).unwrap();
3676        let result = execute_query(
3677            &db,
3678            &schema,
3679            &context,
3680            &CanonicalQuery {
3681                collection: "incidents".into(),
3682                filter: Some(QueryFilter::Eq {
3683                    field: "status".into(),
3684                    value: Value::String("open".into()),
3685                }),
3686                order_by: vec![QueryOrder {
3687                    field: "title".into(),
3688                    direction: SortDirection::Asc,
3689                }],
3690                limit: Some(1),
3691                offset: 0,
3692                cursor: None,
3693                projection: vec!["title".into()],
3694                group_by: vec!["status".into()],
3695                aggregates: vec![AggregateDefinition {
3696                    name: "total".into(),
3697                    function: AggregateFunction::Count,
3698                    field: None,
3699                }],
3700                references: vec![],
3701                semantic: None,
3702            },
3703            None,
3704        )
3705        .unwrap();
3706        assert_eq!(result.records, vec![serde_json::json!({"title":"A"})]);
3707        assert_eq!(result.aggregates[0]["total"], 2);
3708        assert_eq!(result.state_version, 3);
3709    }
3710    #[test]
3711    fn authorization_prevents_projection_before_read() {
3712        let db = db("auth");
3713        let schema = schema();
3714        let mut auth = auth();
3715        auth.field_projections
3716            .insert("incidents".into(), ["status".into()].into());
3717        let context = begin_read(&db, &schema, "app", auth).unwrap();
3718        let error = execute_query(
3719            &db,
3720            &schema,
3721            &context,
3722            &CanonicalQuery {
3723                collection: "incidents".into(),
3724                filter: None,
3725                order_by: vec![],
3726                limit: None,
3727                offset: 0,
3728                cursor: None,
3729                projection: vec!["title".into()],
3730                group_by: vec![],
3731                aggregates: vec![],
3732                references: vec![],
3733                semantic: None,
3734            },
3735            None,
3736        )
3737        .unwrap_err();
3738        assert_eq!(error.code, "UNAUTHORIZED_PROJECTION");
3739    }
3740    #[test]
3741    fn stale_record_precondition_is_structured_and_identity_is_idempotent() {
3742        let db = db("precondition");
3743        let schema = schema();
3744        let mut request = TransactionRequest {
3745            transaction_id: None,
3746            tenant_id: "tenant".into(),
3747            application_id: "app".into(),
3748            revision_id: "rev".into(),
3749            schema_version: 1,
3750            state_namespace: None,
3751            causal_parent: None,
3752            authorization: auth(),
3753            preconditions: vec![],
3754            operations: vec![TransactionOperation {
3755                kind: TransactionOperationKind::Insert,
3756                collection: "incidents".into(),
3757                id: "one".into(),
3758                value: serde_json::json!({"title":"First"}),
3759                if_version: None,
3760            }],
3761            fences: vec![],
3762        };
3763        let first = execute_transaction(&db, &schema, &request, None).unwrap();
3764        let duplicate = execute_transaction(&db, &schema, &request, None).unwrap();
3765        assert_eq!(first.transaction_id, duplicate.transaction_id);
3766        assert!(duplicate.duplicate);
3767        request.transaction_id = Some("stale-update".into());
3768        request.operations = vec![TransactionOperation {
3769            kind: TransactionOperationKind::Update,
3770            collection: "incidents".into(),
3771            id: "one".into(),
3772            value: serde_json::json!({"title":"Changed"}),
3773            if_version: Some(99),
3774        }];
3775        let failure = execute_transaction(&db, &schema, &request, None).unwrap_err();
3776        assert_eq!(failure.code, "PRECONDITION_FAILED");
3777        assert_eq!(failure.expected, Some(Value::from(99)));
3778        assert_eq!(failure.actual, Some(Value::from(1)));
3779    }
3780    #[test]
3781    fn stale_authority_revision_is_a_structured_precondition_failure() {
3782        let db = db("authority-revision-precondition");
3783        let schema = schema();
3784        let mut request = TransactionRequest {
3785            transaction_id: Some("initial".into()),
3786            tenant_id: "tenant".into(),
3787            application_id: "app".into(),
3788            revision_id: "rev".into(),
3789            schema_version: 1,
3790            state_namespace: None,
3791            causal_parent: None,
3792            authorization: auth(),
3793            preconditions: vec![],
3794            operations: vec![TransactionOperation {
3795                kind: TransactionOperationKind::Insert,
3796                collection: "incidents".into(),
3797                id: "one".into(),
3798                value: serde_json::json!({"title":"First"}),
3799                if_version: None,
3800            }],
3801            fences: vec![],
3802        };
3803        execute_transaction(&db, &schema, &request, None).unwrap();
3804        request.transaction_id = Some("stale-parent".into());
3805        request.causal_parent = Some(0);
3806        request.operations[0].id = "two".into();
3807
3808        let failure = execute_transaction(&db, &schema, &request, None).unwrap_err();
3809        assert_eq!(failure.code, "PRECONDITION_FAILED");
3810        assert_eq!(failure.resource.as_deref(), Some("authority_revision"));
3811        assert_eq!(failure.expected, Some(Value::from(0)));
3812        assert_eq!(failure.actual, Some(Value::from(1)));
3813    }
3814    #[test]
3815    fn explicit_transaction_id_is_bound_to_canonical_payload_across_restart() {
3816        let path = std::env::temp_dir().join(format!(
3817            "feltdb-content-id-{}-{}.log",
3818            std::process::id(),
3819            crate::now_ms()
3820        ));
3821        let schema = schema();
3822        let request = TransactionRequest {
3823            transaction_id: Some("stable-client-id".into()),
3824            tenant_id: "tenant".into(),
3825            application_id: "app".into(),
3826            revision_id: "rev".into(),
3827            schema_version: 1,
3828            state_namespace: None,
3829            causal_parent: None,
3830            authorization: auth(),
3831            preconditions: vec![],
3832            operations: vec![TransactionOperation {
3833                kind: TransactionOperationKind::Insert,
3834                collection: "incidents".into(),
3835                id: "content-bound".into(),
3836                value: serde_json::json!({"title":"Original"}),
3837                if_version: None,
3838            }],
3839            fences: vec![],
3840        };
3841        {
3842            let db = crate::open(&path).unwrap();
3843            assert!(
3844                !execute_transaction(&db, &schema, &request, None)
3845                    .unwrap()
3846                    .duplicate
3847            );
3848            assert!(
3849                execute_transaction(&db, &schema, &request, None)
3850                    .unwrap()
3851                    .duplicate
3852            );
3853        }
3854        let reopened = crate::open(&path).unwrap();
3855        assert!(
3856            execute_transaction(&reopened, &schema, &request, None)
3857                .unwrap()
3858                .duplicate
3859        );
3860        let mut changed = request.clone();
3861        changed.operations[0].value = serde_json::json!({"title":"Changed"});
3862        let failure = execute_transaction(&reopened, &schema, &changed, None).unwrap_err();
3863        assert_eq!(failure.code, "IDEMPOTENCY_CONFLICT");
3864        assert!(failure.message.contains("different canonical payload"));
3865        std::fs::remove_file(path).ok();
3866    }
3867    #[test]
3868    fn explicit_transaction_id_is_isolated_between_applications() {
3869        fn application(
3870            app: &str,
3871            namespace: &str,
3872            transaction: &str,
3873            record: &str,
3874        ) -> (StateSchema, TransactionRequest) {
3875            let mut schema = schema();
3876            schema.application_id = app.into();
3877            let mut authorization = auth();
3878            authorization.application_id = app.into();
3879            let request = TransactionRequest {
3880                transaction_id: Some(transaction.into()),
3881                tenant_id: "tenant".into(),
3882                application_id: app.into(),
3883                revision_id: "rev".into(),
3884                schema_version: 1,
3885                state_namespace: Some(namespace.into()),
3886                causal_parent: None,
3887                authorization,
3888                preconditions: vec![],
3889                operations: vec![TransactionOperation {
3890                    kind: TransactionOperationKind::Insert,
3891                    collection: "incidents".into(),
3892                    id: record.into(),
3893                    value: serde_json::json!({"title":format!("{app}-{record}")}),
3894                    if_version: None,
3895                }],
3896                fences: vec![],
3897            };
3898            (schema, request)
3899        }
3900        for (name, order) in [
3901            (
3902                "a-then-b",
3903                vec![("app-a", "shared-1", "one"), ("app-b", "shared-1", "one")],
3904            ),
3905            (
3906                "b-then-a",
3907                vec![("app-b", "shared-1", "one"), ("app-a", "shared-1", "one")],
3908            ),
3909            (
3910                "interleaved",
3911                vec![
3912                    ("app-a", "shared-1", "one"),
3913                    ("app-b", "shared-1", "one"),
3914                    ("app-a", "shared-2", "two"),
3915                    ("app-b", "shared-2", "two"),
3916                ],
3917            ),
3918        ] {
3919            let path = std::env::temp_dir().join(format!(
3920                "feltdb-app-isolation-{name}-{}-{}.log",
3921                std::process::id(),
3922                crate::now_ms()
3923            ));
3924            {
3925                let db = crate::open(&path).unwrap();
3926                for (app, transaction, record) in &order {
3927                    let (schema, request) =
3928                        application(app, &format!("{app}:production"), transaction, record);
3929                    assert!(
3930                        !execute_transaction(&db, &schema, &request, None)
3931                            .unwrap()
3932                            .duplicate
3933                    );
3934                }
3935            }
3936            let reopened = crate::open(&path).unwrap();
3937            for (app, transaction, record) in &order {
3938                let (schema, request) =
3939                    application(app, &format!("{app}:production"), transaction, record);
3940                assert!(
3941                    execute_transaction(&reopened, &schema, &request, None)
3942                        .unwrap()
3943                        .duplicate
3944                );
3945                let rows = reopened.state_rows().unwrap();
3946                assert_eq!(
3947                    rows.iter()
3948                        .filter(
3949                            |row| row.capability == format!("{app}:production:incidents")
3950                                && row.key == *record
3951                                && !row.deleted
3952                        )
3953                        .count(),
3954                    1
3955                );
3956            }
3957            std::fs::remove_file(path).ok();
3958        }
3959    }
3960    fn put_request(id: &str, value: Value, if_version: Option<u64>) -> TransactionRequest {
3961        TransactionRequest {
3962            transaction_id: None,
3963            tenant_id: "tenant".into(),
3964            application_id: "app".into(),
3965            revision_id: "rev".into(),
3966            schema_version: 1,
3967            state_namespace: None,
3968            causal_parent: None,
3969            authorization: auth(),
3970            preconditions: vec![],
3971            operations: vec![TransactionOperation {
3972                kind: TransactionOperationKind::Put,
3973                collection: "incidents".into(),
3974                id: id.into(),
3975                value,
3976                if_version,
3977            }],
3978            fences: vec![],
3979        }
3980    }
3981    /// The one record these tests write (records carry no id of their own).
3982    fn incident(db: &FeltDb, schema: &StateSchema, _id: &str) -> Option<Value> {
3983        let context = begin_read(db, schema, "app", auth()).unwrap();
3984        let query = CanonicalQuery {
3985            collection: "incidents".into(),
3986            filter: None,
3987            order_by: vec![],
3988            limit: None,
3989            offset: 0,
3990            cursor: None,
3991            projection: vec![],
3992            group_by: vec![],
3993            aggregates: vec![],
3994            references: vec![],
3995            semantic: None,
3996        };
3997        let records = execute_query(db, schema, &context, &query, None)
3998            .unwrap()
3999            .records;
4000        assert!(records.len() <= 1, "one record at most: {records:?}");
4001        records.into_iter().next()
4002    }
4003    #[test]
4004    fn put_creates_an_absent_record_on_the_first_write() {
4005        let db = db("put-first-write");
4006        let schema = schema();
4007        execute_transaction(
4008            &db,
4009            &schema,
4010            &put_request("one", serde_json::json!({"title":"One"}), None),
4011            None,
4012        )
4013        .expect("an unconditional write of an absent record creates it");
4014        let stored = incident(&db, &schema, "one").expect("created");
4015        assert_eq!(stored["title"], "One");
4016    }
4017    #[test]
4018    fn put_replaces_the_record_instead_of_merging_into_it() {
4019        let db = db("put-replace");
4020        let schema = schema();
4021        execute_transaction(
4022            &db,
4023            &schema,
4024            &put_request(
4025                "one",
4026                serde_json::json!({"title":"One","status":"closed"}),
4027                None,
4028            ),
4029            None,
4030        )
4031        .unwrap();
4032        execute_transaction(
4033            &db,
4034            &schema,
4035            &put_request("one", serde_json::json!({"title":"Again"}), None),
4036            None,
4037        )
4038        .unwrap();
4039        let stored = incident(&db, &schema, "one").unwrap();
4040        assert_eq!(stored["title"], "Again");
4041        // A merge would have kept "closed"; a replace validates the new record alone,
4042        // so the field it omits takes the contract's default.
4043        assert_eq!(stored["status"], "open");
4044    }
4045    #[test]
4046    fn put_validates_the_record_it_writes() {
4047        let db = db("put-validates");
4048        let schema = schema();
4049        let refused = execute_transaction(
4050            &db,
4051            &schema,
4052            &put_request("one", serde_json::json!({"status":"open"}), None),
4053            None,
4054        )
4055        .expect_err("a record without its required field is refused");
4056        assert_eq!(refused.code, "VALIDATION_FAILED");
4057        assert!(incident(&db, &schema, "one").is_none());
4058    }
4059
4060    #[test]
4061    fn read_context_remains_a_stable_snapshot() {
4062        let db = db("snapshot-read");
4063        let schema = schema();
4064        let insert = |id: &str| TransactionRequest {
4065            transaction_id: None,
4066            tenant_id: "tenant".into(),
4067            application_id: "app".into(),
4068            revision_id: "rev".into(),
4069            schema_version: 1,
4070            state_namespace: None,
4071            causal_parent: None,
4072            authorization: auth(),
4073            preconditions: vec![],
4074            operations: vec![TransactionOperation {
4075                kind: TransactionOperationKind::Insert,
4076                collection: "incidents".into(),
4077                id: id.into(),
4078                value: serde_json::json!({"title":id}),
4079                if_version: None,
4080            }],
4081            fences: vec![],
4082        };
4083        execute_transaction(&db, &schema, &insert("one"), None).unwrap();
4084        let context = begin_read(&db, &schema, "app", auth()).unwrap();
4085        execute_transaction(&db, &schema, &insert("two"), None).unwrap();
4086        let result = execute_query(
4087            &db,
4088            &schema,
4089            &context,
4090            &CanonicalQuery {
4091                collection: "incidents".into(),
4092                filter: None,
4093                order_by: vec![],
4094                limit: None,
4095                offset: 0,
4096                cursor: None,
4097                projection: vec![],
4098                group_by: vec![],
4099                aggregates: vec![],
4100                references: vec![],
4101                semantic: None,
4102            },
4103            None,
4104        )
4105        .unwrap();
4106        assert_eq!(result.records.len(), 1);
4107        assert_eq!(result.state_version, 1);
4108    }
4109    #[test]
4110    fn transaction_log_envelope_replays_all_rows() {
4111        let path =
4112            std::env::temp_dir().join(format!("state-contract-replay-{}.log", crate::now_ms()));
4113        let schema = schema();
4114        {
4115            let db = FeltDb::open(&path).unwrap();
4116            let request = TransactionRequest {
4117                transaction_id: Some("replay-tx".into()),
4118                tenant_id: "tenant".into(),
4119                application_id: "app".into(),
4120                revision_id: "rev".into(),
4121                schema_version: 1,
4122                state_namespace: None,
4123                causal_parent: None,
4124                authorization: auth(),
4125                preconditions: vec![],
4126                operations: vec![
4127                    TransactionOperation {
4128                        kind: TransactionOperationKind::Insert,
4129                        collection: "incidents".into(),
4130                        id: "one".into(),
4131                        value: serde_json::json!({"title":"One"}),
4132                        if_version: None,
4133                    },
4134                    TransactionOperation {
4135                        kind: TransactionOperationKind::Insert,
4136                        collection: "incidents".into(),
4137                        id: "two".into(),
4138                        value: serde_json::json!({"title":"Two"}),
4139                        if_version: None,
4140                    },
4141                ],
4142                fences: vec![],
4143            };
4144            execute_transaction(&db, &schema, &request, None).unwrap();
4145        }
4146        let reopened = FeltDb::open(path).unwrap();
4147        assert_eq!(reopened.export_snapshot().unwrap().rows.len(), 2);
4148        assert_eq!(reopened.current_revision().unwrap(), 1);
4149    }
4150    #[test]
4151    fn indexed_equality_query_uses_index_not_scan() {
4152        let db = db("regression-indexed-equality");
4153        let schema = schema();
4154        let request = TransactionRequest {
4155            transaction_id: None,
4156            tenant_id: "tenant".into(),
4157            application_id: "app".into(),
4158            revision_id: "rev".into(),
4159            schema_version: 1,
4160            state_namespace: None,
4161            causal_parent: None,
4162            authorization: auth(),
4163            preconditions: vec![],
4164            operations: (1..=100)
4165                .map(|i| TransactionOperation {
4166                    kind: TransactionOperationKind::Insert,
4167                    collection: "incidents".into(),
4168                    id: format!("incident-{}", i),
4169                    value: serde_json::json!({
4170                        "title": format!("Incident {}", i),
4171                        "status": if i % 2 == 0 { "open" } else { "closed" }
4172                    }),
4173                    if_version: None,
4174                })
4175                .collect(),
4176            fences: vec![],
4177        };
4178        execute_transaction(&db, &schema, &request, None).unwrap();
4179        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4180        let result = execute_query(
4181            &db,
4182            &schema,
4183            &context,
4184            &CanonicalQuery {
4185                collection: "incidents".into(),
4186                filter: Some(QueryFilter::Eq {
4187                    field: "status".into(),
4188                    value: Value::String("open".into()),
4189                }),
4190                order_by: vec![],
4191                limit: Some(100),
4192                offset: 0,
4193                cursor: None,
4194                projection: vec![],
4195                group_by: vec![],
4196                aggregates: vec![],
4197                references: vec![],
4198                semantic: None,
4199            },
4200            None,
4201        )
4202        .unwrap();
4203        assert_eq!(result.plan.access_method, Some("index".to_string()));
4204        assert_eq!(result.plan.index, Some("status".to_string()));
4205        assert!(!result.plan.scan_fallback);
4206        assert_eq!(result.plan.implementation, "index_lookup");
4207        assert!(result.plan.actual_rows_scanned.unwrap() <= 100);
4208        assert_eq!(
4209            result.plan.index_metrics.as_ref().unwrap().index_used,
4210            Some("status".to_string())
4211        );
4212        assert!(result.plan.index_metrics.as_ref().unwrap().index_hits > 0);
4213    }
4214    #[test]
4215    fn non_indexed_field_query_falls_back_to_scan() {
4216        let db = db("regression-non-indexed");
4217        let schema = schema();
4218        let request = TransactionRequest {
4219            transaction_id: None,
4220            tenant_id: "tenant".into(),
4221            application_id: "app".into(),
4222            revision_id: "rev".into(),
4223            schema_version: 1,
4224            state_namespace: None,
4225            causal_parent: None,
4226            authorization: auth(),
4227            preconditions: vec![],
4228            operations: (1..=50)
4229                .map(|i| TransactionOperation {
4230                    kind: TransactionOperationKind::Insert,
4231                    collection: "incidents".into(),
4232                    id: format!("incident-{}", i),
4233                    value: serde_json::json!({
4234                        "title": format!("Incident {}", i),
4235                        "status": "open"
4236                    }),
4237                    if_version: None,
4238                })
4239                .collect(),
4240            fences: vec![],
4241        };
4242        execute_transaction(&db, &schema, &request, None).unwrap();
4243        let context = begin_read(&db, &schema, "app", auth()).unwrap();
4244        let result = execute_query(
4245            &db,
4246            &schema,
4247            &context,
4248            &CanonicalQuery {
4249                collection: "incidents".into(),
4250                filter: Some(QueryFilter::Eq {
4251                    field: "title".into(),
4252                    value: Value::String("Incident 25".into()),
4253                }),
4254                order_by: vec![],
4255                limit: Some(100),
4256                offset: 0,
4257                cursor: None,
4258                projection: vec![],
4259                group_by: vec![],
4260                aggregates: vec![],
4261                references: vec![],
4262                semantic: None,
4263            },
4264            None,
4265        )
4266        .unwrap();
4267        assert_eq!(
4268            result.plan.access_method,
4269            Some("sequential_scan".to_string())
4270        );
4271        assert!(result.plan.scan_fallback);
4272        assert_eq!(result.plan.implementation, "full_table_scan");
4273        assert_eq!(
4274            result.plan.index_metrics.as_ref().unwrap().fallback_used,
4275            true
4276        );
4277        assert_eq!(
4278            result.plan.index_metrics.as_ref().unwrap().fallback_reason,
4279            Some("no_applicable_index".to_string())
4280        );
4281    }
4282    #[test]
4283    fn timing_measurements_are_populated() {
4284        let db = db("regression-timing");
4285        let schema = schema();
4286        let request = TransactionRequest {
4287            transaction_id: None,
4288            tenant_id: "tenant".into(),
4289            application_id: "app".into(),
4290            revision_id: "rev".into(),
4291            schema_version: 1,
4292            state_namespace: None,
4293            causal_parent: None,
4294            authorization: auth(),
4295            preconditions: vec![],
4296            operations: vec![TransactionOperation {
4297                kind: TransactionOperationKind::Insert,
4298                collection: "incidents".into(),
4299                id: "test-incident".into(),
4300                value: serde_json::json!({"title": "Test", "status": "open"}),
4301                if_version: None,
4302            }],
4303            fences: vec![],
4304        };
4305        execute_transaction(&db, &schema, &request, None).unwrap();
4306        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4307        let result = execute_query(
4308            &db,
4309            &schema,
4310            &context,
4311            &CanonicalQuery {
4312                collection: "incidents".into(),
4313                filter: Some(QueryFilter::Eq {
4314                    field: "status".into(),
4315                    value: Value::String("open".into()),
4316                }),
4317                order_by: vec![],
4318                limit: Some(100),
4319                offset: 0,
4320                cursor: None,
4321                projection: vec![],
4322                group_by: vec![],
4323                aggregates: vec![],
4324                references: vec![],
4325                semantic: None,
4326            },
4327            None,
4328        )
4329        .unwrap();
4330        assert!(result.plan.timing.is_some());
4331        let timing = result.plan.timing.unwrap();
4332        assert!(timing.planning_ms.is_some());
4333        assert!(timing.filtering_ms.is_some());
4334        assert!(timing.serialization_ms.is_some());
4335        assert!(timing.total_ms.is_some());
4336        assert!(timing.total_ms.unwrap() > 0.0);
4337    }
4338    #[test]
4339    fn index_metrics_track_hits_and_misses() {
4340        let db = db("regression-metrics");
4341        let schema = schema();
4342        let request = TransactionRequest {
4343            transaction_id: None,
4344            tenant_id: "tenant".into(),
4345            application_id: "app".into(),
4346            revision_id: "rev".into(),
4347            schema_version: 1,
4348            state_namespace: None,
4349            causal_parent: None,
4350            authorization: auth(),
4351            preconditions: vec![],
4352            operations: (1..=20)
4353                .map(|i| TransactionOperation {
4354                    kind: TransactionOperationKind::Insert,
4355                    collection: "incidents".into(),
4356                    id: format!("incident-{}", i),
4357                    value: serde_json::json!({
4358                        "title": format!("Incident {}", i),
4359                        "status": if i <= 10 { "open" } else { "closed" }
4360                    }),
4361                    if_version: None,
4362                })
4363                .collect(),
4364            fences: vec![],
4365        };
4366        execute_transaction(&db, &schema, &request, None).unwrap();
4367        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4368        let result = execute_query(
4369            &db,
4370            &schema,
4371            &context,
4372            &CanonicalQuery {
4373                collection: "incidents".into(),
4374                filter: Some(QueryFilter::Eq {
4375                    field: "status".into(),
4376                    value: Value::String("open".into()),
4377                }),
4378                order_by: vec![],
4379                limit: Some(100),
4380                offset: 0,
4381                cursor: None,
4382                projection: vec![],
4383                group_by: vec![],
4384                aggregates: vec![],
4385                references: vec![],
4386                semantic: None,
4387            },
4388            None,
4389        )
4390        .unwrap();
4391        let metrics = result.plan.index_metrics.as_ref().unwrap();
4392        assert_eq!(metrics.index_used, Some("status".to_string()));
4393        assert_eq!(metrics.index_hits, 10);
4394        assert_eq!(metrics.index_misses, 0);
4395        assert!(!metrics.fallback_used);
4396    }
4397
4398    #[test]
4399    fn scoped_cursor_is_complete_deterministic_and_bound_to_scope_and_collection() {
4400        let db = db("scoped-pagination");
4401        let schema = scoped_schema();
4402        let operations = (0..237)
4403            .map(|i| (format!("a-{i:03}"), "tenant-a"))
4404            .chain((0..1000).map(|i| (format!("b-{i:04}"), "tenant-b")))
4405            .map(|(id, scope)| TransactionOperation {
4406                kind: TransactionOperationKind::Insert,
4407                collection: "incidents".into(),
4408                id: id.clone(),
4409                value: serde_json::json!({"title": id, "status": scope}),
4410                if_version: None,
4411            })
4412            .collect();
4413        execute_transaction(
4414            &db,
4415            &schema,
4416            &TransactionRequest {
4417                transaction_id: Some("seed-scopes".into()),
4418                tenant_id: "tenant".into(),
4419                application_id: "app".into(),
4420                revision_id: "rev".into(),
4421                schema_version: 1,
4422                state_namespace: None,
4423                causal_parent: None,
4424                authorization: auth(),
4425                preconditions: vec![],
4426                operations,
4427                fences: vec![],
4428            },
4429            None,
4430        )
4431        .unwrap();
4432
4433        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4434        let mut query = CanonicalQuery {
4435            collection: "incidents".into(),
4436            filter: Some(QueryFilter::Eq {
4437                field: "status".into(),
4438                value: Value::String("tenant-a".into()),
4439            }),
4440            order_by: vec![],
4441            limit: Some(25),
4442            offset: 0,
4443            cursor: None,
4444            projection: vec![],
4445            group_by: vec![],
4446            aggregates: vec![],
4447            references: vec![],
4448            semantic: None,
4449        };
4450        let mut ids = Vec::new();
4451        let first_cursor = loop {
4452            let page = execute_query(&db, &schema, &context, &query, None).unwrap();
4453            assert!(page.records.len() <= 25);
4454            assert!(
4455                page.records
4456                    .iter()
4457                    .all(|record| record["status"] == "tenant-a")
4458            );
4459            ids.extend(
4460                page.records
4461                    .iter()
4462                    .map(|record| record["_id"].as_str().unwrap().to_owned()),
4463            );
4464            let Some(cursor) = page.next_cursor else {
4465                break query.cursor.clone();
4466            };
4467            query.cursor = Some(cursor);
4468        };
4469        assert_eq!(ids.len(), 237);
4470        let unique = ids.iter().collect::<std::collections::BTreeSet<_>>();
4471        assert_eq!(unique.len(), 237);
4472        assert!(first_cursor.is_some());
4473
4474        let cursor = encode_query_cursor(
4475            &pagination_query_hash(&CanonicalQuery {
4476                cursor: None,
4477                ..query.clone()
4478            })
4479            .unwrap(),
4480            25,
4481        );
4482        let mut wrong_scope = query.clone();
4483        wrong_scope.cursor = Some(cursor.clone());
4484        wrong_scope.filter = Some(QueryFilter::Eq {
4485            field: "status".into(),
4486            value: Value::String("tenant-b".into()),
4487        });
4488        assert_eq!(
4489            execute_query(&db, &schema, &context, &wrong_scope, None)
4490                .unwrap_err()
4491                .code,
4492            "INVALID_CURSOR"
4493        );
4494        let mut malformed = query;
4495        malformed.cursor = Some("not-a-cursor".into());
4496        assert_eq!(
4497            execute_query(&db, &schema, &context, &malformed, None)
4498                .unwrap_err()
4499                .code,
4500            "INVALID_CURSOR"
4501        );
4502    }
4503
4504    /// Every query surface must either implement a recognized semantic clause
4505    /// per `docs/reference/semantic-query.md` or refuse it. This surface
4506    /// refuses: a semantic clause is `SEMANTIC_UNSUPPORTED` before any
4507    /// deterministic work, a malformed one is `INVALID_QUERY`, nothing is
4508    /// read, nothing is judged, and the same query without the clause still
4509    /// answers.
4510    #[test]
4511    fn semantic_clause_is_recognized_and_refused_never_ignored() {
4512        let db = db("semantic-refusal");
4513        let schema = schema();
4514        execute_transaction(
4515            &db,
4516            &schema,
4517            &TransactionRequest {
4518                transaction_id: Some("seed".into()),
4519                tenant_id: "tenant".into(),
4520                application_id: "app".into(),
4521                revision_id: "rev".into(),
4522                schema_version: 1,
4523                state_namespace: None,
4524                causal_parent: None,
4525                authorization: auth(),
4526                preconditions: vec![],
4527                operations: vec![TransactionOperation {
4528                    kind: TransactionOperationKind::Insert,
4529                    collection: "incidents".into(),
4530                    id: "i1".into(),
4531                    value: serde_json::json!({ "title": "First", "status": "open" }),
4532                    if_version: None,
4533                }],
4534                fences: vec![],
4535            },
4536            None,
4537        )
4538        .unwrap();
4539        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4540        let plain = CanonicalQuery {
4541            collection: "incidents".into(),
4542            filter: Some(QueryFilter::Eq {
4543                field: "status".into(),
4544                value: Value::String("open".into()),
4545            }),
4546            order_by: vec![],
4547            limit: Some(10),
4548            offset: 0,
4549            cursor: None,
4550            projection: vec![],
4551            group_by: vec![],
4552            aggregates: vec![],
4553            references: vec![],
4554            semantic: None,
4555        };
4556        let state_before = db.state_rows().unwrap().len();
4557
4558        // A semantic clause in the bounded query's own grammar, and one that
4559        // only expresses semantic intent, are both recognized and refused.
4560        for clause in [
4561            serde_json::json!({
4562                "decision": { "kind": "binary", "predicate": "needs a human" },
4563                "filter": { "is": true },
4564            }),
4565            serde_json::json!({ "decision": "needs_human" }),
4566            serde_json::json!({}),
4567        ] {
4568            let failure = execute_query(
4569                &db,
4570                &schema,
4571                &context,
4572                &CanonicalQuery {
4573                    semantic: Some(clause),
4574                    ..plain.clone()
4575                },
4576                None,
4577            )
4578            .expect_err("semantic intent is never silently dropped");
4579            assert_eq!(failure.code, "SEMANTIC_UNSUPPORTED");
4580        }
4581
4582        // A semantic value that is not a clause is a validation error, not a
4583        // recognized-but-unsupported operation.
4584        for malformed in [
4585            serde_json::json!("yes"),
4586            serde_json::json!(true),
4587            serde_json::json!(1),
4588            serde_json::json!(["needs_human"]),
4589        ] {
4590            let failure = execute_query(
4591                &db,
4592                &schema,
4593                &context,
4594                &CanonicalQuery {
4595                    semantic: Some(malformed),
4596                    ..plain.clone()
4597                },
4598                None,
4599            )
4600            .expect_err("a malformed semantic value is invalid");
4601            assert_eq!(failure.code, "INVALID_QUERY");
4602        }
4603
4604        // The clause is refused ahead of every other validation, so a query
4605        // that is also otherwise invalid still fails on semantic intent.
4606        let failure = execute_query(
4607            &db,
4608            &schema,
4609            &context,
4610            &CanonicalQuery {
4611                semantic: Some(serde_json::json!({ "decision": "needs_human" })),
4612                limit: Some(0),
4613                ..plain.clone()
4614            },
4615            None,
4616        )
4617        .unwrap_err();
4618        assert_eq!(failure.code, "SEMANTIC_UNSUPPORTED");
4619
4620        // Refusal reads nothing, judges nothing, and writes nothing: no
4621        // judgment collection appears and the state is what it was.
4622        assert_eq!(db.state_rows().unwrap().len(), state_before);
4623        let judgments: Vec<Value> = db
4624            .capability(crate::semantic_decision::DEFAULT_DECISION_COLLECTION)
4625            .find(|_: &Value| true)
4626            .unwrap();
4627        assert!(judgments.is_empty());
4628
4629        // The same query without the clause answers as before.
4630        let page = execute_query(&db, &schema, &context, &plain, None).unwrap();
4631        assert_eq!(page.records.len(), 1);
4632    }
4633
4634    /// Over the wire, `semantic` is carried into the query rather than lost
4635    /// as an unknown key, `null` means absent, and an unrelated unknown key
4636    /// keeps its existing behavior of being ignored: only semantic intent is
4637    /// recognized, because only semantic intent changes what a query means.
4638    #[test]
4639    fn semantic_clause_survives_deserialization_and_changes_query_identity() {
4640        let plain: CanonicalQuery = serde_json::from_value(serde_json::json!({
4641            "collection": "incidents",
4642            "limit": 10,
4643        }))
4644        .unwrap();
4645        assert!(plain.semantic.is_none());
4646        let with_clause: CanonicalQuery = serde_json::from_value(serde_json::json!({
4647            "collection": "incidents",
4648            "limit": 10,
4649            "semantic": { "decision": "needs_human" },
4650        }))
4651        .unwrap();
4652        assert_eq!(
4653            with_clause.semantic,
4654            Some(serde_json::json!({ "decision": "needs_human" }))
4655        );
4656        assert_ne!(
4657            query_hash(&plain).unwrap(),
4658            query_hash(&with_clause).unwrap(),
4659            "a semantic clause is part of what the query is"
4660        );
4661        let malformed: CanonicalQuery = serde_json::from_value(serde_json::json!({
4662            "collection": "incidents",
4663            "semantic": "yes",
4664        }))
4665        .unwrap();
4666        assert_eq!(malformed.semantic, Some(Value::String("yes".into())));
4667        let null: CanonicalQuery = serde_json::from_value(serde_json::json!({
4668            "collection": "incidents",
4669            "semantic": null,
4670        }))
4671        .unwrap();
4672        assert!(null.semantic.is_none());
4673        let unrelated: CanonicalQuery = serde_json::from_value(serde_json::json!({
4674            "collection": "incidents",
4675            "limit": 10,
4676            "refill": true,
4677        }))
4678        .unwrap();
4679        assert_eq!(query_hash(&unrelated).unwrap(), query_hash(&plain).unwrap());
4680        // Serialization of a query without a clause is unchanged, so every
4681        // existing query hash and cursor binding is unchanged.
4682        assert!(!serde_json::to_string(&plain).unwrap().contains("semantic"));
4683    }
4684
4685    #[test]
4686    fn scoped_index_work_is_independent_of_unrelated_growth() {
4687        fn examined(unrelated: usize) -> usize {
4688            let db = db(&format!("scope-growth-{unrelated}"));
4689            let schema = scoped_schema();
4690            let operations = (0..20)
4691                .map(|i| (format!("target-{i}"), "tenant-a"))
4692                .chain((0..unrelated).map(|i| (format!("other-{i}"), "tenant-b")))
4693                .map(|(id, status)| TransactionOperation {
4694                    kind: TransactionOperationKind::Insert,
4695                    collection: "incidents".into(),
4696                    id: id.clone(),
4697                    value: serde_json::json!({"title": id, "status": status}),
4698                    if_version: None,
4699                })
4700                .collect();
4701            execute_transaction(
4702                &db,
4703                &schema,
4704                &TransactionRequest {
4705                    transaction_id: None,
4706                    tenant_id: "tenant".into(),
4707                    application_id: "app".into(),
4708                    revision_id: "rev".into(),
4709                    schema_version: 1,
4710                    state_namespace: None,
4711                    causal_parent: None,
4712                    authorization: auth(),
4713                    preconditions: vec![],
4714                    operations,
4715                    fences: vec![],
4716                },
4717                None,
4718            )
4719            .unwrap();
4720            let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4721            let result = execute_query(
4722                &db,
4723                &schema,
4724                &context,
4725                &CanonicalQuery {
4726                    collection: "incidents".into(),
4727                    filter: Some(QueryFilter::Eq {
4728                        field: "status".into(),
4729                        value: Value::String("tenant-a".into()),
4730                    }),
4731                    order_by: vec![],
4732                    limit: Some(20),
4733                    offset: 0,
4734                    cursor: None,
4735                    projection: vec![],
4736                    group_by: vec![],
4737                    aggregates: vec![],
4738                    references: vec![],
4739                    semantic: None,
4740                },
4741                None,
4742            )
4743            .unwrap();
4744            assert_eq!(result.records.len(), 20);
4745            result.plan.actual_rows_scanned.unwrap()
4746        }
4747        assert_eq!(
4748            [examined(100), examined(1000), examined(10_000)],
4749            [20, 20, 20]
4750        );
4751    }
4752
4753    #[test]
4754    fn managed_scope_without_schema_index_fails_closed() {
4755        let db = db("scope-missing-index");
4756        let mut schema = schema();
4757        schema.collections[0].fields[0].name = "tenant_id".into();
4758        execute_transaction(
4759            &db,
4760            &schema,
4761            &TransactionRequest {
4762                transaction_id: None,
4763                tenant_id: "tenant".into(),
4764                application_id: "app".into(),
4765                revision_id: "rev".into(),
4766                schema_version: 1,
4767                state_namespace: None,
4768                causal_parent: None,
4769                authorization: auth(),
4770                preconditions: vec![],
4771                operations: vec![TransactionOperation {
4772                    kind: TransactionOperationKind::Insert,
4773                    collection: "incidents".into(),
4774                    id: "a".into(),
4775                    value: serde_json::json!({"tenant_id":"tenant-a"}),
4776                    if_version: None,
4777                }],
4778                fences: vec![],
4779            },
4780            None,
4781        )
4782        .unwrap();
4783        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4784        let failure = execute_query(
4785            &db,
4786            &schema,
4787            &context,
4788            &CanonicalQuery {
4789                collection: "incidents".into(),
4790                filter: Some(QueryFilter::Eq {
4791                    field: "tenant_id".into(),
4792                    value: Value::String("tenant-a".into()),
4793                }),
4794                order_by: vec![],
4795                limit: Some(20),
4796                offset: 0,
4797                cursor: None,
4798                projection: vec![],
4799                group_by: vec![],
4800                aggregates: vec![],
4801                references: vec![],
4802                semantic: None,
4803            },
4804            None,
4805        )
4806        .unwrap_err();
4807        assert_eq!(failure.code, "INDEX_UNAVAILABLE");
4808    }
4809    #[test]
4810    #[ignore]
4811    fn gate_a_46_collection_sherpa_workload_benchmark() {
4812        use std::time::Instant;
4813        fn sherpa_schema() -> StateSchema {
4814            let mut collections = vec![];
4815            for i in 1..=46 {
4816                collections.push(CollectionSchema {
4817                    name: format!("collection_{}", i),
4818                    version: 1,
4819                    fields: vec![
4820                        FieldSchema {
4821                            name: "id".into(),
4822                            field_type: FieldType::Primitive {
4823                                primitive: PrimitiveType::String,
4824                            },
4825                            nullable: false,
4826                            required: true,
4827                            default: None,
4828                            constraints: Default::default(),
4829                            computed: None,
4830                        },
4831                        FieldSchema {
4832                            name: "name".into(),
4833                            field_type: FieldType::Primitive {
4834                                primitive: PrimitiveType::String,
4835                            },
4836                            nullable: false,
4837                            required: true,
4838                            default: None,
4839                            constraints: Default::default(),
4840                            computed: None,
4841                        },
4842                        FieldSchema {
4843                            name: "status".into(),
4844                            field_type: FieldType::Enum {
4845                                values: vec!["active".into(), "inactive".into()],
4846                            },
4847                            nullable: false,
4848                            required: false,
4849                            default: Some(Value::String("active".into())),
4850                            constraints: Default::default(),
4851                            computed: None,
4852                        },
4853                    ],
4854                    indexes: vec![
4855                        SchemaIndex {
4856                            name: "id_idx".into(),
4857                            fields: vec!["id".into()],
4858                            unique: false,
4859                            sparse: false,
4860                            version: 1,
4861                        },
4862                        SchemaIndex {
4863                            name: "status_idx".into(),
4864                            fields: vec!["status".into()],
4865                            unique: false,
4866                            sparse: false,
4867                            version: 1,
4868                        },
4869                    ],
4870                });
4871            }
4872            StateSchema {
4873                contract_version: 1,
4874                schema_version: 1,
4875                application_id: "app".into(),
4876                revision_id: "rev".into(),
4877                collections,
4878            }
4879        }
4880        let db = db("gate-a-sherpa-bench");
4881        let schema = sherpa_schema();
4882        let auth_ctx = auth();
4883        println!("Gate A Benchmark: Loading 46 collections...");
4884        let load_start = Instant::now();
4885        for col_idx in 1..=46 {
4886            let collection_name = format!("collection_{}", col_idx);
4887            let records_per_collection = 500;
4888            let mut operations = vec![];
4889            for record_idx in 1..=records_per_collection {
4890                operations.push(TransactionOperation {
4891                    kind: TransactionOperationKind::Insert,
4892                    collection: collection_name.clone(),
4893                    id: format!("rec-{}", record_idx),
4894                    value: serde_json::json!({
4895                        "id": format!("id-{}", record_idx),
4896                        "name": format!("Record {}", record_idx),
4897                        "status": if record_idx % 2 == 0 { "active" } else { "inactive" }
4898                    }),
4899                    if_version: None,
4900                });
4901            }
4902            let request = TransactionRequest {
4903                transaction_id: None,
4904                tenant_id: "tenant".into(),
4905                application_id: "app".into(),
4906                revision_id: "rev".into(),
4907                schema_version: 1,
4908                state_namespace: None,
4909                causal_parent: None,
4910                authorization: auth_ctx.clone(),
4911                preconditions: vec![],
4912                operations,
4913                fences: vec![],
4914            };
4915            execute_transaction(&db, &schema, &request, None).unwrap();
4916        }
4917        let load_ms = load_start.elapsed().as_secs_f64() * 1000.0;
4918        println!("  Loaded ~23,000 records in {:.2}ms", load_ms);
4919        println!("Gate A Benchmark: Executing 46 indexed queries...");
4920        let query_start = Instant::now();
4921        let context = begin_read(&db, &schema, "app", auth_ctx).unwrap();
4922        let mut query_times = vec![];
4923        let mut index_used_count = 0;
4924        for col_idx in 1..=46 {
4925            let collection_name = format!("collection_{}", col_idx);
4926            let q_start = Instant::now();
4927            let result = execute_query(
4928                &db,
4929                &schema,
4930                &context,
4931                &CanonicalQuery {
4932                    collection: collection_name,
4933                    filter: Some(QueryFilter::Eq {
4934                        field: "status".into(),
4935                        value: Value::String("active".into()),
4936                    }),
4937                    order_by: vec![],
4938                    limit: Some(1000),
4939                    offset: 0,
4940                    cursor: None,
4941                    projection: vec![],
4942                    group_by: vec![],
4943                    aggregates: vec![],
4944                    references: vec![],
4945                    semantic: None,
4946                },
4947                None,
4948            )
4949            .unwrap();
4950            let q_ms = q_start.elapsed().as_secs_f64() * 1000.0;
4951            query_times.push(q_ms);
4952            assert_eq!(
4953                result.plan.access_method,
4954                Some("index".to_string()),
4955                "Query {} should use index",
4956                col_idx
4957            );
4958            if result
4959                .plan
4960                .index_metrics
4961                .as_ref()
4962                .unwrap()
4963                .index_used
4964                .is_some()
4965            {
4966                index_used_count += 1;
4967            }
4968        }
4969        let total_query_time_ms = query_start.elapsed().as_secs_f64() * 1000.0;
4970        query_times.sort_by(|a, b| a.partial_cmp(b).unwrap());
4971        let median_query_time = query_times[query_times.len() / 2];
4972        println!("\n=== Gate A Verification ===");
4973        println!("Indexes used: {}/46", index_used_count);
4974        println!(
4975            "Total query time: {:.2}ms (target: <10,000ms)",
4976            total_query_time_ms
4977        );
4978        println!(
4979            "Per-query median: {:.2}ms (target: <100ms)",
4980            median_query_time
4981        );
4982        assert_eq!(index_used_count, 46, "All queries must use indexes");
4983        assert!(
4984            total_query_time_ms < 10000.0,
4985            "Total time must be <10s (was {:.2}ms)",
4986            total_query_time_ms
4987        );
4988    }
4989
4990    #[test]
4991    fn authenticated_policy_allows_query_with_capability() {
4992        let db = db("auth_read_allow");
4993        let schema = schema();
4994
4995        // Actor with "state:read" capability
4996        let mut auth_context = auth();
4997        auth_context.subject = "user:alice".into();
4998
4999        let context = begin_read(&db, &schema, "app", auth_context).unwrap();
5000        let query = CanonicalQuery {
5001            collection: "incidents".into(),
5002            filter: None,
5003            order_by: vec![],
5004            limit: Some(100),
5005            offset: 0,
5006            cursor: None,
5007            projection: vec![],
5008            group_by: vec![],
5009            aggregates: vec![],
5010            references: vec![],
5011            semantic: None,
5012        };
5013
5014        let result = execute_query(&db, &schema, &context, &query, None);
5015        assert!(
5016            result.is_ok(),
5017            "Query should succeed with state:read capability"
5018        );
5019    }
5020
5021    #[test]
5022    fn authenticated_policy_denies_query_without_capability() {
5023        let db = db("auth_read_deny");
5024        let schema = schema();
5025
5026        // Actor without "state:read" capability (unauthenticated equivalent)
5027        let auth_context = AuthorizationContext {
5028            subject: "user:bob".into(),
5029            tenant_id: "tenant".into(),
5030            application_id: "app".into(),
5031            revision_id: "rev".into(),
5032            capabilities: BTreeSet::new(), // No capabilities
5033            readable_collections: Default::default(),
5034            writable_collections: Default::default(),
5035            field_projections: Default::default(),
5036        };
5037
5038        let context = begin_read(&db, &schema, "app", auth_context).unwrap();
5039        let query = CanonicalQuery {
5040            collection: "incidents".into(),
5041            filter: None,
5042            order_by: vec![],
5043            limit: Some(100),
5044            offset: 0,
5045            cursor: None,
5046            projection: vec![],
5047            group_by: vec![],
5048            aggregates: vec![],
5049            references: vec![],
5050            semantic: None,
5051        };
5052
5053        let result = execute_query(&db, &schema, &context, &query, None);
5054        assert!(
5055            result.is_err(),
5056            "Query should fail without state:read capability"
5057        );
5058        if let Err(e) = result {
5059            assert_eq!(e.code, "AUTHORIZATION_DENIED");
5060        }
5061    }
5062
5063    #[test]
5064    fn write_policy_requires_state_write_capability() {
5065        let db = db("auth_write_deny");
5066        let schema = schema();
5067
5068        // Transaction without state:write capability
5069        let auth_context = AuthorizationContext {
5070            subject: "user:charlie".into(),
5071            tenant_id: "tenant".into(),
5072            application_id: "app".into(),
5073            revision_id: "rev".into(),
5074            capabilities: BTreeSet::new(), // No capabilities
5075            readable_collections: Default::default(),
5076            writable_collections: Default::default(),
5077            field_projections: Default::default(),
5078        };
5079
5080        let request = TransactionRequest {
5081            transaction_id: None,
5082            tenant_id: "tenant".into(),
5083            application_id: "app".into(),
5084            revision_id: "rev".into(),
5085            schema_version: 1,
5086            state_namespace: None,
5087            causal_parent: None,
5088            authorization: auth_context,
5089            operations: vec![TransactionOperation {
5090                kind: TransactionOperationKind::Insert,
5091                collection: "incidents".into(),
5092                id: "proj-1".into(),
5093                value: serde_json::json!({"title": "Test Project"}),
5094                if_version: None,
5095            }],
5096            preconditions: vec![],
5097            fences: vec![],
5098        };
5099
5100        let result = execute_transaction(&db, &schema, &request, None);
5101        assert!(
5102            result.is_err(),
5103            "Transaction should fail without state:write capability"
5104        );
5105        if let Err(e) = result {
5106            assert_eq!(e.code, "AUTHORIZATION_DENIED");
5107        }
5108    }
5109
5110    // PR #2.1 Verification Tests: Prove runtime enforcement works end-to-end
5111
5112    #[test]
5113    fn policy_enforcement_unauthenticated_read_denied() {
5114        // Test: Unauthenticated actor cannot read even with valid query
5115        let db = db("policy_unauth_read");
5116        let schema = schema();
5117
5118        // Pre-populate data
5119        let insert_request = TransactionRequest {
5120            transaction_id: None,
5121            tenant_id: "tenant".into(),
5122            application_id: "app".into(),
5123            revision_id: "rev".into(),
5124            schema_version: 1,
5125            state_namespace: None,
5126            causal_parent: None,
5127            authorization: auth(),
5128            operations: vec![TransactionOperation {
5129                kind: TransactionOperationKind::Insert,
5130                collection: "incidents".into(),
5131                id: "inc-1".into(),
5132                value: serde_json::json!({"title": "Test Incident"}),
5133                if_version: None,
5134            }],
5135            preconditions: vec![],
5136            fences: vec![],
5137        };
5138        execute_transaction(&db, &schema, &insert_request, None).unwrap();
5139
5140        // Unauthenticated read attempt (no capabilities)
5141        let unauth_context = AuthorizationContext {
5142            subject: "anonymous".into(),
5143            tenant_id: "tenant".into(),
5144            application_id: "app".into(),
5145            revision_id: "rev".into(),
5146            capabilities: BTreeSet::new(), // No capabilities
5147            readable_collections: Default::default(),
5148            writable_collections: Default::default(),
5149            field_projections: Default::default(),
5150        };
5151
5152        let context = begin_read(&db, &schema, "app", unauth_context).unwrap();
5153        let query = CanonicalQuery {
5154            collection: "incidents".into(),
5155            filter: None,
5156            order_by: vec![],
5157            limit: Some(100),
5158            offset: 0,
5159            cursor: None,
5160            projection: vec![],
5161            group_by: vec![],
5162            aggregates: vec![],
5163            references: vec![],
5164            semantic: None,
5165        };
5166
5167        let result = execute_query(&db, &schema, &context, &query, None);
5168        assert!(result.is_err(), "Unauthenticated read must be denied");
5169        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5170    }
5171
5172    #[test]
5173    fn policy_enforcement_unauthenticated_write_denied() {
5174        // Test: Unauthenticated actor cannot write
5175        let db = db("policy_unauth_write");
5176        let schema = schema();
5177
5178        let unauth_context = AuthorizationContext {
5179            subject: "anonymous".into(),
5180            tenant_id: "tenant".into(),
5181            application_id: "app".into(),
5182            revision_id: "rev".into(),
5183            capabilities: BTreeSet::new(), // No capabilities
5184            readable_collections: Default::default(),
5185            writable_collections: Default::default(),
5186            field_projections: Default::default(),
5187        };
5188
5189        let request = TransactionRequest {
5190            transaction_id: None,
5191            tenant_id: "tenant".into(),
5192            application_id: "app".into(),
5193            revision_id: "rev".into(),
5194            schema_version: 1,
5195            state_namespace: None,
5196            causal_parent: None,
5197            authorization: unauth_context,
5198            operations: vec![TransactionOperation {
5199                kind: TransactionOperationKind::Insert,
5200                collection: "incidents".into(),
5201                id: "inc-1".into(),
5202                value: serde_json::json!({"title": "Incident"}),
5203                if_version: None,
5204            }],
5205            preconditions: vec![],
5206            fences: vec![],
5207        };
5208
5209        let result = execute_transaction(&db, &schema, &request, None);
5210        assert!(result.is_err(), "Unauthenticated write must be denied");
5211        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5212    }
5213
5214    #[test]
5215    fn policy_enforcement_authenticated_read_allowed() {
5216        // Test: Authenticated actor can read with proper capability
5217        let db = db("policy_auth_read_ok");
5218        let schema = schema();
5219
5220        // Pre-populate data
5221        let insert_request = TransactionRequest {
5222            transaction_id: None,
5223            tenant_id: "tenant".into(),
5224            application_id: "app".into(),
5225            revision_id: "rev".into(),
5226            schema_version: 1,
5227            state_namespace: None,
5228            causal_parent: None,
5229            authorization: auth(),
5230            operations: vec![TransactionOperation {
5231                kind: TransactionOperationKind::Insert,
5232                collection: "incidents".into(),
5233                id: "inc-1".into(),
5234                value: serde_json::json!({"title": "Test Incident"}),
5235                if_version: None,
5236            }],
5237            preconditions: vec![],
5238            fences: vec![],
5239        };
5240        execute_transaction(&db, &schema, &insert_request, None).unwrap();
5241
5242        // Authenticated read (with state:read capability)
5243        let auth_context = AuthorizationContext {
5244            subject: "user:alice".into(),
5245            tenant_id: "tenant".into(),
5246            application_id: "app".into(),
5247            revision_id: "rev".into(),
5248            capabilities: ["state:read".into()].into(),
5249            readable_collections: Default::default(),
5250            writable_collections: Default::default(),
5251            field_projections: Default::default(),
5252        };
5253
5254        let context = begin_read(&db, &schema, "app", auth_context).unwrap();
5255        let query = CanonicalQuery {
5256            collection: "incidents".into(),
5257            filter: None,
5258            order_by: vec![],
5259            limit: Some(100),
5260            offset: 0,
5261            cursor: None,
5262            projection: vec![],
5263            group_by: vec![],
5264            aggregates: vec![],
5265            references: vec![],
5266            semantic: None,
5267        };
5268
5269        let result = execute_query(&db, &schema, &context, &query, None);
5270        assert!(result.is_ok(), "Authenticated read must be allowed");
5271        let query_result = result.unwrap();
5272        assert!(
5273            query_result.records.len() >= 1,
5274            "Query should return the inserted record"
5275        );
5276    }
5277
5278    #[test]
5279    fn policy_enforcement_authenticated_write_allowed() {
5280        // Test: Authenticated actor can write with proper capability
5281        let db = db("policy_auth_write_ok");
5282        let schema = schema();
5283
5284        let auth_context = AuthorizationContext {
5285            subject: "user:bob".into(),
5286            tenant_id: "tenant".into(),
5287            application_id: "app".into(),
5288            revision_id: "rev".into(),
5289            capabilities: ["state:write".into()].into(),
5290            readable_collections: Default::default(),
5291            writable_collections: Default::default(),
5292            field_projections: Default::default(),
5293        };
5294
5295        let request = TransactionRequest {
5296            transaction_id: None,
5297            tenant_id: "tenant".into(),
5298            application_id: "app".into(),
5299            revision_id: "rev".into(),
5300            schema_version: 1,
5301            state_namespace: None,
5302            causal_parent: None,
5303            authorization: auth_context,
5304            operations: vec![TransactionOperation {
5305                kind: TransactionOperationKind::Insert,
5306                collection: "incidents".into(),
5307                id: "inc-1".into(),
5308                value: serde_json::json!({"title": "New Incident"}),
5309                if_version: None,
5310            }],
5311            preconditions: vec![],
5312            fences: vec![],
5313        };
5314
5315        let result = execute_transaction(&db, &schema, &request, None);
5316        assert!(result.is_ok(), "Authenticated write must be allowed");
5317        assert!(
5318            result.unwrap().state_after > 0,
5319            "Transaction should have succeeded"
5320        );
5321    }
5322
5323    #[test]
5324    fn policy_enforcement_transaction_atomicity_with_auth() {
5325        // Test: Transaction authorization is checked before any operations commit
5326        let db = db("policy_tx_atomic");
5327        let schema = schema();
5328
5329        // Start with one record
5330        let insert_initial = TransactionRequest {
5331            transaction_id: None,
5332            tenant_id: "tenant".into(),
5333            application_id: "app".into(),
5334            revision_id: "rev".into(),
5335            schema_version: 1,
5336            state_namespace: None,
5337            causal_parent: None,
5338            authorization: auth(),
5339            operations: vec![TransactionOperation {
5340                kind: TransactionOperationKind::Insert,
5341                collection: "incidents".into(),
5342                id: "initial".into(),
5343                value: serde_json::json!({"title": "Initial"}),
5344                if_version: None,
5345            }],
5346            preconditions: vec![],
5347            fences: vec![],
5348        };
5349        let initial_result = execute_transaction(&db, &schema, &insert_initial, None).unwrap();
5350        let state_before_denied_tx = initial_result.state_after;
5351
5352        // Attempt transaction without authorization
5353        let unauth_context = AuthorizationContext {
5354            subject: "anonymous".into(),
5355            tenant_id: "tenant".into(),
5356            application_id: "app".into(),
5357            revision_id: "rev".into(),
5358            capabilities: BTreeSet::new(), // No capabilities
5359            readable_collections: Default::default(),
5360            writable_collections: Default::default(),
5361            field_projections: Default::default(),
5362        };
5363
5364        let denied_request = TransactionRequest {
5365            transaction_id: None,
5366            tenant_id: "tenant".into(),
5367            application_id: "app".into(),
5368            revision_id: "rev".into(),
5369            schema_version: 1,
5370            state_namespace: None,
5371            causal_parent: None,
5372            authorization: unauth_context,
5373            operations: vec![TransactionOperation {
5374                kind: TransactionOperationKind::Insert,
5375                collection: "incidents".into(),
5376                id: "denied".into(),
5377                value: serde_json::json!({"title": "Should Not Commit"}),
5378                if_version: None,
5379            }],
5380            preconditions: vec![],
5381            fences: vec![],
5382        };
5383
5384        let denied_result = execute_transaction(&db, &schema, &denied_request, None);
5385        assert!(denied_result.is_err(), "Transaction should be denied");
5386
5387        // Verify state hasn't changed
5388        let verify_auth = auth();
5389        let verify_context = begin_read(&db, &schema, "app", verify_auth).unwrap();
5390        let verify_query = CanonicalQuery {
5391            collection: "incidents".into(),
5392            filter: None,
5393            order_by: vec![],
5394            limit: Some(100),
5395            offset: 0,
5396            cursor: None,
5397            projection: vec![],
5398            group_by: vec![],
5399            aggregates: vec![],
5400            references: vec![],
5401            semantic: None,
5402        };
5403
5404        let verify_result =
5405            execute_query(&db, &schema, &verify_context, &verify_query, None).unwrap();
5406        assert_eq!(
5407            verify_result.records.len(),
5408            1,
5409            "Only initial record should exist; denied transaction must not partially commit"
5410        );
5411    }
5412
5413    #[test]
5414    fn policy_enforcement_capability_must_match_operation() {
5415        // Test: Having state:read doesn't allow writes
5416        let db = db("policy_cap_mismatch");
5417        let schema = schema();
5418
5419        let read_only_context = AuthorizationContext {
5420            subject: "user:charlie".into(),
5421            tenant_id: "tenant".into(),
5422            application_id: "app".into(),
5423            revision_id: "rev".into(),
5424            capabilities: ["state:read".into()].into(), // Only read capability
5425            readable_collections: Default::default(),
5426            writable_collections: Default::default(),
5427            field_projections: Default::default(),
5428        };
5429
5430        let write_request = TransactionRequest {
5431            transaction_id: None,
5432            tenant_id: "tenant".into(),
5433            application_id: "app".into(),
5434            revision_id: "rev".into(),
5435            schema_version: 1,
5436            state_namespace: None,
5437            causal_parent: None,
5438            authorization: read_only_context,
5439            operations: vec![TransactionOperation {
5440                kind: TransactionOperationKind::Insert,
5441                collection: "incidents".into(),
5442                id: "inc-1".into(),
5443                value: serde_json::json!({"title": "Test"}),
5444                if_version: None,
5445            }],
5446            preconditions: vec![],
5447            fences: vec![],
5448        };
5449
5450        let result = execute_transaction(&db, &schema, &write_request, None);
5451        assert!(
5452            result.is_err(),
5453            "Write must be denied even with read capability"
5454        );
5455        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5456    }
5457
5458    #[test]
5459    fn member_policy_read_allowed_when_member() {
5460        // Test: Member policy allows reads when actor is a member of the org
5461        let db = db("member_policy_read_allow");
5462        let schema = schema();
5463
5464        // Pre-populate a record with organization_id
5465        let insert_context = AuthorizationContext {
5466            subject: "user:admin".into(),
5467            tenant_id: "tenant".into(),
5468            application_id: "app".into(),
5469            revision_id: "rev".into(),
5470            capabilities: ["state:write".into()].into(),
5471            readable_collections: Default::default(),
5472            writable_collections: Default::default(),
5473            field_projections: Default::default(),
5474        };
5475
5476        let insert_request = TransactionRequest {
5477            transaction_id: None,
5478            tenant_id: "tenant".into(),
5479            application_id: "app".into(),
5480            revision_id: "rev".into(),
5481            schema_version: 1,
5482            state_namespace: None,
5483            causal_parent: None,
5484            authorization: insert_context,
5485            operations: vec![TransactionOperation {
5486                kind: TransactionOperationKind::Insert,
5487                collection: "incidents".into(),
5488                id: "proj-1".into(),
5489                value: serde_json::json!({"title": "Test Project"}),
5490                if_version: None,
5491            }],
5492            preconditions: vec![],
5493            fences: vec![],
5494        };
5495
5496        let _ = execute_transaction(&db, &schema, &insert_request, None);
5497
5498        // Now test member policy read with capability
5499        let member_context = AuthorizationContext {
5500            subject: "user:alice".into(),
5501            tenant_id: "tenant".into(),
5502            application_id: "app".into(),
5503            revision_id: "rev".into(),
5504            capabilities: ["state:read".into()].into(),
5505            readable_collections: Default::default(),
5506            writable_collections: Default::default(),
5507            field_projections: Default::default(),
5508        };
5509
5510        let read_context = begin_read(&db, &schema, "app", member_context).unwrap();
5511        let query = CanonicalQuery {
5512            collection: "incidents".into(),
5513            filter: None,
5514            order_by: vec![],
5515            limit: Some(100),
5516            offset: 0,
5517            cursor: None,
5518            projection: vec![],
5519            group_by: vec![],
5520            aggregates: vec![],
5521            references: vec![],
5522            semantic: None,
5523        };
5524
5525        let result = execute_query(&db, &schema, &read_context, &query, None);
5526        assert!(
5527            result.is_ok(),
5528            "Member policy read should be allowed with state:read"
5529        );
5530    }
5531
5532    #[test]
5533    fn member_policy_read_denied_without_capability() {
5534        // Test: Member policy denies reads without state:read capability
5535        let db = db("member_policy_read_deny");
5536        let schema = schema();
5537
5538        let empty_context = AuthorizationContext {
5539            subject: "user:bob".into(),
5540            tenant_id: "tenant".into(),
5541            application_id: "app".into(),
5542            revision_id: "rev".into(),
5543            capabilities: BTreeSet::new(), // No capabilities
5544            readable_collections: Default::default(),
5545            writable_collections: Default::default(),
5546            field_projections: Default::default(),
5547        };
5548
5549        let read_context = begin_read(&db, &schema, "app", empty_context).unwrap();
5550        let query = CanonicalQuery {
5551            collection: "incidents".into(),
5552            filter: None,
5553            order_by: vec![],
5554            limit: Some(100),
5555            offset: 0,
5556            cursor: None,
5557            projection: vec![],
5558            group_by: vec![],
5559            aggregates: vec![],
5560            references: vec![],
5561            semantic: None,
5562        };
5563
5564        let result = execute_query(&db, &schema, &read_context, &query, None);
5565        assert!(
5566            result.is_err(),
5567            "Member policy read must be denied without state:read"
5568        );
5569        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5570    }
5571
5572    #[test]
5573    fn member_policy_write_allowed_when_member() {
5574        // Test: Member policy allows writes when actor is a member of the org
5575        let db = db("member_policy_write_allow");
5576        let schema = schema();
5577
5578        let member_context = AuthorizationContext {
5579            subject: "user:alice".into(),
5580            tenant_id: "tenant".into(),
5581            application_id: "app".into(),
5582            revision_id: "rev".into(),
5583            capabilities: ["state:write".into()].into(),
5584            readable_collections: Default::default(),
5585            writable_collections: Default::default(),
5586            field_projections: Default::default(),
5587        };
5588
5589        let write_request = TransactionRequest {
5590            transaction_id: None,
5591            tenant_id: "tenant".into(),
5592            application_id: "app".into(),
5593            revision_id: "rev".into(),
5594            schema_version: 1,
5595            state_namespace: None,
5596            causal_parent: None,
5597            authorization: member_context,
5598            operations: vec![TransactionOperation {
5599                kind: TransactionOperationKind::Insert,
5600                collection: "incidents".into(),
5601                id: "inc-member-1".into(),
5602                value: serde_json::json!({"title": "Member Project"}),
5603                if_version: None,
5604            }],
5605            preconditions: vec![],
5606            fences: vec![],
5607        };
5608
5609        let result = execute_transaction(&db, &schema, &write_request, None);
5610        assert!(
5611            result.is_ok(),
5612            "Member policy write should be allowed with state:write"
5613        );
5614    }
5615
5616    #[test]
5617    fn member_policy_write_denied_without_capability() {
5618        // Test: Member policy denies writes without state:write capability
5619        let db = db("member_policy_write_deny");
5620        let schema = schema();
5621
5622        let empty_context = AuthorizationContext {
5623            subject: "user:bob".into(),
5624            tenant_id: "tenant".into(),
5625            application_id: "app".into(),
5626            revision_id: "rev".into(),
5627            capabilities: BTreeSet::new(), // No capabilities
5628            readable_collections: Default::default(),
5629            writable_collections: Default::default(),
5630            field_projections: Default::default(),
5631        };
5632
5633        let write_request = TransactionRequest {
5634            transaction_id: None,
5635            tenant_id: "tenant".into(),
5636            application_id: "app".into(),
5637            revision_id: "rev".into(),
5638            schema_version: 1,
5639            state_namespace: None,
5640            causal_parent: None,
5641            authorization: empty_context,
5642            operations: vec![TransactionOperation {
5643                kind: TransactionOperationKind::Insert,
5644                collection: "incidents".into(),
5645                id: "inc-member-2".into(),
5646                value: serde_json::json!({"title": "Denied Project"}),
5647                if_version: None,
5648            }],
5649            preconditions: vec![],
5650            fences: vec![],
5651        };
5652
5653        let result = execute_transaction(&db, &schema, &write_request, None);
5654        assert!(
5655            result.is_err(),
5656            "Member policy write must be denied without state:write"
5657        );
5658        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5659    }
5660
5661    #[test]
5662    fn member_policy_cross_org_mutation_prevented() {
5663        // Test: Cross-organization mutation attack is prevented by lack of state:write
5664        // Scenario: user:alice is member of org-1 but tries to create/modify resource in org-2
5665        // Expected: Denied because server wouldn't grant state:write capability for org-2
5666        let db = db("member_policy_cross_org");
5667        let schema = schema();
5668
5669        // Admin inserts record in org-2
5670        let admin_context = AuthorizationContext {
5671            subject: "user:admin".into(),
5672            tenant_id: "tenant".into(),
5673            application_id: "app".into(),
5674            revision_id: "rev".into(),
5675            capabilities: ["state:write".into()].into(),
5676            readable_collections: Default::default(),
5677            writable_collections: Default::default(),
5678            field_projections: Default::default(),
5679        };
5680
5681        let insert_in_org2 = TransactionRequest {
5682            transaction_id: None,
5683            tenant_id: "tenant".into(),
5684            application_id: "app".into(),
5685            revision_id: "rev".into(),
5686            schema_version: 1,
5687            state_namespace: None,
5688            causal_parent: None,
5689            authorization: admin_context,
5690            operations: vec![TransactionOperation {
5691                kind: TransactionOperationKind::Insert,
5692                collection: "incidents".into(),
5693                id: "inc-org2-1".into(),
5694                value: serde_json::json!({"title": "Org2 Resource"}),
5695                if_version: None,
5696            }],
5697            preconditions: vec![],
5698            fences: vec![],
5699        };
5700
5701        let _ = execute_transaction(&db, &schema, &insert_in_org2, None);
5702
5703        // Now user:alice (member of org-1 only) tries to modify org-2 resource
5704        // Without state:write capability (which would only be granted for org-1),
5705        // the mutation is denied
5706        let alice_context = AuthorizationContext {
5707            subject: "user:alice".into(),
5708            tenant_id: "tenant".into(),
5709            application_id: "app".into(),
5710            revision_id: "rev".into(),
5711            capabilities: BTreeSet::new(), // No state:write for org-2
5712            readable_collections: Default::default(),
5713            writable_collections: Default::default(),
5714            field_projections: Default::default(),
5715        };
5716
5717        let cross_org_mutation = TransactionRequest {
5718            transaction_id: None,
5719            tenant_id: "tenant".into(),
5720            application_id: "app".into(),
5721            revision_id: "rev".into(),
5722            schema_version: 1,
5723            state_namespace: None,
5724            causal_parent: None,
5725            authorization: alice_context,
5726            operations: vec![TransactionOperation {
5727                kind: TransactionOperationKind::Update,
5728                collection: "incidents".into(),
5729                id: "inc-org2-1".into(),
5730                value: serde_json::json!({"title": "Hacked by alice"}),
5731                if_version: Some(0),
5732            }],
5733            preconditions: vec![],
5734            fences: vec![],
5735        };
5736
5737        let result = execute_transaction(&db, &schema, &cross_org_mutation, None);
5738        assert!(
5739            result.is_err(),
5740            "Cross-org mutation must be denied (member of org-1 only)"
5741        );
5742        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5743    }
5744
5745    #[test]
5746    fn record_authorization_owner_policy_filters_owned_records() {
5747        let db = db("record_auth_owner_filter");
5748        let schema = schema_with_owner();
5749
5750        let alice_context = AuthorizationContext {
5751            subject: "user:alice".into(),
5752            tenant_id: "tenant".into(),
5753            application_id: "app".into(),
5754            revision_id: "rev".into(),
5755            capabilities: ["state:read".into()].into(),
5756            readable_collections: Default::default(),
5757            writable_collections: Default::default(),
5758            field_projections: Default::default(),
5759        };
5760
5761        let bob_context = AuthorizationContext {
5762            subject: "user:bob".into(),
5763            tenant_id: "tenant".into(),
5764            application_id: "app".into(),
5765            revision_id: "rev".into(),
5766            capabilities: ["state:read".into()].into(),
5767            readable_collections: Default::default(),
5768            writable_collections: Default::default(),
5769            field_projections: Default::default(),
5770        };
5771
5772        let admin_context = AuthorizationContext {
5773            subject: "user:admin".into(),
5774            tenant_id: "tenant".into(),
5775            application_id: "app".into(),
5776            revision_id: "rev".into(),
5777            capabilities: ["state:write".into(), "state:read".into()].into(),
5778            readable_collections: Default::default(),
5779            writable_collections: Default::default(),
5780            field_projections: Default::default(),
5781        };
5782
5783        let insert_records = TransactionRequest {
5784            transaction_id: None,
5785            tenant_id: "tenant".into(),
5786            application_id: "app".into(),
5787            revision_id: "rev".into(),
5788            schema_version: 1,
5789            state_namespace: None,
5790            causal_parent: None,
5791            authorization: admin_context.clone(),
5792            operations: vec![
5793                TransactionOperation {
5794                    kind: TransactionOperationKind::Insert,
5795                    collection: "incidents".into(),
5796                    id: "inc-alice-1".into(),
5797                    value: serde_json::json!({"title": "Alice 1", "owner_id": "alice"}),
5798                    if_version: None,
5799                },
5800                TransactionOperation {
5801                    kind: TransactionOperationKind::Insert,
5802                    collection: "incidents".into(),
5803                    id: "inc-bob-1".into(),
5804                    value: serde_json::json!({"title": "Bob 1", "owner_id": "bob"}),
5805                    if_version: None,
5806                },
5807                TransactionOperation {
5808                    kind: TransactionOperationKind::Insert,
5809                    collection: "incidents".into(),
5810                    id: "inc-alice-2".into(),
5811                    value: serde_json::json!({"title": "Alice 2", "owner_id": "alice"}),
5812                    if_version: None,
5813                },
5814            ],
5815            preconditions: vec![],
5816            fences: vec![],
5817        };
5818
5819        execute_transaction(&db, &schema, &insert_records, None).expect("insert should succeed");
5820
5821        let alice_read = begin_read(&db, &schema, "app", alice_context).unwrap();
5822        let query = CanonicalQuery {
5823            collection: "incidents".into(),
5824            filter: None,
5825            order_by: vec![],
5826            limit: None,
5827            offset: 0,
5828            cursor: None,
5829            projection: vec!["title".into(), "owner_id".into()],
5830            group_by: vec![],
5831            aggregates: vec![],
5832            references: vec![],
5833            semantic: None,
5834        };
5835
5836        let alice_result = execute_query(
5837            &db,
5838            &schema,
5839            &alice_read,
5840            &query,
5841            Some(PolicySubject::Owner),
5842        )
5843        .expect("query should succeed");
5844
5845        assert_eq!(
5846            alice_result.records.len(),
5847            2,
5848            "Alice should see only 2 incidents she owns"
5849        );
5850        for record in &alice_result.records {
5851            assert_eq!(record.get("owner_id").unwrap(), "alice");
5852        }
5853
5854        let bob_read = begin_read(&db, &schema, "app", bob_context).unwrap();
5855        let bob_result = execute_query(&db, &schema, &bob_read, &query, Some(PolicySubject::Owner))
5856            .expect("query should succeed");
5857
5858        assert_eq!(
5859            bob_result.records.len(),
5860            1,
5861            "Bob should see only 1 incident he owns"
5862        );
5863        assert_eq!(bob_result.records[0].get("owner_id").unwrap(), "bob");
5864    }
5865
5866    #[test]
5867    fn record_authorization_owner_policy_denies_direct_access() {
5868        let db = db("record_auth_owner_direct");
5869        let schema = schema_with_owner();
5870
5871        let admin_context = AuthorizationContext {
5872            subject: "user:admin".into(),
5873            tenant_id: "tenant".into(),
5874            application_id: "app".into(),
5875            revision_id: "rev".into(),
5876            capabilities: ["state:write".into(), "state:read".into()].into(),
5877            readable_collections: Default::default(),
5878            writable_collections: Default::default(),
5879            field_projections: Default::default(),
5880        };
5881
5882        let bob_context = AuthorizationContext {
5883            subject: "user:bob".into(),
5884            tenant_id: "tenant".into(),
5885            application_id: "app".into(),
5886            revision_id: "rev".into(),
5887            capabilities: ["state:read".into()].into(),
5888            readable_collections: Default::default(),
5889            writable_collections: Default::default(),
5890            field_projections: Default::default(),
5891        };
5892
5893        let insert_request = TransactionRequest {
5894            transaction_id: None,
5895            tenant_id: "tenant".into(),
5896            application_id: "app".into(),
5897            revision_id: "rev".into(),
5898            schema_version: 1,
5899            state_namespace: None,
5900            causal_parent: None,
5901            authorization: admin_context,
5902            operations: vec![TransactionOperation {
5903                kind: TransactionOperationKind::Insert,
5904                collection: "incidents".into(),
5905                id: "inc-alice-secret".into(),
5906                value: serde_json::json!({"title": "Secret", "owner_id": "alice"}),
5907                if_version: None,
5908            }],
5909            preconditions: vec![],
5910            fences: vec![],
5911        };
5912
5913        execute_transaction(&db, &schema, &insert_request, None).expect("insert should succeed");
5914
5915        let bob_read = begin_read(&db, &schema, "app", bob_context).unwrap();
5916        let direct_query = CanonicalQuery {
5917            collection: "incidents".into(),
5918            filter: Some(QueryFilter::Eq {
5919                field: "_id".into(),
5920                value: Value::String("inc-alice-secret".into()),
5921            }),
5922            order_by: vec![],
5923            limit: None,
5924            offset: 0,
5925            cursor: None,
5926            projection: vec!["title".into(), "owner_id".into()],
5927            group_by: vec![],
5928            aggregates: vec![],
5929            references: vec![],
5930            semantic: None,
5931        };
5932
5933        let result = execute_query(
5934            &db,
5935            &schema,
5936            &bob_read,
5937            &direct_query,
5938            Some(PolicySubject::Owner),
5939        )
5940        .expect("query should succeed");
5941
5942        assert_eq!(
5943            result.records.len(),
5944            0,
5945            "Bob should not see Alice's record even with direct _id filter"
5946        );
5947    }
5948
5949    #[test]
5950    fn record_authorization_authenticated_policy_allows_all() {
5951        let db = db("record_auth_authenticated");
5952        let schema = schema();
5953
5954        let alice_context = AuthorizationContext {
5955            subject: "user:alice".into(),
5956            tenant_id: "tenant".into(),
5957            application_id: "app".into(),
5958            revision_id: "rev".into(),
5959            capabilities: ["state:write".into(), "state:read".into()].into(),
5960            readable_collections: Default::default(),
5961            writable_collections: Default::default(),
5962            field_projections: Default::default(),
5963        };
5964
5965        let bob_context = AuthorizationContext {
5966            subject: "user:bob".into(),
5967            tenant_id: "tenant".into(),
5968            application_id: "app".into(),
5969            revision_id: "rev".into(),
5970            capabilities: ["state:read".into()].into(),
5971            readable_collections: Default::default(),
5972            writable_collections: Default::default(),
5973            field_projections: Default::default(),
5974        };
5975
5976        let insert_records = TransactionRequest {
5977            transaction_id: None,
5978            tenant_id: "tenant".into(),
5979            application_id: "app".into(),
5980            revision_id: "rev".into(),
5981            schema_version: 1,
5982            state_namespace: None,
5983            causal_parent: None,
5984            authorization: alice_context,
5985            operations: vec![
5986                TransactionOperation {
5987                    kind: TransactionOperationKind::Insert,
5988                    collection: "incidents".into(),
5989                    id: "inc-alice-auth".into(),
5990                    value: serde_json::json!({"title": "Alice's incident"}),
5991                    if_version: None,
5992                },
5993                TransactionOperation {
5994                    kind: TransactionOperationKind::Insert,
5995                    collection: "incidents".into(),
5996                    id: "inc-bob-auth".into(),
5997                    value: serde_json::json!({"title": "Bob's incident"}),
5998                    if_version: None,
5999                },
6000            ],
6001            preconditions: vec![],
6002            fences: vec![],
6003        };
6004
6005        execute_transaction(&db, &schema, &insert_records, None).expect("insert should succeed");
6006
6007        let bob_read = begin_read(&db, &schema, "app", bob_context).unwrap();
6008        let query = CanonicalQuery {
6009            collection: "incidents".into(),
6010            filter: None,
6011            order_by: vec![],
6012            limit: None,
6013            offset: 0,
6014            cursor: None,
6015            projection: vec!["title".into()],
6016            group_by: vec![],
6017            aggregates: vec![],
6018            references: vec![],
6019            semantic: None,
6020        };
6021
6022        let result = execute_query(
6023            &db,
6024            &schema,
6025            &bob_read,
6026            &query,
6027            Some(PolicySubject::Authenticated),
6028        )
6029        .expect("query should succeed");
6030
6031        assert_eq!(
6032            result.records.len(),
6033            2,
6034            "Authenticated policy should allow access to all records for authenticated actor"
6035        );
6036    }
6037
6038    #[test]
6039    fn record_authorization_pagination_respects_authorization() {
6040        let db = db("record_auth_pagination");
6041        let schema = schema_with_owner();
6042
6043        let admin_context = AuthorizationContext {
6044            subject: "user:admin".into(),
6045            tenant_id: "tenant".into(),
6046            application_id: "app".into(),
6047            revision_id: "rev".into(),
6048            capabilities: ["state:write".into(), "state:read".into()].into(),
6049            readable_collections: Default::default(),
6050            writable_collections: Default::default(),
6051            field_projections: Default::default(),
6052        };
6053
6054        let alice_context = AuthorizationContext {
6055            subject: "user:alice".into(),
6056            tenant_id: "tenant".into(),
6057            application_id: "app".into(),
6058            revision_id: "rev".into(),
6059            capabilities: ["state:read".into()].into(),
6060            readable_collections: Default::default(),
6061            writable_collections: Default::default(),
6062            field_projections: Default::default(),
6063        };
6064
6065        let mut ops = vec![];
6066        for i in 0..5 {
6067            ops.push(TransactionOperation {
6068                kind: TransactionOperationKind::Insert,
6069                collection: "incidents".into(),
6070                id: format!("incident-{}", i),
6071                value: serde_json::json!({
6072                    "title": format!("Incident {}", i),
6073                    "owner_id": if i % 2 == 0 { "alice" } else { "bob" }
6074                }),
6075                if_version: None,
6076            });
6077        }
6078
6079        let insert_request = TransactionRequest {
6080            transaction_id: None,
6081            tenant_id: "tenant".into(),
6082            application_id: "app".into(),
6083            revision_id: "rev".into(),
6084            schema_version: 1,
6085            state_namespace: None,
6086            causal_parent: None,
6087            authorization: admin_context,
6088            operations: ops,
6089            preconditions: vec![],
6090            fences: vec![],
6091        };
6092
6093        execute_transaction(&db, &schema, &insert_request, None).expect("insert should succeed");
6094
6095        let alice_read = begin_read(&db, &schema, "app", alice_context).unwrap();
6096        let query_no_limit = CanonicalQuery {
6097            collection: "incidents".into(),
6098            filter: None,
6099            order_by: vec![QueryOrder {
6100                field: "_id".into(),
6101                direction: SortDirection::Asc,
6102            }],
6103            limit: None,
6104            offset: 0,
6105            cursor: None,
6106            projection: vec!["title".into(), "owner_id".into()],
6107            group_by: vec![],
6108            aggregates: vec![],
6109            references: vec![],
6110            semantic: None,
6111        };
6112
6113        let result = execute_query(
6114            &db,
6115            &schema,
6116            &alice_read,
6117            &query_no_limit,
6118            Some(PolicySubject::Owner),
6119        )
6120        .expect("query should succeed");
6121
6122        assert_eq!(
6123            result.records.len(),
6124            3,
6125            "Alice should see 3 incidents she owns (0, 2, 4)"
6126        );
6127        for record in &result.records {
6128            assert_eq!(record.get("owner_id").unwrap(), "alice");
6129        }
6130    }
6131}