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    Insert,
1927    Update,
1928    Delete,
1929}
1930#[derive(Debug, Clone, Serialize, Deserialize)]
1931pub struct TransactionOperation {
1932    pub kind: TransactionOperationKind,
1933    pub collection: String,
1934    pub id: String,
1935    #[serde(default)]
1936    pub value: Value,
1937    #[serde(default)]
1938    pub if_version: Option<u64>,
1939}
1940#[derive(Debug, Clone, Serialize, Deserialize)]
1941#[serde(tag = "kind", rename_all = "snake_case")]
1942pub enum TransactionPrecondition {
1943    CollectionVersion {
1944        collection: String,
1945        version: u64,
1946    },
1947    StateVersion {
1948        version: u64,
1949    },
1950    ApplicationRevision {
1951        revision_id: String,
1952    },
1953    CausalCursor {
1954        cursor: BTreeMap<String, u64>,
1955    },
1956    /// What must be true of one record for the transaction to commit.
1957    ///
1958    /// Distinct from every variant above, and evaluated in a different place
1959    /// for a reason: those compare counters that can be read before the commit
1960    /// lock is taken, while this one compares the fields a caller reads back —
1961    /// the document's `__version`, its authority epoch, its lease — and must
1962    /// therefore be evaluated *inside* the same lock as the writes. Checking it
1963    /// against the pre-commit snapshot would be a client-side check wearing the
1964    /// authority's name: between the read and the write, another writer
1965    /// commits.
1966    ///
1967    /// It is also what makes `putIfAbsent` and `updateIfVersion` mean the same
1968    /// thing against a managed authority as against the file runtime. Mapping
1969    /// a caller's `expectedVersion` onto `if_version` would not: that fences
1970    /// the row's internal storage sequence, which is a different number that
1971    /// happens to share a name.
1972    Record {
1973        collection: String,
1974        id: String,
1975        #[serde(default)]
1976        require_absent: bool,
1977        #[serde(default)]
1978        expected_version: Option<u64>,
1979        #[serde(default)]
1980        expected_epoch: Option<u64>,
1981        #[serde(default)]
1982        expected_lease_id: Option<String>,
1983    },
1984}
1985#[derive(Debug, Clone, Serialize, Deserialize)]
1986pub struct TransactionRequest {
1987    #[serde(default)]
1988    pub transaction_id: Option<String>,
1989    pub tenant_id: String,
1990    pub application_id: String,
1991    pub revision_id: String,
1992    pub schema_version: u64,
1993    #[serde(default)]
1994    pub state_namespace: Option<String>,
1995    #[serde(default)]
1996    /// Optimistic authority revision. A mismatched revision is rejected.
1997    #[serde(rename = "base_revision")]
1998    pub causal_parent: Option<u64>,
1999    pub authorization: AuthorizationContext,
2000    pub operations: Vec<TransactionOperation>,
2001    #[serde(default)]
2002    pub preconditions: Vec<TransactionPrecondition>,
2003    /// Read bases this authority issued for this state namespace
2004    /// (`capture_read_basis`), re-evaluated inside the commit lock.
2005    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2006    pub fences: Vec<crate::ReadBasis>,
2007}
2008
2009/// Collections the composition substrate keeps inside an application's state
2010/// namespace. They are not part of the application contract: their records
2011/// are validated by the substrate, and reading or writing them requires the
2012/// dedicated `composition:read` / `composition:write` capabilities rather
2013/// than `state:*`. Every other `_feltdb` name remains unwritable here.
2014pub const SUBSTRATE_COLLECTIONS: &[&str] = &[
2015    "_feltdb.CompositionContract",
2016    "_feltdb.Composition",
2017    "_feltdb.CompositionVersion",
2018    "_feltdb.CompositionTopology",
2019    "_feltdb.StateOwnership",
2020    "_feltdb.StateDependency",
2021    "_feltdb.StateChange",
2022    "_feltdb.StateAttribution",
2023    "_feltdb.StateObservation",
2024    "_feltdb.StateProposal",
2025    "_feltdb.StateImpact",
2026    "_feltdb.Intent",
2027    "_feltdb.IntentEvent",
2028    "_feltdb.ProposalEvent",
2029];
2030
2031pub fn is_substrate_collection(name: &str) -> bool {
2032    SUBSTRATE_COLLECTIONS.contains(&name)
2033}
2034
2035/// Capture a read basis over an application state namespace.
2036///
2037/// Every collection the read set names must be one the caller may read in
2038/// full: an application collection readable under `state:read` whose read
2039/// policy does not depend on the record, or a substrate collection under
2040/// `composition:read`. A token is a digest over every row it covers, so a
2041/// basis over rows the caller could not read would disclose them; such a
2042/// read set is refused, never digested.
2043pub fn capture_read_basis(
2044    db: &FeltDb,
2045    schema: &StateSchema,
2046    state_namespace: &str,
2047    reads: &crate::ReadSet,
2048    readable: &dyn Fn(&str) -> bool,
2049) -> Result<crate::ReadBasis, StateFailure> {
2050    let reads = reads
2051        .normalised()
2052        .map_err(|error| StateFailure::new("VALIDATION_FAILED", &error.to_string()))?;
2053    authorize_read_set(schema, &reads, readable)?;
2054    db.read_basis_in(
2055        &reads,
2056        &crate::ReadLayout::Namespace(state_namespace.to_string()),
2057    )
2058    .map_err(StateFailure::storage)
2059}
2060
2061/// The refusal for a read basis over a collection the caller cannot read in full.
2062pub fn read_basis_denied(collection: &str) -> StateFailure {
2063    let mut failure = StateFailure::new(
2064        "AUTHORIZATION_DENIED",
2065        "a read basis may only cover collections the caller can read in full",
2066    );
2067    failure.resource = Some(collection.to_string());
2068    failure
2069}
2070
2071fn authorize_read_set(
2072    schema: &StateSchema,
2073    reads: &crate::ReadSet,
2074    readable: &dyn Fn(&str) -> bool,
2075) -> Result<(), StateFailure> {
2076    let collections = reads
2077        .records
2078        .iter()
2079        .map(|record| record.collection.as_str())
2080        .chain(
2081            reads
2082                .predicates
2083                .iter()
2084                .map(|predicate| predicate.collection.as_str()),
2085        );
2086    for name in collections {
2087        if !is_substrate_collection(name) {
2088            collection(schema, name)?;
2089        }
2090        if !readable(name) {
2091            return Err(read_basis_denied(name));
2092        }
2093    }
2094    Ok(())
2095}
2096#[derive(Debug, Clone, Serialize, Deserialize)]
2097pub struct TransactionAudit {
2098    pub transaction_id: String,
2099    pub subject: String,
2100    pub tenant: String,
2101    pub application: String,
2102    pub revision: String,
2103    pub schema_version: u64,
2104    pub state_before: u64,
2105    pub state_after: u64,
2106    pub operations: usize,
2107    pub authorization_decisions: Vec<String>,
2108    pub causal_parent: Option<u64>,
2109    pub result: String,
2110}
2111#[derive(Debug, Clone, Serialize, Deserialize)]
2112pub struct TransactionResult {
2113    pub transaction_id: String,
2114    pub base_revision: u64,
2115    pub commit_revision: u64,
2116    pub status: crate::TransactionStatus,
2117    pub state_before: u64,
2118    pub state_after: u64,
2119    pub causal_cursor: BTreeMap<String, u64>,
2120    pub duplicate: bool,
2121    pub audit: TransactionAudit,
2122}
2123
2124fn valid_type(value: &Value, field: &FieldSchema) -> bool {
2125    if value.is_null() {
2126        return field.nullable;
2127    }
2128    match &field.field_type {
2129        FieldType::Primitive { primitive } => match primitive {
2130            PrimitiveType::String => value.is_string(),
2131            PrimitiveType::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
2132            PrimitiveType::Number => value.is_number(),
2133            PrimitiveType::Boolean => value.is_boolean(),
2134            PrimitiveType::Json => true,
2135            PrimitiveType::Timestamp => value.as_str().is_some_and(valid_timestamp),
2136            PrimitiveType::Date => value.as_str().is_some_and(valid_date),
2137            PrimitiveType::Time => value.as_str().is_some_and(valid_time),
2138            PrimitiveType::Uuid => value.as_str().is_some_and(valid_uuid),
2139            PrimitiveType::Decimal => value.as_str().is_some_and(valid_decimal),
2140            PrimitiveType::Bigint => value.as_str().is_some_and(valid_integer_string),
2141            PrimitiveType::Email => value.as_str().is_some_and(valid_email),
2142            PrimitiveType::Url => value.as_str().is_some_and(valid_url),
2143            PrimitiveType::Phone => value.as_str().is_some_and(valid_phone),
2144            PrimitiveType::Binary => value.as_str().is_some_and(valid_base64),
2145            PrimitiveType::Money => valid_money(value),
2146            PrimitiveType::File => valid_file(value),
2147            PrimitiveType::GeoPoint => valid_geo(value),
2148            PrimitiveType::Object => value.is_object(),
2149        },
2150        FieldType::Enum { values } => value
2151            .as_str()
2152            .is_some_and(|v| values.iter().any(|x| x == v)),
2153        FieldType::Reference { .. } => value.is_string(),
2154        FieldType::Array { items } => value.as_array().is_some_and(|values| {
2155            values.iter().all(|value| {
2156                valid_type(
2157                    value,
2158                    &FieldSchema {
2159                        name: "item".into(),
2160                        field_type: (**items).clone(),
2161                        nullable: false,
2162                        required: true,
2163                        default: None,
2164                        constraints: Default::default(),
2165                        computed: None,
2166                    },
2167                )
2168            })
2169        }),
2170        FieldType::Map { values } => value.as_object().is_some_and(|entries| {
2171            entries
2172                .values()
2173                .all(|value| valid_nested_type(value, values))
2174        }),
2175        FieldType::Vector { dimensions } => value.as_array().is_some_and(|values| {
2176            dimensions.is_none_or(|expected| values.len() == expected)
2177                && values.iter().all(Value::is_number)
2178        }),
2179    }
2180}
2181
2182fn valid_nested_type(value: &Value, field_type: &FieldType) -> bool {
2183    valid_type(
2184        value,
2185        &FieldSchema {
2186            name: "value".into(),
2187            field_type: field_type.clone(),
2188            nullable: false,
2189            required: true,
2190            default: None,
2191            constraints: Default::default(),
2192            computed: None,
2193        },
2194    )
2195}
2196fn valid_integer_string(value: &str) -> bool {
2197    !value.is_empty()
2198        && value
2199            .strip_prefix('-')
2200            .unwrap_or(value)
2201            .chars()
2202            .all(|c| c.is_ascii_digit())
2203}
2204fn valid_decimal(value: &str) -> bool {
2205    let unsigned = value.strip_prefix('-').unwrap_or(value);
2206    let mut parts = unsigned.split('.');
2207    parts
2208        .next()
2209        .is_some_and(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
2210        && parts
2211            .next()
2212            .is_none_or(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
2213        && parts.next().is_none()
2214}
2215fn valid_date(value: &str) -> bool {
2216    value.len() == 10
2217        && value.as_bytes()[4] == b'-'
2218        && value.as_bytes()[7] == b'-'
2219        && value[..4].parse::<u16>().is_ok()
2220        && value[5..7]
2221            .parse::<u8>()
2222            .is_ok_and(|v| (1..=12).contains(&v))
2223        && value[8..]
2224            .parse::<u8>()
2225            .is_ok_and(|v| (1..=31).contains(&v))
2226}
2227fn valid_time(value: &str) -> bool {
2228    value.len() >= 5
2229        && value.as_bytes().get(2) == Some(&b':')
2230        && value[..2].parse::<u8>().is_ok_and(|v| v < 24)
2231        && value[3..5].parse::<u8>().is_ok_and(|v| v < 60)
2232        && (value.len() == 5
2233            || value.as_bytes().get(5) == Some(&b':')
2234                && value
2235                    .get(6..8)
2236                    .is_some_and(|v| v.parse::<u8>().is_ok_and(|s| s < 60)))
2237}
2238fn valid_timestamp(value: &str) -> bool {
2239    value.get(..10).is_some_and(valid_date)
2240        && value
2241            .as_bytes()
2242            .get(10)
2243            .is_some_and(|v| *v == b'T' || *v == b' ')
2244        && value.get(11..).is_some_and(valid_time)
2245}
2246fn valid_uuid(value: &str) -> bool {
2247    value.len() == 36
2248        && value.chars().enumerate().all(|(i, c)| {
2249            matches!(i, 8 | 13 | 18 | 23) && c == '-'
2250                || !matches!(i, 8 | 13 | 18 | 23) && c.is_ascii_hexdigit()
2251        })
2252}
2253fn valid_email(value: &str) -> bool {
2254    let mut parts = value.split('@');
2255    parts.next().is_some_and(|v| !v.is_empty())
2256        && parts
2257            .next()
2258            .is_some_and(|v| v.contains('.') && !v.starts_with('.') && !v.ends_with('.'))
2259        && parts.next().is_none()
2260}
2261fn valid_url(value: &str) -> bool {
2262    (value.starts_with("https://") || value.starts_with("http://"))
2263        && value.split("//").nth(1).is_some_and(|v| !v.is_empty())
2264}
2265fn valid_phone(value: &str) -> bool {
2266    let digits = value.chars().filter(|c| c.is_ascii_digit()).count();
2267    (7..=15).contains(&digits)
2268        && value
2269            .chars()
2270            .all(|c| c.is_ascii_digit() || "+-(). ".contains(c))
2271}
2272fn valid_base64(value: &str) -> bool {
2273    !value.is_empty()
2274        && value.len() % 4 == 0
2275        && value
2276            .chars()
2277            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '='))
2278}
2279fn valid_money(value: &Value) -> bool {
2280    value.as_object().is_some_and(|v| {
2281        v.get("amount")
2282            .and_then(Value::as_str)
2283            .is_some_and(valid_decimal)
2284            && v.get("currency")
2285                .and_then(Value::as_str)
2286                .is_some_and(|c| c.len() == 3 && c.chars().all(|x| x.is_ascii_uppercase()))
2287    })
2288}
2289fn valid_file(value: &Value) -> bool {
2290    value.as_object().is_some_and(|v| {
2291        v.get("name").is_some_and(Value::is_string)
2292            && v.get("type").is_some_and(Value::is_string)
2293            && v.get("size").and_then(Value::as_u64).is_some()
2294            && (v.get("url").is_some_and(Value::is_string)
2295                || v.get("ref").is_some_and(Value::is_string))
2296    })
2297}
2298fn valid_geo(value: &Value) -> bool {
2299    value.as_object().is_some_and(|v| {
2300        v.get("latitude")
2301            .and_then(Value::as_f64)
2302            .is_some_and(|x| (-90.0..=90.0).contains(&x))
2303            && v.get("longitude")
2304                .and_then(Value::as_f64)
2305                .is_some_and(|x| (-180.0..=180.0).contains(&x))
2306    })
2307}
2308fn validate_record(collection: &CollectionSchema, value: &mut Value) -> Result<(), StateFailure> {
2309    let object = value
2310        .as_object_mut()
2311        .ok_or_else(|| StateFailure::new("VALIDATION_FAILED", "record must be an object"))?;
2312    for field in &collection.fields {
2313        if let Some(expression) = &field.computed {
2314            let computed = if let Some(source) = expression.strip_prefix("copy:") {
2315                object.get(source.trim()).cloned().unwrap_or(Value::Null)
2316            } else if let Some(sources) = expression.strip_prefix("concat:") {
2317                Value::String(
2318                    sources
2319                        .split(',')
2320                        .filter_map(|source| object.get(source.trim()).and_then(Value::as_str))
2321                        .collect::<String>(),
2322                )
2323            } else {
2324                return Err(StateFailure::new(
2325                    "VALIDATION_FAILED",
2326                    "unsupported computed expression",
2327                ));
2328            };
2329            object.insert(field.name.clone(), computed);
2330        }
2331    }
2332    for field in &collection.fields {
2333        if !object.contains_key(&field.name) {
2334            if let Some(default) = &field.default {
2335                object.insert(field.name.clone(), default.clone());
2336            } else if field.required && field.computed.is_none() {
2337                return Err(StateFailure::new(
2338                    "VALIDATION_FAILED",
2339                    &format!("missing required field {}", field.name),
2340                ));
2341            }
2342        }
2343        if let Some(value) = object.get(&field.name) {
2344            if !valid_type(value, field) {
2345                return Err(StateFailure::new(
2346                    "VALIDATION_FAILED",
2347                    &format!("invalid value for {}", field.name),
2348                ));
2349            }
2350            if let Some(text) = value.as_str() {
2351                if field.constraints.min_length.is_some_and(|n| text.len() < n)
2352                    || field.constraints.max_length.is_some_and(|n| text.len() > n)
2353                {
2354                    return Err(StateFailure::new(
2355                        "VALIDATION_FAILED",
2356                        &format!("length constraint failed for {}", field.name),
2357                    ));
2358                }
2359            }
2360            if let Some(number) = value.as_f64() {
2361                if field.constraints.minimum.is_some_and(|n| number < n)
2362                    || field.constraints.maximum.is_some_and(|n| number > n)
2363                {
2364                    return Err(StateFailure::new(
2365                        "VALIDATION_FAILED",
2366                        &format!("numeric constraint failed for {}", field.name),
2367                    ));
2368                }
2369            }
2370        }
2371    }
2372    // `__version` and `id` are record identity, carried by every client
2373    // record whether or not the contract declares them; a declared `id` is
2374    // validated by its declaration above.
2375    if object.keys().any(|name| {
2376        name != "__version" && name != "id" && !collection.fields.iter().any(|f| &f.name == name)
2377    }) {
2378        return Err(StateFailure::new(
2379            "VALIDATION_FAILED",
2380            "record contains an unknown field",
2381        ));
2382    }
2383    Ok(())
2384}
2385pub fn execute_transaction(
2386    db: &FeltDb,
2387    schema: &StateSchema,
2388    request: &TransactionRequest,
2389    write_policy: Option<PolicySubject>,
2390) -> Result<TransactionResult, StateFailure> {
2391    execute_transaction_with_policies(db, schema, request, write_policy.as_ref(), None)
2392}
2393pub fn execute_transaction_with_collection_policies(
2394    db: &FeltDb,
2395    schema: &StateSchema,
2396    request: &TransactionRequest,
2397    write_policies: &BTreeMap<String, PolicySubject>,
2398) -> Result<TransactionResult, StateFailure> {
2399    execute_transaction_with_policies(db, schema, request, None, Some(write_policies))
2400}
2401fn execute_transaction_with_policies(
2402    db: &FeltDb,
2403    schema: &StateSchema,
2404    request: &TransactionRequest,
2405    uniform_write_policy: Option<&PolicySubject>,
2406    collection_write_policies: Option<&BTreeMap<String, PolicySubject>>,
2407) -> Result<TransactionResult, StateFailure> {
2408    if request.application_id != schema.application_id
2409        || request.revision_id != schema.revision_id
2410        || request.schema_version != schema.schema_version
2411    {
2412        return Err(StateFailure::new(
2413            "SCHEMA_MISMATCH",
2414            "transaction does not match state schema",
2415        ));
2416    }
2417    if request.authorization.subject.is_empty()
2418        || request.authorization.tenant_id != request.tenant_id
2419        || request.authorization.application_id != request.application_id
2420        || request.authorization.revision_id != request.revision_id
2421    {
2422        return Err(StateFailure::new(
2423            "AUTHORIZATION_DENIED",
2424            "transaction authorization context is invalid",
2425        ));
2426    }
2427    if !request.authorization.can("state:write") {
2428        return Err(StateFailure::new(
2429            "AUTHORIZATION_DENIED",
2430            "state:write is required",
2431        ));
2432    }
2433    let canonical = if request.fences.is_empty() {
2434        serde_json::to_vec(&(
2435            request.tenant_id.as_str(),
2436            request.application_id.as_str(),
2437            request.revision_id.as_str(),
2438            request.schema_version,
2439            request.state_namespace.as_deref(),
2440            request.causal_parent,
2441            &request.authorization.subject,
2442            &request.preconditions,
2443            &request.operations,
2444        ))
2445    } else {
2446        // Fences are part of what the transaction means, so they bind its
2447        // identity. Unfenced transactions hash exactly as they always have.
2448        serde_json::to_vec(&(
2449            request.tenant_id.as_str(),
2450            request.application_id.as_str(),
2451            request.revision_id.as_str(),
2452            request.schema_version,
2453            request.state_namespace.as_deref(),
2454            request.causal_parent,
2455            &request.authorization.subject,
2456            &request.preconditions,
2457            &request.operations,
2458            &request.fences,
2459        ))
2460    }
2461    .map_err(StateFailure::storage)?;
2462    let transaction_id = request
2463        .transaction_id
2464        .clone()
2465        .unwrap_or_else(|| format!("tx_{:x}", Sha256::digest(&canonical)));
2466    // Client transaction identities are application-local. The managed store
2467    // serves many applications from one durable log, so using the bare client
2468    // id as the substrate id lets one application collide with another.
2469    // Hash the authorization boundary into the private storage identity while
2470    // preserving the caller's id in every public result and audit record.
2471    let storage_transaction_id = format!(
2472        "txscope_{:x}_{}",
2473        Sha256::digest(
2474            format!(
2475                "{}\0{}\0{}",
2476                request.tenant_id,
2477                request.application_id,
2478                request
2479                    .state_namespace
2480                    .as_deref()
2481                    .unwrap_or(&request.application_id)
2482            )
2483            .as_bytes()
2484        ),
2485        transaction_id
2486    );
2487    let payload_hash = format!("sha256:{:x}", Sha256::digest(&canonical));
2488    // A retried id is answered before anything is evaluated against current
2489    // state, with the original commit: its revisions and positions, not the
2490    // store's current ones. The same answer the commit lock gives a retry that
2491    // races past this check.
2492    if let Some(result) = committed_result(
2493        db,
2494        request,
2495        &transaction_id,
2496        &storage_transaction_id,
2497        &payload_hash,
2498    )? {
2499        return Ok(result);
2500    }
2501    match commit_new_transaction(
2502        db,
2503        schema,
2504        request,
2505        uniform_write_policy,
2506        collection_write_policies,
2507        &transaction_id,
2508        &storage_transaction_id,
2509        &payload_hash,
2510    ) {
2511        Ok(result) => Ok(result),
2512        // The request was evaluated against state read before the commit
2513        // lock. An identical request that committed in between makes this one
2514        // its duplicate, and the duplicate -- the original commit -- is the
2515        // answer, not the refusal that later state produced (a create that
2516        // now "exists", a fence that now "changed").
2517        Err(failure) => match committed_result(
2518            db,
2519            request,
2520            &transaction_id,
2521            &storage_transaction_id,
2522            &payload_hash,
2523        )? {
2524            Some(result) => Ok(result),
2525            None => Err(failure),
2526        },
2527    }
2528}
2529
2530/// A retried id, answered with the original commit: its revisions and
2531/// positions, never the store's current ones. `IDEMPOTENCY_CONFLICT` when the id
2532/// committed with a different payload; `None` when it has not committed.
2533fn committed_result(
2534    db: &FeltDb,
2535    request: &TransactionRequest,
2536    transaction_id: &str,
2537    storage_transaction_id: &str,
2538    payload_hash: &str,
2539) -> Result<Option<TransactionResult>, StateFailure> {
2540    let Some(original) = db
2541        .committed_transaction(storage_transaction_id, Some(payload_hash))
2542        .map_err(|error| match error {
2543            crate::FlowError::TransactionIdPayloadMismatch { .. } => idempotency_conflict(),
2544            other => StateFailure::storage(other),
2545        })?
2546    else {
2547        return Ok(None);
2548    };
2549    let cursor = db
2550        .operation_versions()
2551        .map_err(StateFailure::storage)?
2552        .into_iter()
2553        .collect();
2554    let audit = TransactionAudit {
2555        transaction_id: transaction_id.to_string(),
2556        subject: request.authorization.subject.clone(),
2557        tenant: request.tenant_id.clone(),
2558        application: request.application_id.clone(),
2559        revision: request.revision_id.clone(),
2560        schema_version: request.schema_version,
2561        state_before: original.state_before,
2562        state_after: original.state_after,
2563        operations: request.operations.len(),
2564        authorization_decisions: vec!["state:write allowed".into()],
2565        causal_parent: request.causal_parent,
2566        result: "duplicate".into(),
2567    };
2568    Ok(Some(TransactionResult {
2569        transaction_id: transaction_id.to_string(),
2570        base_revision: original.base_revision,
2571        commit_revision: original.commit_revision,
2572        status: original.status,
2573        state_before: original.state_before,
2574        state_after: original.state_after,
2575        causal_cursor: cursor,
2576        duplicate: true,
2577        audit,
2578    }))
2579}
2580
2581/// Evaluate and commit a transaction id that has not committed.
2582#[allow(clippy::too_many_arguments)]
2583fn commit_new_transaction(
2584    db: &FeltDb,
2585    schema: &StateSchema,
2586    request: &TransactionRequest,
2587    uniform_write_policy: Option<&PolicySubject>,
2588    collection_write_policies: Option<&BTreeMap<String, PolicySubject>>,
2589    transaction_id: &str,
2590    storage_transaction_id: &str,
2591    payload_hash: &str,
2592) -> Result<TransactionResult, StateFailure> {
2593    let transaction_id = transaction_id.to_string();
2594    let storage_transaction_id = storage_transaction_id.to_string();
2595    let payload_hash = payload_hash.to_string();
2596    let namespace = request
2597        .state_namespace
2598        .as_deref()
2599        .unwrap_or(&request.application_id)
2600        .to_string();
2601    let snapshot = transaction_snapshot_rows(
2602        db,
2603        schema,
2604        request,
2605        uniform_write_policy,
2606        collection_write_policies,
2607    )?;
2608    // Record preconditions are collected here and evaluated by the store,
2609    // inside the same lock as the writes. Evaluating them against `snapshot`
2610    // alongside the counters below would reintroduce exactly the gap they
2611    // exist to close.
2612    let mut record_preconditions: Vec<RecordPrecondition> = Vec::new();
2613    for condition in &request.preconditions {
2614        let (resource, expected, actual, passed) = match condition {
2615            TransactionPrecondition::Record {
2616                collection,
2617                id,
2618                require_absent,
2619                expected_version,
2620                expected_epoch,
2621                expected_lease_id,
2622            } => {
2623                let precondition = RecordPrecondition {
2624                    capability: format!("{namespace}:{collection}"),
2625                    key: id.clone(),
2626                    require_absent: *require_absent,
2627                    expected_version: *expected_version,
2628                    expected_epoch: *expected_epoch,
2629                    expected_lease_id: expected_lease_id.clone(),
2630                };
2631                if precondition.is_empty() {
2632                    // Refused rather than ignored: a precondition that
2633                    // constrains nothing reads as protection and provides none.
2634                    let mut failure = StateFailure::new(
2635                        "VALIDATION_FAILED",
2636                        &format!("precondition on {collection}:{id} constrains nothing"),
2637                    );
2638                    failure.resource = Some(format!("{collection}:{id}"));
2639                    return Err(failure);
2640                }
2641                record_preconditions.push(precondition);
2642                continue;
2643            }
2644            TransactionPrecondition::CollectionVersion {
2645                collection,
2646                version,
2647            } => {
2648                let capability = format!(
2649                    "{}:{}",
2650                    request
2651                        .state_namespace
2652                        .as_deref()
2653                        .unwrap_or(&request.application_id),
2654                    collection
2655                );
2656                let actual = snapshot
2657                    .iter()
2658                    .filter(|row| row.capability == capability)
2659                    .filter_map(|row| row.operation.as_ref().map(|op| op.sequence))
2660                    .max()
2661                    .unwrap_or(0);
2662                (
2663                    collection.clone(),
2664                    Value::from(*version),
2665                    Value::from(actual),
2666                    actual == *version,
2667                )
2668            }
2669            TransactionPrecondition::StateVersion { version } => {
2670                let actual = db.sequence().map_err(StateFailure::storage)?;
2671                (
2672                    "state".into(),
2673                    Value::from(*version),
2674                    Value::from(actual),
2675                    actual == *version,
2676                )
2677            }
2678            TransactionPrecondition::ApplicationRevision { revision_id } => (
2679                ("application_revision").into(),
2680                Value::String(revision_id.clone()),
2681                Value::String(schema.revision_id.clone()),
2682                revision_id == &schema.revision_id,
2683            ),
2684            TransactionPrecondition::CausalCursor { cursor } => {
2685                let actual: BTreeMap<_, _> = db
2686                    .operation_versions()
2687                    .map_err(StateFailure::storage)?
2688                    .into_iter()
2689                    .collect();
2690                (
2691                    "causal_cursor".into(),
2692                    serde_json::to_value(cursor).map_err(StateFailure::storage)?,
2693                    serde_json::to_value(&actual).map_err(StateFailure::storage)?,
2694                    cursor == &actual,
2695                )
2696            }
2697        };
2698        if !passed {
2699            let mut failure =
2700                StateFailure::new("PRECONDITION_FAILED", "transaction precondition failed");
2701            failure.resource = Some(resource);
2702            failure.expected = Some(expected);
2703            failure.actual = Some(actual);
2704            failure.transaction = Some(transaction_id.clone());
2705            failure.causal_position = db.sequence().ok();
2706            return Err(failure);
2707        }
2708    }
2709    let mut mutations = vec![];
2710    let mut preconditions = vec![];
2711    let mut overlay: BTreeMap<(String, String), Option<Value>> = BTreeMap::new();
2712    for operation in &request.operations {
2713        if !request.authorization.writable_collections.is_empty()
2714            && !request
2715                .authorization
2716                .writable_collections
2717                .contains(&operation.collection)
2718        {
2719            return Err(StateFailure::new(
2720                "AUTHORIZATION_DENIED",
2721                "collection is not writable",
2722            ));
2723        }
2724        // Substrate collections are validated by the substrate that owns them,
2725        // and written only under the dedicated capability.
2726        let substrate = is_substrate_collection(&operation.collection);
2727        if substrate && !request.authorization.can("composition:write") {
2728            let mut failure = StateFailure::new(
2729                "AUTHORIZATION_DENIED",
2730                "composition:write is required for substrate collections",
2731            );
2732            failure.resource = Some(operation.collection.clone());
2733            return Err(failure);
2734        }
2735        let definition = if substrate {
2736            None
2737        } else {
2738            Some(collection(schema, &operation.collection)?)
2739        };
2740        let capability = format!(
2741            "{}:{}",
2742            request
2743                .state_namespace
2744                .as_deref()
2745                .unwrap_or(&request.application_id),
2746            operation.collection
2747        );
2748        let key = operation.id.clone();
2749        let persisted = snapshot
2750            .iter()
2751            .find(|row| row.capability == capability && row.key == key && !row.deleted);
2752        let existing_value = overlay
2753            .get(&(capability.clone(), key.clone()))
2754            .cloned()
2755            .unwrap_or_else(|| persisted.map(|row| Some(row.value.clone())).unwrap_or(None));
2756
2757        // Per-operation record authorization
2758        let write_policy = collection_write_policies
2759            .and_then(|policies| policies.get(&operation.collection))
2760            .or(uniform_write_policy)
2761            .filter(|_| !substrate);
2762        if let Some(policy) = write_policy {
2763            let actor = if request.authorization.subject.is_empty()
2764                || request.authorization.subject == ":"
2765            {
2766                None
2767            } else {
2768                request
2769                    .authorization
2770                    .subject
2771                    .split(':')
2772                    .nth(1)
2773                    .map(|id| Actor::new(id))
2774            };
2775
2776            // Select authorization record based on operation type
2777            let auth_record_value = match operation.kind {
2778                TransactionOperationKind::Insert => {
2779                    // For create: authorize against proposed record
2780                    operation.value.clone()
2781                }
2782                TransactionOperationKind::Update | TransactionOperationKind::Delete => {
2783                    // For update/delete: authorize against EXISTING record (before changes)
2784                    // This prevents authorization bypass via ownership/org changes
2785                    existing_value
2786                        .clone()
2787                        .ok_or_else(|| StateFailure::new("CONFLICT", "record does not exist"))?
2788                }
2789            };
2790
2791            // Create authorization state for policies that need to verify relationships
2792            let auth_state = AuthorizationState::new(
2793                Arc::new(schema.clone()),
2794                request
2795                    .state_namespace
2796                    .as_deref()
2797                    .unwrap_or(&request.application_id)
2798                    .to_string(),
2799                Arc::new(snapshot.clone()),
2800            );
2801
2802            let record_context = RecordAuthorizationContext {
2803                actor,
2804                collection: operation.collection.clone(),
2805                record_id: key.clone(),
2806                record_value: auth_record_value,
2807                authorization_state: Some(auth_state),
2808            };
2809
2810            PolicyEvaluator::evaluate_record(policy.clone(), &record_context).map_err(|_| {
2811                StateFailure::new(
2812                    "AUTHORIZATION_DENIED",
2813                    &format!(
2814                        "record authorization failed for operation on {}:{}",
2815                        operation.collection, key
2816                    ),
2817                )
2818            })?;
2819        }
2820
2821        let value = match operation.kind {
2822            TransactionOperationKind::Insert => {
2823                if existing_value.is_some() {
2824                    // A create is conditional on absence: the same structured
2825                    // refusal a `requireAbsent` create gets on every route.
2826                    let refusal = crate::PreconditionFailure::Present {
2827                        collection: capability.clone(),
2828                        key: key.clone(),
2829                    };
2830                    let mut failure =
2831                        StateFailure::new("PRECONDITION_FAILED", &refusal.to_string());
2832                    failure.transaction = Some(transaction_id.clone());
2833                    failure.resource = Some(format!("{}:{}", operation.collection, key));
2834                    failure.failure =
2835                        Some(refusal.public_json(&crate::ReadLayout::Namespace(namespace.clone())));
2836                    return Err(failure);
2837                }
2838                let mut value = operation.value.clone();
2839                match definition {
2840                    Some(definition) => validate_record(definition, &mut value)?,
2841                    None if value.is_object() => {}
2842                    None => {
2843                        return Err(StateFailure::new(
2844                            "VALIDATION_FAILED",
2845                            "record must be an object",
2846                        ));
2847                    }
2848                }
2849                Some(value)
2850            }
2851            TransactionOperationKind::Update => {
2852                let current = existing_value
2853                    .ok_or_else(|| StateFailure::new("CONFLICT", "record does not exist"))?;
2854                let mut value = current;
2855                let patch = operation.value.as_object().ok_or_else(|| {
2856                    StateFailure::new("VALIDATION_FAILED", "update must be an object")
2857                })?;
2858                for (k, v) in patch {
2859                    value
2860                        .as_object_mut()
2861                        .expect("existing object")
2862                        .insert(k.clone(), v.clone());
2863                }
2864                if let Some(definition) = definition {
2865                    validate_record(definition, &mut value)?;
2866                }
2867                Some(value)
2868            }
2869            TransactionOperationKind::Delete => {
2870                if existing_value.is_none() {
2871                    return Err(StateFailure::new("CONFLICT", "record does not exist"));
2872                }
2873                None
2874            }
2875        };
2876        preconditions.push(AtomicPrecondition {
2877            capability: capability.clone(),
2878            key: key.clone(),
2879            expected_version: operation.if_version.or_else(|| {
2880                persisted
2881                    .and_then(|row| row.operation.as_ref())
2882                    .map(|op| op.sequence)
2883            }),
2884        });
2885        overlay.insert((capability.clone(), key.clone()), value.clone());
2886        mutations.push(AtomicMutation {
2887            capability,
2888            key,
2889            value,
2890        });
2891    }
2892    for definition in &schema.collections {
2893        let capability = format!(
2894            "{}:{}",
2895            request
2896                .state_namespace
2897                .as_deref()
2898                .unwrap_or(&request.application_id),
2899            definition.name
2900        );
2901        let mut referenced_records = BTreeMap::new();
2902        for ((cap, key), value) in overlay
2903            .iter()
2904            .filter(|((cap, _), value)| cap == &capability && value.is_some())
2905        {
2906            let value = value.as_ref().expect("filtered");
2907            for field in &definition.fields {
2908                if let FieldType::Reference { collection: target } = &field.field_type {
2909                    if let Some(reference) = value.get(&field.name).and_then(Value::as_str) {
2910                        let target_capability = format!(
2911                            "{}:{}",
2912                            request
2913                                .state_namespace
2914                                .as_deref()
2915                                .unwrap_or(&request.application_id),
2916                            target
2917                        );
2918                        let present = if let Some(value) =
2919                            overlay.get(&(target_capability.clone(), reference.into()))
2920                        {
2921                            value.is_some()
2922                        } else if snapshot.iter().any(|row| {
2923                            row.capability == target_capability
2924                                && row.key == reference
2925                                && !row.deleted
2926                        }) {
2927                            true
2928                        } else {
2929                            let cache_key = (target_capability.clone(), reference.to_string());
2930                            if let Some(present) = referenced_records.get(&cache_key) {
2931                                *present
2932                            } else {
2933                                let present = db
2934                                    .get_collection_record(&target_capability, reference)
2935                                    .map_err(StateFailure::storage)?
2936                                    .is_some();
2937                                referenced_records.insert(cache_key, present);
2938                                present
2939                            }
2940                        };
2941                        if !present {
2942                            let mut failure = StateFailure::new(
2943                                "VALIDATION_FAILED",
2944                                &format!(
2945                                    "reference {}.{} points to a missing record",
2946                                    definition.name, field.name
2947                                ),
2948                            );
2949                            failure.resource = Some(format!("{cap}:{key}"));
2950                            return Err(failure);
2951                        }
2952                    }
2953                }
2954            }
2955        }
2956        for index in definition.indexes.iter().filter(|index| index.unique) {
2957            let mut seen = BTreeSet::new();
2958            let existing = snapshot
2959                .iter()
2960                .filter(|row| row.capability == capability && !row.deleted)
2961                .filter(|row| !overlay.contains_key(&(capability.clone(), row.key.clone())))
2962                .map(|row| &row.value)
2963                .chain(
2964                    overlay
2965                        .iter()
2966                        .filter(|((cap, _), _)| cap == &capability)
2967                        .filter_map(|(_, value)| value.as_ref()),
2968                );
2969            for value in existing {
2970                let tuple = index
2971                    .fields
2972                    .iter()
2973                    .map(|field| value.get(field).cloned().unwrap_or(Value::Null))
2974                    .collect::<Vec<_>>();
2975                if index.sparse && tuple.iter().any(Value::is_null) {
2976                    continue;
2977                }
2978                let encoded = serde_json::to_string(&tuple).map_err(StateFailure::storage)?;
2979                if !seen.insert(encoded) {
2980                    return Err(StateFailure::new(
2981                        "CONFLICT",
2982                        &format!("unique index {} violated", index.name),
2983                    ));
2984                }
2985            }
2986        }
2987    }
2988    let commit = db
2989        .apply_atomic_transaction_fenced_in(
2990            &storage_transaction_id,
2991            Some(&payload_hash),
2992            request.causal_parent,
2993            &preconditions,
2994            &record_preconditions,
2995            &crate::ReadLayout::Namespace(namespace.clone()),
2996            &request.fences,
2997            &mutations,
2998            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"})),
2999        )
3000        .map_err(|error| {
3001            // A record precondition that did not hold is a lost race, and it
3002            // says which predicate failed and what the authority holds. That
3003            // detail is the difference between "re-read and decide again" and
3004            // "this deployment is broken", so it is carried through as
3005            // structure rather than flattened into the message.
3006            if let FlowError::PreconditionFailed(refusal) = &error {
3007                let layout = crate::ReadLayout::Namespace(namespace.clone());
3008                let public = refusal.public_json(&layout);
3009                let mut failure =
3010                    StateFailure::new("PRECONDITION_FAILED", &error.to_string());
3011                failure.transaction = Some(transaction_id.clone());
3012                failure.causal_position = db.sequence().ok();
3013                failure.resource = public
3014                    .get("key")
3015                    .and_then(Value::as_str)
3016                    .map(str::to_owned)
3017                    .or_else(|| Some(refusal.key().to_string()));
3018                failure.failure = Some(public);
3019                if let PreconditionFailure::Version {
3020                    expected, actual, ..
3021                } = refusal.as_ref()
3022                {
3023                    failure.expected = Some(Value::from(*expected));
3024                    failure.actual = Some(Value::from(*actual));
3025                }
3026                return failure;
3027            }
3028            if let FlowError::RevisionConflict { expected, actual } = &error {
3029                let mut failure =
3030                    StateFailure::new("PRECONDITION_FAILED", &error.to_string());
3031                failure.transaction = Some(transaction_id.clone());
3032                failure.causal_position = db.sequence().ok();
3033                failure.resource = Some("authority_revision".into());
3034                failure.expected = Some(Value::from(*expected));
3035                failure.actual = Some(Value::from(*actual));
3036                return failure;
3037            }
3038            if let FlowError::TransactionIdPayloadMismatch { .. } = &error {
3039                // Two different payloads raced past the check above under one
3040                // id; the commit lock admitted the first. Same refusal.
3041                let mut failure = idempotency_conflict();
3042                failure.transaction = Some(transaction_id.clone());
3043                return failure;
3044            }
3045            if let FlowError::Io(_) | FlowError::Serde(_) = &error {
3046                // The commit could not be made durable. Never a lost race and
3047                // never a success: the outcome must be treated as unknown and
3048                // reconciled by retrying with the same transaction id.
3049                let mut failure = StateFailure::new("DURABILITY_FAILURE", &error.to_string());
3050                failure.transaction = Some(transaction_id.clone());
3051                return failure;
3052            }
3053            let text = error.to_string();
3054            if text.contains("INVALID_READ_SET:") {
3055                // A malformed or foreign basis is a caller error, never a lost race.
3056                let mut failure = StateFailure::new("VALIDATION_FAILED", &text);
3057                failure.transaction = Some(transaction_id.clone());
3058                return failure;
3059            }
3060            if text.contains("PRECONDITION_FAILED") {
3061                let mut failure = StateFailure::new("PRECONDITION_FAILED", &text);
3062                failure.transaction = Some(transaction_id.clone());
3063                failure.causal_position = db.sequence().ok();
3064                if let Some(condition) = preconditions.first() {
3065                    failure.resource = Some(format!("{}:{}", condition.capability, condition.key));
3066                    failure.expected = Some(
3067                        condition
3068                            .expected_version
3069                            .map(Value::from)
3070                            .unwrap_or(Value::Null),
3071                    );
3072                    failure.actual = Some(
3073                        snapshot
3074                            .iter()
3075                            .find(|row| {
3076                                row.capability == condition.capability && row.key == condition.key
3077                            })
3078                            .and_then(|row| row.operation.as_ref())
3079                            .map(|op| Value::from(op.sequence))
3080                            .unwrap_or(Value::Null),
3081                    );
3082                }
3083                failure
3084            } else {
3085                StateFailure::storage(error)
3086            }
3087        })?;
3088    let cursor = db
3089        .operation_versions()
3090        .map_err(StateFailure::storage)?
3091        .into_iter()
3092        .collect();
3093    let audit = TransactionAudit {
3094        transaction_id: transaction_id.clone(),
3095        subject: request.authorization.subject.clone(),
3096        tenant: request.tenant_id.clone(),
3097        application: request.application_id.clone(),
3098        revision: request.revision_id.clone(),
3099        schema_version: request.schema_version,
3100        state_before: commit.state_before,
3101        state_after: commit.state_after,
3102        operations: request.operations.len(),
3103        authorization_decisions: vec!["state:write allowed".into()],
3104        causal_parent: request.causal_parent,
3105        result: if commit.duplicate {
3106            "duplicate"
3107        } else {
3108            "committed"
3109        }
3110        .into(),
3111    };
3112    Ok(TransactionResult {
3113        transaction_id,
3114        base_revision: commit.base_revision,
3115        commit_revision: commit.commit_revision,
3116        status: commit.status,
3117        state_before: commit.state_before,
3118        state_after: commit.state_after,
3119        causal_cursor: cursor,
3120        duplicate: commit.duplicate,
3121        audit,
3122    })
3123}
3124
3125#[cfg(test)]
3126mod tests {
3127    use super::*;
3128    #[test]
3129    fn extended_field_types_have_canonical_validation() {
3130        let valid = [
3131            (
3132                "uuid",
3133                serde_json::json!("550e8400-e29b-41d4-a716-446655440000"),
3134            ),
3135            ("decimal", serde_json::json!("-1234567890.0123456789")),
3136            ("bigint", serde_json::json!("9223372036854775808123")),
3137            ("date", serde_json::json!("2026-08-21")),
3138            ("time", serde_json::json!("13:45:30")),
3139            ("datetime", serde_json::json!("2026-08-21T13:45:30Z")),
3140            ("email", serde_json::json!("person@example.com")),
3141            ("url", serde_json::json!("https://example.com/path")),
3142            ("phone", serde_json::json!("+1 (212) 555-0100")),
3143            ("binary", serde_json::json!("SGVsbG8=")),
3144            (
3145                "money",
3146                serde_json::json!({"amount":"19.99","currency":"USD"}),
3147            ),
3148            (
3149                "file",
3150                serde_json::json!({"name":"report.pdf","type":"application/pdf","size":42,"ref":"sha256:abc"}),
3151            ),
3152            (
3153                "geo",
3154                serde_json::json!({"latitude":40.7,"longitude":-74.0}),
3155            ),
3156            ("object", serde_json::json!({"enabled":true})),
3157            (
3158                "array<uuid>",
3159                serde_json::json!(["550e8400-e29b-41d4-a716-446655440000"]),
3160            ),
3161            ("map<integer>", serde_json::json!({"one":1,"two":2})),
3162            ("vector<3>", serde_json::json!([0.1, 0.2, 0.3])),
3163        ];
3164        for (kind, value) in valid {
3165            assert!(
3166                valid_nested_type(&value, &parse_manifest_type(kind)),
3167                "{kind} rejected {value}"
3168            );
3169        }
3170        assert!(!valid_nested_type(
3171            &serde_json::json!("12.3x"),
3172            &parse_manifest_type("decimal")
3173        ));
3174        assert!(!valid_nested_type(
3175            &serde_json::json!([1, 2]),
3176            &parse_manifest_type("vector<3>")
3177        ));
3178        assert!(!valid_nested_type(
3179            &serde_json::json!({"latitude":100,"longitude":0}),
3180            &parse_manifest_type("geo")
3181        ));
3182    }
3183    fn schema() -> StateSchema {
3184        StateSchema {
3185            contract_version: 1,
3186            schema_version: 1,
3187            application_id: "app".into(),
3188            revision_id: "rev".into(),
3189            collections: vec![CollectionSchema {
3190                name: "incidents".into(),
3191                version: 1,
3192                fields: vec![
3193                    FieldSchema {
3194                        name: "title".into(),
3195                        field_type: FieldType::Primitive {
3196                            primitive: PrimitiveType::String,
3197                        },
3198                        nullable: false,
3199                        required: true,
3200                        default: None,
3201                        constraints: Default::default(),
3202                        computed: None,
3203                    },
3204                    FieldSchema {
3205                        name: "status".into(),
3206                        field_type: FieldType::Enum {
3207                            values: vec!["open".into(), "closed".into()],
3208                        },
3209                        nullable: false,
3210                        required: false,
3211                        default: Some(Value::String("open".into())),
3212                        constraints: Default::default(),
3213                        computed: None,
3214                    },
3215                ],
3216                indexes: vec![SchemaIndex {
3217                    name: "status".into(),
3218                    fields: vec!["status".into()],
3219                    unique: false,
3220                    sparse: false,
3221                    version: 1,
3222                }],
3223            }],
3224        }
3225    }
3226    fn scoped_schema() -> StateSchema {
3227        let mut schema = schema();
3228        schema.collections[0].fields[1].field_type = FieldType::Primitive {
3229            primitive: PrimitiveType::String,
3230        };
3231        schema
3232    }
3233    fn auth() -> AuthorizationContext {
3234        AuthorizationContext {
3235            subject: "user:1".into(),
3236            tenant_id: "tenant".into(),
3237            application_id: "app".into(),
3238            revision_id: "rev".into(),
3239            capabilities: ["state:read".into(), "state:write".into()].into(),
3240            readable_collections: Default::default(),
3241            writable_collections: Default::default(),
3242            field_projections: Default::default(),
3243        }
3244    }
3245    fn schema_with_owner() -> StateSchema {
3246        StateSchema {
3247            contract_version: 1,
3248            schema_version: 1,
3249            application_id: "app".into(),
3250            revision_id: "rev".into(),
3251            collections: vec![CollectionSchema {
3252                name: "incidents".into(),
3253                version: 1,
3254                fields: vec![
3255                    FieldSchema {
3256                        name: "title".into(),
3257                        field_type: FieldType::Primitive {
3258                            primitive: PrimitiveType::String,
3259                        },
3260                        nullable: false,
3261                        required: true,
3262                        default: None,
3263                        constraints: Default::default(),
3264                        computed: None,
3265                    },
3266                    FieldSchema {
3267                        name: "owner_id".into(),
3268                        field_type: FieldType::Primitive {
3269                            primitive: PrimitiveType::String,
3270                        },
3271                        nullable: false,
3272                        required: false,
3273                        default: None,
3274                        constraints: Default::default(),
3275                        computed: None,
3276                    },
3277                ],
3278                indexes: vec![],
3279            }],
3280        }
3281    }
3282
3283    fn db(name: &str) -> FeltDb {
3284        FeltDb::open(
3285            std::env::temp_dir().join(format!("state-contract-{name}-{}.log", crate::now_ms())),
3286        )
3287        .unwrap()
3288    }
3289    #[test]
3290    fn schema_hash_is_deterministic_and_invalid_schema_fails() {
3291        let a = schema();
3292        let mut b = a.clone();
3293        b.collections[0].fields.reverse();
3294        assert_eq!(schema_hash(&a).unwrap(), schema_hash(&b).unwrap());
3295        let mut invalid = a;
3296        invalid.collections[0].fields[0].required = true;
3297        invalid.collections[0].fields[0].nullable = true;
3298        assert!(!validate_schema(&invalid).valid);
3299    }
3300    #[test]
3301    fn compatibility_classifies_required_and_type_changes() {
3302        let a = schema();
3303        let mut b = a.clone();
3304        b.schema_version = 2;
3305        b.collections[0].fields.push(FieldSchema {
3306            name: "owner".into(),
3307            field_type: FieldType::Primitive {
3308                primitive: PrimitiveType::String,
3309            },
3310            nullable: false,
3311            required: true,
3312            default: None,
3313            constraints: Default::default(),
3314            computed: None,
3315        });
3316        assert_eq!(
3317            compare_schemas(&a, &b).classification,
3318            SchemaCompatibility::Destructive
3319        );
3320        b.collections[0].fields[0].field_type = FieldType::Primitive {
3321            primitive: PrimitiveType::Integer,
3322        };
3323        assert_eq!(
3324            compare_schemas(&a, &b).classification,
3325            SchemaCompatibility::Incompatible
3326        );
3327    }
3328    #[test]
3329    fn transaction_is_atomic_and_preconditions_are_structured() {
3330        let db = db("tx");
3331        let schema = schema();
3332        let request = TransactionRequest {
3333            transaction_id: None,
3334            tenant_id: "tenant".into(),
3335            application_id: "app".into(),
3336            revision_id: "rev".into(),
3337            schema_version: 1,
3338            state_namespace: None,
3339            causal_parent: Some(0),
3340            authorization: auth(),
3341            preconditions: vec![],
3342            operations: vec![
3343                TransactionOperation {
3344                    kind: TransactionOperationKind::Insert,
3345                    collection: "incidents".into(),
3346                    id: "one".into(),
3347                    value: serde_json::json!({"title":"First"}),
3348                    if_version: None,
3349                },
3350                TransactionOperation {
3351                    kind: TransactionOperationKind::Insert,
3352                    collection: "incidents".into(),
3353                    id: "two".into(),
3354                    value: serde_json::json!({"title":"Second"}),
3355                    if_version: None,
3356                },
3357            ],
3358            fences: vec![],
3359        };
3360        let result = execute_transaction(&db, &schema, &request, None).unwrap();
3361        assert_eq!(result.state_after, 2);
3362        assert_eq!(db.export_snapshot().unwrap().rows.len(), 2);
3363        let mut stale = request;
3364        stale.transaction_id = Some("stale".into());
3365        stale.causal_parent = Some(0);
3366        // A create of a record that exists is refused as a structured presence
3367        // precondition, as a `requireAbsent` create is on every other route.
3368        let refused = execute_transaction(&db, &schema, &stale, None).unwrap_err();
3369        assert_eq!(refused.code, "PRECONDITION_FAILED");
3370        let failure = refused.failure.expect("structured failure");
3371        assert_eq!(failure["predicate"], "present");
3372        assert_eq!(failure["collection"], "incidents");
3373        assert_eq!(failure["id"], "one");
3374        assert_eq!(failure["key"], "incidents:one");
3375        assert_eq!(
3376            db.export_snapshot().unwrap().rows.len(),
3377            2,
3378            "nothing written"
3379        );
3380    }
3381    /// A conditional write through the canonical contract has to mean the same
3382    /// thing it means everywhere else in FeltDB, or `putIfAbsent` and
3383    /// `updateIfVersion` become file-runtime privileges.
3384    ///
3385    /// The predicate under test is the document's `__version` -- the number a
3386    /// caller reads back -- not the row's internal storage sequence. Those are
3387    /// two different numbers that happen to share a name, and a fence on the
3388    /// wrong one is worse than no fence, because it reads as protection.
3389    #[test]
3390    fn a_record_precondition_fences_the_document_version() {
3391        let db = db("record-precondition");
3392        let schema = schema();
3393        let write = |id: &str,
3394                     transaction: &str,
3395                     title: &str,
3396                     preconditions: Vec<TransactionPrecondition>,
3397                     kind: TransactionOperationKind| {
3398            execute_transaction(
3399                &db,
3400                &schema,
3401                &TransactionRequest {
3402                    transaction_id: Some(transaction.into()),
3403                    tenant_id: "tenant".into(),
3404                    application_id: "app".into(),
3405                    revision_id: "rev".into(),
3406                    schema_version: 1,
3407                    state_namespace: None,
3408                    causal_parent: None,
3409                    authorization: auth(),
3410                    preconditions,
3411                    operations: vec![TransactionOperation {
3412                        kind,
3413                        collection: "incidents".into(),
3414                        id: id.into(),
3415                        value: serde_json::json!({ "title": title }),
3416                        if_version: None,
3417                    }],
3418                    fences: vec![],
3419                },
3420                None,
3421            )
3422        };
3423        let fence = |version: Option<u64>, absent: bool| {
3424            vec![TransactionPrecondition::Record {
3425                collection: "incidents".into(),
3426                id: "i1".into(),
3427                require_absent: absent,
3428                expected_version: version,
3429                expected_epoch: None,
3430                expected_lease_id: None,
3431            }]
3432        };
3433        let version = || {
3434            db.state_rows()
3435                .unwrap()
3436                .into_iter()
3437                .find(|row| row.capability == "app:incidents" && row.key == "i1" && !row.deleted)
3438                .and_then(|row| row.value.get("__version").and_then(Value::as_u64))
3439        };
3440
3441        // A conditional create is a create at version 1.
3442        write(
3443            "i1",
3444            "create",
3445            "First",
3446            fence(None, true),
3447            TransactionOperationKind::Insert,
3448        )
3449        .expect("the record is absent, so the create commits");
3450        assert_eq!(version(), Some(1));
3451
3452        // The same create again is a lost race. Whichever check catches it,
3453        // the answer is a conflict and nothing was written.
3454        let refused = write(
3455            "i1",
3456            "create-again",
3457            "Second",
3458            fence(None, true),
3459            TransactionOperationKind::Insert,
3460        )
3461        .expect_err("the record now exists");
3462        assert!(
3463            refused.code == "CONFLICT" || refused.code == "PRECONDITION_FAILED",
3464            "a lost creation race is a conflict, got {}",
3465            refused.code,
3466        );
3467        assert_eq!(version(), Some(1), "a refused transaction writes nothing");
3468
3469        // A guard on a record the transaction does not write is evaluated
3470        // inside the commit lock, and says which predicate failed.
3471        let guarded = write(
3472            "i2",
3473            "guarded",
3474            "Other",
3475            fence(None, true),
3476            TransactionOperationKind::Insert,
3477        )
3478        .expect_err("i1 exists, so the guard fails and i2 is never written");
3479        assert_eq!(guarded.code, "PRECONDITION_FAILED");
3480        assert_eq!(
3481            guarded
3482                .failure
3483                .as_ref()
3484                .and_then(|value| value.get("predicate").and_then(Value::as_str)),
3485            Some("present"),
3486        );
3487        assert!(
3488            db.state_rows()
3489                .unwrap()
3490                .iter()
3491                .all(|row| row.key != "i2" || row.deleted),
3492            "a refused guard leaves the staged write unwritten",
3493        );
3494
3495        // A stale fence conflicts and names what the authority actually holds.
3496        let stale = write(
3497            "i1",
3498            "stale",
3499            "Third",
3500            fence(Some(7), false),
3501            TransactionOperationKind::Update,
3502        )
3503        .expect_err("version 7 is not the version on record");
3504        assert_eq!(stale.code, "PRECONDITION_FAILED");
3505        assert_eq!(stale.expected, Some(Value::from(7)));
3506        assert_eq!(stale.actual, Some(Value::from(1)));
3507        assert_eq!(version(), Some(1));
3508
3509        // The fenced write commits and the authority owns the next version.
3510        write(
3511            "i1",
3512            "fenced",
3513            "Fourth",
3514            fence(Some(1), false),
3515            TransactionOperationKind::Update,
3516        )
3517        .expect("version 1 is the version on record");
3518        assert_eq!(version(), Some(2));
3519
3520        // Replaying a committed transaction id applies nothing, so it cannot
3521        // advance the version a second time.
3522        write(
3523            "i1",
3524            "fenced",
3525            "Fourth",
3526            fence(Some(1), false),
3527            TransactionOperationKind::Update,
3528        )
3529        .expect("a duplicate transaction id is not a failure");
3530        assert_eq!(version(), Some(2));
3531    }
3532
3533    /// A precondition that constrains nothing is refused rather than accepted,
3534    /// because one that is silently ignored reads as protection and provides
3535    /// none.
3536    #[test]
3537    fn an_empty_record_precondition_is_refused() {
3538        let db = db("empty-precondition");
3539        let schema = schema();
3540        let failure = execute_transaction(
3541            &db,
3542            &schema,
3543            &TransactionRequest {
3544                transaction_id: Some("empty".into()),
3545                tenant_id: "tenant".into(),
3546                application_id: "app".into(),
3547                revision_id: "rev".into(),
3548                schema_version: 1,
3549                state_namespace: None,
3550                causal_parent: None,
3551                authorization: auth(),
3552                preconditions: vec![TransactionPrecondition::Record {
3553                    collection: "incidents".into(),
3554                    id: "i1".into(),
3555                    require_absent: false,
3556                    expected_version: None,
3557                    expected_epoch: None,
3558                    expected_lease_id: None,
3559                }],
3560                operations: vec![TransactionOperation {
3561                    kind: TransactionOperationKind::Insert,
3562                    collection: "incidents".into(),
3563                    id: "i1".into(),
3564                    value: serde_json::json!({ "title": "First" }),
3565                    if_version: None,
3566                }],
3567                fences: vec![],
3568            },
3569            None,
3570        )
3571        .expect_err("a precondition that constrains nothing is a caller error");
3572        assert_eq!(failure.code, "VALIDATION_FAILED");
3573        assert!(db.state_rows().unwrap().iter().all(|row| row.deleted));
3574    }
3575
3576    #[test]
3577    fn validation_failure_rolls_back_every_operation() {
3578        let db = db("rollback");
3579        let schema = schema();
3580        let request = TransactionRequest {
3581            transaction_id: None,
3582            tenant_id: "tenant".into(),
3583            application_id: "app".into(),
3584            revision_id: "rev".into(),
3585            schema_version: 1,
3586            state_namespace: None,
3587            causal_parent: None,
3588            authorization: auth(),
3589            preconditions: vec![],
3590            operations: vec![
3591                TransactionOperation {
3592                    kind: TransactionOperationKind::Insert,
3593                    collection: "incidents".into(),
3594                    id: "one".into(),
3595                    value: serde_json::json!({"title":"ok"}),
3596                    if_version: None,
3597                },
3598                TransactionOperation {
3599                    kind: TransactionOperationKind::Insert,
3600                    collection: "incidents".into(),
3601                    id: "two".into(),
3602                    value: serde_json::json!({"status":"open"}),
3603                    if_version: None,
3604                },
3605            ],
3606            fences: vec![],
3607        };
3608        assert_eq!(
3609            execute_transaction(&db, &schema, &request, None)
3610                .unwrap_err()
3611                .code,
3612            "VALIDATION_FAILED"
3613        );
3614        assert!(db.export_snapshot().unwrap().rows.is_empty());
3615    }
3616    #[test]
3617    fn query_filters_orders_projects_and_aggregates() {
3618        let db = db("query");
3619        let schema = schema();
3620        for (id, title, status) in [("1", "B", "open"), ("2", "A", "open"), ("3", "C", "closed")] {
3621            execute_transaction(
3622                &db,
3623                &schema,
3624                &TransactionRequest {
3625                    transaction_id: None,
3626                    tenant_id: "tenant".into(),
3627                    application_id: "app".into(),
3628                    revision_id: "rev".into(),
3629                    schema_version: 1,
3630                    state_namespace: None,
3631                    causal_parent: None,
3632                    authorization: auth(),
3633                    preconditions: vec![],
3634                    operations: vec![TransactionOperation {
3635                        kind: TransactionOperationKind::Insert,
3636                        collection: "incidents".into(),
3637                        id: id.into(),
3638                        value: serde_json::json!({"title":title,"status":status}),
3639                        if_version: None,
3640                    }],
3641                    fences: vec![],
3642                },
3643                None,
3644            )
3645            .unwrap();
3646        }
3647        let context = begin_read(&db, &schema, "app", auth()).unwrap();
3648        let result = execute_query(
3649            &db,
3650            &schema,
3651            &context,
3652            &CanonicalQuery {
3653                collection: "incidents".into(),
3654                filter: Some(QueryFilter::Eq {
3655                    field: "status".into(),
3656                    value: Value::String("open".into()),
3657                }),
3658                order_by: vec![QueryOrder {
3659                    field: "title".into(),
3660                    direction: SortDirection::Asc,
3661                }],
3662                limit: Some(1),
3663                offset: 0,
3664                cursor: None,
3665                projection: vec!["title".into()],
3666                group_by: vec!["status".into()],
3667                aggregates: vec![AggregateDefinition {
3668                    name: "total".into(),
3669                    function: AggregateFunction::Count,
3670                    field: None,
3671                }],
3672                references: vec![],
3673                semantic: None,
3674            },
3675            None,
3676        )
3677        .unwrap();
3678        assert_eq!(result.records, vec![serde_json::json!({"title":"A"})]);
3679        assert_eq!(result.aggregates[0]["total"], 2);
3680        assert_eq!(result.state_version, 3);
3681    }
3682    #[test]
3683    fn authorization_prevents_projection_before_read() {
3684        let db = db("auth");
3685        let schema = schema();
3686        let mut auth = auth();
3687        auth.field_projections
3688            .insert("incidents".into(), ["status".into()].into());
3689        let context = begin_read(&db, &schema, "app", auth).unwrap();
3690        let error = execute_query(
3691            &db,
3692            &schema,
3693            &context,
3694            &CanonicalQuery {
3695                collection: "incidents".into(),
3696                filter: None,
3697                order_by: vec![],
3698                limit: None,
3699                offset: 0,
3700                cursor: None,
3701                projection: vec!["title".into()],
3702                group_by: vec![],
3703                aggregates: vec![],
3704                references: vec![],
3705                semantic: None,
3706            },
3707            None,
3708        )
3709        .unwrap_err();
3710        assert_eq!(error.code, "UNAUTHORIZED_PROJECTION");
3711    }
3712    #[test]
3713    fn stale_record_precondition_is_structured_and_identity_is_idempotent() {
3714        let db = db("precondition");
3715        let schema = schema();
3716        let mut request = TransactionRequest {
3717            transaction_id: None,
3718            tenant_id: "tenant".into(),
3719            application_id: "app".into(),
3720            revision_id: "rev".into(),
3721            schema_version: 1,
3722            state_namespace: None,
3723            causal_parent: None,
3724            authorization: auth(),
3725            preconditions: vec![],
3726            operations: vec![TransactionOperation {
3727                kind: TransactionOperationKind::Insert,
3728                collection: "incidents".into(),
3729                id: "one".into(),
3730                value: serde_json::json!({"title":"First"}),
3731                if_version: None,
3732            }],
3733            fences: vec![],
3734        };
3735        let first = execute_transaction(&db, &schema, &request, None).unwrap();
3736        let duplicate = execute_transaction(&db, &schema, &request, None).unwrap();
3737        assert_eq!(first.transaction_id, duplicate.transaction_id);
3738        assert!(duplicate.duplicate);
3739        request.transaction_id = Some("stale-update".into());
3740        request.operations = vec![TransactionOperation {
3741            kind: TransactionOperationKind::Update,
3742            collection: "incidents".into(),
3743            id: "one".into(),
3744            value: serde_json::json!({"title":"Changed"}),
3745            if_version: Some(99),
3746        }];
3747        let failure = execute_transaction(&db, &schema, &request, None).unwrap_err();
3748        assert_eq!(failure.code, "PRECONDITION_FAILED");
3749        assert_eq!(failure.expected, Some(Value::from(99)));
3750        assert_eq!(failure.actual, Some(Value::from(1)));
3751    }
3752    #[test]
3753    fn stale_authority_revision_is_a_structured_precondition_failure() {
3754        let db = db("authority-revision-precondition");
3755        let schema = schema();
3756        let mut request = TransactionRequest {
3757            transaction_id: Some("initial".into()),
3758            tenant_id: "tenant".into(),
3759            application_id: "app".into(),
3760            revision_id: "rev".into(),
3761            schema_version: 1,
3762            state_namespace: None,
3763            causal_parent: None,
3764            authorization: auth(),
3765            preconditions: vec![],
3766            operations: vec![TransactionOperation {
3767                kind: TransactionOperationKind::Insert,
3768                collection: "incidents".into(),
3769                id: "one".into(),
3770                value: serde_json::json!({"title":"First"}),
3771                if_version: None,
3772            }],
3773            fences: vec![],
3774        };
3775        execute_transaction(&db, &schema, &request, None).unwrap();
3776        request.transaction_id = Some("stale-parent".into());
3777        request.causal_parent = Some(0);
3778        request.operations[0].id = "two".into();
3779
3780        let failure = execute_transaction(&db, &schema, &request, None).unwrap_err();
3781        assert_eq!(failure.code, "PRECONDITION_FAILED");
3782        assert_eq!(failure.resource.as_deref(), Some("authority_revision"));
3783        assert_eq!(failure.expected, Some(Value::from(0)));
3784        assert_eq!(failure.actual, Some(Value::from(1)));
3785    }
3786    #[test]
3787    fn explicit_transaction_id_is_bound_to_canonical_payload_across_restart() {
3788        let path = std::env::temp_dir().join(format!(
3789            "feltdb-content-id-{}-{}.log",
3790            std::process::id(),
3791            crate::now_ms()
3792        ));
3793        let schema = schema();
3794        let request = TransactionRequest {
3795            transaction_id: Some("stable-client-id".into()),
3796            tenant_id: "tenant".into(),
3797            application_id: "app".into(),
3798            revision_id: "rev".into(),
3799            schema_version: 1,
3800            state_namespace: None,
3801            causal_parent: None,
3802            authorization: auth(),
3803            preconditions: vec![],
3804            operations: vec![TransactionOperation {
3805                kind: TransactionOperationKind::Insert,
3806                collection: "incidents".into(),
3807                id: "content-bound".into(),
3808                value: serde_json::json!({"title":"Original"}),
3809                if_version: None,
3810            }],
3811            fences: vec![],
3812        };
3813        {
3814            let db = crate::open(&path).unwrap();
3815            assert!(
3816                !execute_transaction(&db, &schema, &request, None)
3817                    .unwrap()
3818                    .duplicate
3819            );
3820            assert!(
3821                execute_transaction(&db, &schema, &request, None)
3822                    .unwrap()
3823                    .duplicate
3824            );
3825        }
3826        let reopened = crate::open(&path).unwrap();
3827        assert!(
3828            execute_transaction(&reopened, &schema, &request, None)
3829                .unwrap()
3830                .duplicate
3831        );
3832        let mut changed = request.clone();
3833        changed.operations[0].value = serde_json::json!({"title":"Changed"});
3834        let failure = execute_transaction(&reopened, &schema, &changed, None).unwrap_err();
3835        assert_eq!(failure.code, "IDEMPOTENCY_CONFLICT");
3836        assert!(failure.message.contains("different canonical payload"));
3837        std::fs::remove_file(path).ok();
3838    }
3839    #[test]
3840    fn explicit_transaction_id_is_isolated_between_applications() {
3841        fn application(
3842            app: &str,
3843            namespace: &str,
3844            transaction: &str,
3845            record: &str,
3846        ) -> (StateSchema, TransactionRequest) {
3847            let mut schema = schema();
3848            schema.application_id = app.into();
3849            let mut authorization = auth();
3850            authorization.application_id = app.into();
3851            let request = TransactionRequest {
3852                transaction_id: Some(transaction.into()),
3853                tenant_id: "tenant".into(),
3854                application_id: app.into(),
3855                revision_id: "rev".into(),
3856                schema_version: 1,
3857                state_namespace: Some(namespace.into()),
3858                causal_parent: None,
3859                authorization,
3860                preconditions: vec![],
3861                operations: vec![TransactionOperation {
3862                    kind: TransactionOperationKind::Insert,
3863                    collection: "incidents".into(),
3864                    id: record.into(),
3865                    value: serde_json::json!({"title":format!("{app}-{record}")}),
3866                    if_version: None,
3867                }],
3868                fences: vec![],
3869            };
3870            (schema, request)
3871        }
3872        for (name, order) in [
3873            (
3874                "a-then-b",
3875                vec![("app-a", "shared-1", "one"), ("app-b", "shared-1", "one")],
3876            ),
3877            (
3878                "b-then-a",
3879                vec![("app-b", "shared-1", "one"), ("app-a", "shared-1", "one")],
3880            ),
3881            (
3882                "interleaved",
3883                vec![
3884                    ("app-a", "shared-1", "one"),
3885                    ("app-b", "shared-1", "one"),
3886                    ("app-a", "shared-2", "two"),
3887                    ("app-b", "shared-2", "two"),
3888                ],
3889            ),
3890        ] {
3891            let path = std::env::temp_dir().join(format!(
3892                "feltdb-app-isolation-{name}-{}-{}.log",
3893                std::process::id(),
3894                crate::now_ms()
3895            ));
3896            {
3897                let db = crate::open(&path).unwrap();
3898                for (app, transaction, record) in &order {
3899                    let (schema, request) =
3900                        application(app, &format!("{app}:production"), transaction, record);
3901                    assert!(
3902                        !execute_transaction(&db, &schema, &request, None)
3903                            .unwrap()
3904                            .duplicate
3905                    );
3906                }
3907            }
3908            let reopened = crate::open(&path).unwrap();
3909            for (app, transaction, record) in &order {
3910                let (schema, request) =
3911                    application(app, &format!("{app}:production"), transaction, record);
3912                assert!(
3913                    execute_transaction(&reopened, &schema, &request, None)
3914                        .unwrap()
3915                        .duplicate
3916                );
3917                let rows = reopened.state_rows().unwrap();
3918                assert_eq!(
3919                    rows.iter()
3920                        .filter(
3921                            |row| row.capability == format!("{app}:production:incidents")
3922                                && row.key == *record
3923                                && !row.deleted
3924                        )
3925                        .count(),
3926                    1
3927                );
3928            }
3929            std::fs::remove_file(path).ok();
3930        }
3931    }
3932    #[test]
3933    fn read_context_remains_a_stable_snapshot() {
3934        let db = db("snapshot-read");
3935        let schema = schema();
3936        let insert = |id: &str| TransactionRequest {
3937            transaction_id: None,
3938            tenant_id: "tenant".into(),
3939            application_id: "app".into(),
3940            revision_id: "rev".into(),
3941            schema_version: 1,
3942            state_namespace: None,
3943            causal_parent: None,
3944            authorization: auth(),
3945            preconditions: vec![],
3946            operations: vec![TransactionOperation {
3947                kind: TransactionOperationKind::Insert,
3948                collection: "incidents".into(),
3949                id: id.into(),
3950                value: serde_json::json!({"title":id}),
3951                if_version: None,
3952            }],
3953            fences: vec![],
3954        };
3955        execute_transaction(&db, &schema, &insert("one"), None).unwrap();
3956        let context = begin_read(&db, &schema, "app", auth()).unwrap();
3957        execute_transaction(&db, &schema, &insert("two"), None).unwrap();
3958        let result = execute_query(
3959            &db,
3960            &schema,
3961            &context,
3962            &CanonicalQuery {
3963                collection: "incidents".into(),
3964                filter: None,
3965                order_by: vec![],
3966                limit: None,
3967                offset: 0,
3968                cursor: None,
3969                projection: vec![],
3970                group_by: vec![],
3971                aggregates: vec![],
3972                references: vec![],
3973                semantic: None,
3974            },
3975            None,
3976        )
3977        .unwrap();
3978        assert_eq!(result.records.len(), 1);
3979        assert_eq!(result.state_version, 1);
3980    }
3981    #[test]
3982    fn transaction_log_envelope_replays_all_rows() {
3983        let path =
3984            std::env::temp_dir().join(format!("state-contract-replay-{}.log", crate::now_ms()));
3985        let schema = schema();
3986        {
3987            let db = FeltDb::open(&path).unwrap();
3988            let request = TransactionRequest {
3989                transaction_id: Some("replay-tx".into()),
3990                tenant_id: "tenant".into(),
3991                application_id: "app".into(),
3992                revision_id: "rev".into(),
3993                schema_version: 1,
3994                state_namespace: None,
3995                causal_parent: None,
3996                authorization: auth(),
3997                preconditions: vec![],
3998                operations: vec![
3999                    TransactionOperation {
4000                        kind: TransactionOperationKind::Insert,
4001                        collection: "incidents".into(),
4002                        id: "one".into(),
4003                        value: serde_json::json!({"title":"One"}),
4004                        if_version: None,
4005                    },
4006                    TransactionOperation {
4007                        kind: TransactionOperationKind::Insert,
4008                        collection: "incidents".into(),
4009                        id: "two".into(),
4010                        value: serde_json::json!({"title":"Two"}),
4011                        if_version: None,
4012                    },
4013                ],
4014                fences: vec![],
4015            };
4016            execute_transaction(&db, &schema, &request, None).unwrap();
4017        }
4018        let reopened = FeltDb::open(path).unwrap();
4019        assert_eq!(reopened.export_snapshot().unwrap().rows.len(), 2);
4020        assert_eq!(reopened.current_revision().unwrap(), 1);
4021    }
4022    #[test]
4023    fn indexed_equality_query_uses_index_not_scan() {
4024        let db = db("regression-indexed-equality");
4025        let schema = schema();
4026        let request = TransactionRequest {
4027            transaction_id: None,
4028            tenant_id: "tenant".into(),
4029            application_id: "app".into(),
4030            revision_id: "rev".into(),
4031            schema_version: 1,
4032            state_namespace: None,
4033            causal_parent: None,
4034            authorization: auth(),
4035            preconditions: vec![],
4036            operations: (1..=100)
4037                .map(|i| TransactionOperation {
4038                    kind: TransactionOperationKind::Insert,
4039                    collection: "incidents".into(),
4040                    id: format!("incident-{}", i),
4041                    value: serde_json::json!({
4042                        "title": format!("Incident {}", i),
4043                        "status": if i % 2 == 0 { "open" } else { "closed" }
4044                    }),
4045                    if_version: None,
4046                })
4047                .collect(),
4048            fences: vec![],
4049        };
4050        execute_transaction(&db, &schema, &request, None).unwrap();
4051        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4052        let result = execute_query(
4053            &db,
4054            &schema,
4055            &context,
4056            &CanonicalQuery {
4057                collection: "incidents".into(),
4058                filter: Some(QueryFilter::Eq {
4059                    field: "status".into(),
4060                    value: Value::String("open".into()),
4061                }),
4062                order_by: vec![],
4063                limit: Some(100),
4064                offset: 0,
4065                cursor: None,
4066                projection: vec![],
4067                group_by: vec![],
4068                aggregates: vec![],
4069                references: vec![],
4070                semantic: None,
4071            },
4072            None,
4073        )
4074        .unwrap();
4075        assert_eq!(result.plan.access_method, Some("index".to_string()));
4076        assert_eq!(result.plan.index, Some("status".to_string()));
4077        assert!(!result.plan.scan_fallback);
4078        assert_eq!(result.plan.implementation, "index_lookup");
4079        assert!(result.plan.actual_rows_scanned.unwrap() <= 100);
4080        assert_eq!(
4081            result.plan.index_metrics.as_ref().unwrap().index_used,
4082            Some("status".to_string())
4083        );
4084        assert!(result.plan.index_metrics.as_ref().unwrap().index_hits > 0);
4085    }
4086    #[test]
4087    fn non_indexed_field_query_falls_back_to_scan() {
4088        let db = db("regression-non-indexed");
4089        let schema = schema();
4090        let request = TransactionRequest {
4091            transaction_id: None,
4092            tenant_id: "tenant".into(),
4093            application_id: "app".into(),
4094            revision_id: "rev".into(),
4095            schema_version: 1,
4096            state_namespace: None,
4097            causal_parent: None,
4098            authorization: auth(),
4099            preconditions: vec![],
4100            operations: (1..=50)
4101                .map(|i| TransactionOperation {
4102                    kind: TransactionOperationKind::Insert,
4103                    collection: "incidents".into(),
4104                    id: format!("incident-{}", i),
4105                    value: serde_json::json!({
4106                        "title": format!("Incident {}", i),
4107                        "status": "open"
4108                    }),
4109                    if_version: None,
4110                })
4111                .collect(),
4112            fences: vec![],
4113        };
4114        execute_transaction(&db, &schema, &request, None).unwrap();
4115        let context = begin_read(&db, &schema, "app", auth()).unwrap();
4116        let result = execute_query(
4117            &db,
4118            &schema,
4119            &context,
4120            &CanonicalQuery {
4121                collection: "incidents".into(),
4122                filter: Some(QueryFilter::Eq {
4123                    field: "title".into(),
4124                    value: Value::String("Incident 25".into()),
4125                }),
4126                order_by: vec![],
4127                limit: Some(100),
4128                offset: 0,
4129                cursor: None,
4130                projection: vec![],
4131                group_by: vec![],
4132                aggregates: vec![],
4133                references: vec![],
4134                semantic: None,
4135            },
4136            None,
4137        )
4138        .unwrap();
4139        assert_eq!(
4140            result.plan.access_method,
4141            Some("sequential_scan".to_string())
4142        );
4143        assert!(result.plan.scan_fallback);
4144        assert_eq!(result.plan.implementation, "full_table_scan");
4145        assert_eq!(
4146            result.plan.index_metrics.as_ref().unwrap().fallback_used,
4147            true
4148        );
4149        assert_eq!(
4150            result.plan.index_metrics.as_ref().unwrap().fallback_reason,
4151            Some("no_applicable_index".to_string())
4152        );
4153    }
4154    #[test]
4155    fn timing_measurements_are_populated() {
4156        let db = db("regression-timing");
4157        let schema = schema();
4158        let request = TransactionRequest {
4159            transaction_id: None,
4160            tenant_id: "tenant".into(),
4161            application_id: "app".into(),
4162            revision_id: "rev".into(),
4163            schema_version: 1,
4164            state_namespace: None,
4165            causal_parent: None,
4166            authorization: auth(),
4167            preconditions: vec![],
4168            operations: vec![TransactionOperation {
4169                kind: TransactionOperationKind::Insert,
4170                collection: "incidents".into(),
4171                id: "test-incident".into(),
4172                value: serde_json::json!({"title": "Test", "status": "open"}),
4173                if_version: None,
4174            }],
4175            fences: vec![],
4176        };
4177        execute_transaction(&db, &schema, &request, None).unwrap();
4178        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4179        let result = execute_query(
4180            &db,
4181            &schema,
4182            &context,
4183            &CanonicalQuery {
4184                collection: "incidents".into(),
4185                filter: Some(QueryFilter::Eq {
4186                    field: "status".into(),
4187                    value: Value::String("open".into()),
4188                }),
4189                order_by: vec![],
4190                limit: Some(100),
4191                offset: 0,
4192                cursor: None,
4193                projection: vec![],
4194                group_by: vec![],
4195                aggregates: vec![],
4196                references: vec![],
4197                semantic: None,
4198            },
4199            None,
4200        )
4201        .unwrap();
4202        assert!(result.plan.timing.is_some());
4203        let timing = result.plan.timing.unwrap();
4204        assert!(timing.planning_ms.is_some());
4205        assert!(timing.filtering_ms.is_some());
4206        assert!(timing.serialization_ms.is_some());
4207        assert!(timing.total_ms.is_some());
4208        assert!(timing.total_ms.unwrap() > 0.0);
4209    }
4210    #[test]
4211    fn index_metrics_track_hits_and_misses() {
4212        let db = db("regression-metrics");
4213        let schema = schema();
4214        let request = TransactionRequest {
4215            transaction_id: None,
4216            tenant_id: "tenant".into(),
4217            application_id: "app".into(),
4218            revision_id: "rev".into(),
4219            schema_version: 1,
4220            state_namespace: None,
4221            causal_parent: None,
4222            authorization: auth(),
4223            preconditions: vec![],
4224            operations: (1..=20)
4225                .map(|i| TransactionOperation {
4226                    kind: TransactionOperationKind::Insert,
4227                    collection: "incidents".into(),
4228                    id: format!("incident-{}", i),
4229                    value: serde_json::json!({
4230                        "title": format!("Incident {}", i),
4231                        "status": if i <= 10 { "open" } else { "closed" }
4232                    }),
4233                    if_version: None,
4234                })
4235                .collect(),
4236            fences: vec![],
4237        };
4238        execute_transaction(&db, &schema, &request, None).unwrap();
4239        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4240        let result = execute_query(
4241            &db,
4242            &schema,
4243            &context,
4244            &CanonicalQuery {
4245                collection: "incidents".into(),
4246                filter: Some(QueryFilter::Eq {
4247                    field: "status".into(),
4248                    value: Value::String("open".into()),
4249                }),
4250                order_by: vec![],
4251                limit: Some(100),
4252                offset: 0,
4253                cursor: None,
4254                projection: vec![],
4255                group_by: vec![],
4256                aggregates: vec![],
4257                references: vec![],
4258                semantic: None,
4259            },
4260            None,
4261        )
4262        .unwrap();
4263        let metrics = result.plan.index_metrics.as_ref().unwrap();
4264        assert_eq!(metrics.index_used, Some("status".to_string()));
4265        assert_eq!(metrics.index_hits, 10);
4266        assert_eq!(metrics.index_misses, 0);
4267        assert!(!metrics.fallback_used);
4268    }
4269
4270    #[test]
4271    fn scoped_cursor_is_complete_deterministic_and_bound_to_scope_and_collection() {
4272        let db = db("scoped-pagination");
4273        let schema = scoped_schema();
4274        let operations = (0..237)
4275            .map(|i| (format!("a-{i:03}"), "tenant-a"))
4276            .chain((0..1000).map(|i| (format!("b-{i:04}"), "tenant-b")))
4277            .map(|(id, scope)| TransactionOperation {
4278                kind: TransactionOperationKind::Insert,
4279                collection: "incidents".into(),
4280                id: id.clone(),
4281                value: serde_json::json!({"title": id, "status": scope}),
4282                if_version: None,
4283            })
4284            .collect();
4285        execute_transaction(
4286            &db,
4287            &schema,
4288            &TransactionRequest {
4289                transaction_id: Some("seed-scopes".into()),
4290                tenant_id: "tenant".into(),
4291                application_id: "app".into(),
4292                revision_id: "rev".into(),
4293                schema_version: 1,
4294                state_namespace: None,
4295                causal_parent: None,
4296                authorization: auth(),
4297                preconditions: vec![],
4298                operations,
4299                fences: vec![],
4300            },
4301            None,
4302        )
4303        .unwrap();
4304
4305        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4306        let mut query = CanonicalQuery {
4307            collection: "incidents".into(),
4308            filter: Some(QueryFilter::Eq {
4309                field: "status".into(),
4310                value: Value::String("tenant-a".into()),
4311            }),
4312            order_by: vec![],
4313            limit: Some(25),
4314            offset: 0,
4315            cursor: None,
4316            projection: vec![],
4317            group_by: vec![],
4318            aggregates: vec![],
4319            references: vec![],
4320            semantic: None,
4321        };
4322        let mut ids = Vec::new();
4323        let first_cursor = loop {
4324            let page = execute_query(&db, &schema, &context, &query, None).unwrap();
4325            assert!(page.records.len() <= 25);
4326            assert!(
4327                page.records
4328                    .iter()
4329                    .all(|record| record["status"] == "tenant-a")
4330            );
4331            ids.extend(
4332                page.records
4333                    .iter()
4334                    .map(|record| record["_id"].as_str().unwrap().to_owned()),
4335            );
4336            let Some(cursor) = page.next_cursor else {
4337                break query.cursor.clone();
4338            };
4339            query.cursor = Some(cursor);
4340        };
4341        assert_eq!(ids.len(), 237);
4342        let unique = ids.iter().collect::<std::collections::BTreeSet<_>>();
4343        assert_eq!(unique.len(), 237);
4344        assert!(first_cursor.is_some());
4345
4346        let cursor = encode_query_cursor(
4347            &pagination_query_hash(&CanonicalQuery {
4348                cursor: None,
4349                ..query.clone()
4350            })
4351            .unwrap(),
4352            25,
4353        );
4354        let mut wrong_scope = query.clone();
4355        wrong_scope.cursor = Some(cursor.clone());
4356        wrong_scope.filter = Some(QueryFilter::Eq {
4357            field: "status".into(),
4358            value: Value::String("tenant-b".into()),
4359        });
4360        assert_eq!(
4361            execute_query(&db, &schema, &context, &wrong_scope, None)
4362                .unwrap_err()
4363                .code,
4364            "INVALID_CURSOR"
4365        );
4366        let mut malformed = query;
4367        malformed.cursor = Some("not-a-cursor".into());
4368        assert_eq!(
4369            execute_query(&db, &schema, &context, &malformed, None)
4370                .unwrap_err()
4371                .code,
4372            "INVALID_CURSOR"
4373        );
4374    }
4375
4376    /// Every query surface must either implement a recognized semantic clause
4377    /// per `docs/reference/semantic-query.md` or refuse it. This surface
4378    /// refuses: a semantic clause is `SEMANTIC_UNSUPPORTED` before any
4379    /// deterministic work, a malformed one is `INVALID_QUERY`, nothing is
4380    /// read, nothing is judged, and the same query without the clause still
4381    /// answers.
4382    #[test]
4383    fn semantic_clause_is_recognized_and_refused_never_ignored() {
4384        let db = db("semantic-refusal");
4385        let schema = schema();
4386        execute_transaction(
4387            &db,
4388            &schema,
4389            &TransactionRequest {
4390                transaction_id: Some("seed".into()),
4391                tenant_id: "tenant".into(),
4392                application_id: "app".into(),
4393                revision_id: "rev".into(),
4394                schema_version: 1,
4395                state_namespace: None,
4396                causal_parent: None,
4397                authorization: auth(),
4398                preconditions: vec![],
4399                operations: vec![TransactionOperation {
4400                    kind: TransactionOperationKind::Insert,
4401                    collection: "incidents".into(),
4402                    id: "i1".into(),
4403                    value: serde_json::json!({ "title": "First", "status": "open" }),
4404                    if_version: None,
4405                }],
4406                fences: vec![],
4407            },
4408            None,
4409        )
4410        .unwrap();
4411        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4412        let plain = CanonicalQuery {
4413            collection: "incidents".into(),
4414            filter: Some(QueryFilter::Eq {
4415                field: "status".into(),
4416                value: Value::String("open".into()),
4417            }),
4418            order_by: vec![],
4419            limit: Some(10),
4420            offset: 0,
4421            cursor: None,
4422            projection: vec![],
4423            group_by: vec![],
4424            aggregates: vec![],
4425            references: vec![],
4426            semantic: None,
4427        };
4428        let state_before = db.state_rows().unwrap().len();
4429
4430        // A semantic clause in the bounded query's own grammar, and one that
4431        // only expresses semantic intent, are both recognized and refused.
4432        for clause in [
4433            serde_json::json!({
4434                "decision": { "kind": "binary", "predicate": "needs a human" },
4435                "filter": { "is": true },
4436            }),
4437            serde_json::json!({ "decision": "needs_human" }),
4438            serde_json::json!({}),
4439        ] {
4440            let failure = execute_query(
4441                &db,
4442                &schema,
4443                &context,
4444                &CanonicalQuery {
4445                    semantic: Some(clause),
4446                    ..plain.clone()
4447                },
4448                None,
4449            )
4450            .expect_err("semantic intent is never silently dropped");
4451            assert_eq!(failure.code, "SEMANTIC_UNSUPPORTED");
4452        }
4453
4454        // A semantic value that is not a clause is a validation error, not a
4455        // recognized-but-unsupported operation.
4456        for malformed in [
4457            serde_json::json!("yes"),
4458            serde_json::json!(true),
4459            serde_json::json!(1),
4460            serde_json::json!(["needs_human"]),
4461        ] {
4462            let failure = execute_query(
4463                &db,
4464                &schema,
4465                &context,
4466                &CanonicalQuery {
4467                    semantic: Some(malformed),
4468                    ..plain.clone()
4469                },
4470                None,
4471            )
4472            .expect_err("a malformed semantic value is invalid");
4473            assert_eq!(failure.code, "INVALID_QUERY");
4474        }
4475
4476        // The clause is refused ahead of every other validation, so a query
4477        // that is also otherwise invalid still fails on semantic intent.
4478        let failure = execute_query(
4479            &db,
4480            &schema,
4481            &context,
4482            &CanonicalQuery {
4483                semantic: Some(serde_json::json!({ "decision": "needs_human" })),
4484                limit: Some(0),
4485                ..plain.clone()
4486            },
4487            None,
4488        )
4489        .unwrap_err();
4490        assert_eq!(failure.code, "SEMANTIC_UNSUPPORTED");
4491
4492        // Refusal reads nothing, judges nothing, and writes nothing: no
4493        // judgment collection appears and the state is what it was.
4494        assert_eq!(db.state_rows().unwrap().len(), state_before);
4495        let judgments: Vec<Value> = db
4496            .capability(crate::semantic_decision::DEFAULT_DECISION_COLLECTION)
4497            .find(|_: &Value| true)
4498            .unwrap();
4499        assert!(judgments.is_empty());
4500
4501        // The same query without the clause answers as before.
4502        let page = execute_query(&db, &schema, &context, &plain, None).unwrap();
4503        assert_eq!(page.records.len(), 1);
4504    }
4505
4506    /// Over the wire, `semantic` is carried into the query rather than lost
4507    /// as an unknown key, `null` means absent, and an unrelated unknown key
4508    /// keeps its existing behavior of being ignored: only semantic intent is
4509    /// recognized, because only semantic intent changes what a query means.
4510    #[test]
4511    fn semantic_clause_survives_deserialization_and_changes_query_identity() {
4512        let plain: CanonicalQuery = serde_json::from_value(serde_json::json!({
4513            "collection": "incidents",
4514            "limit": 10,
4515        }))
4516        .unwrap();
4517        assert!(plain.semantic.is_none());
4518        let with_clause: CanonicalQuery = serde_json::from_value(serde_json::json!({
4519            "collection": "incidents",
4520            "limit": 10,
4521            "semantic": { "decision": "needs_human" },
4522        }))
4523        .unwrap();
4524        assert_eq!(
4525            with_clause.semantic,
4526            Some(serde_json::json!({ "decision": "needs_human" }))
4527        );
4528        assert_ne!(
4529            query_hash(&plain).unwrap(),
4530            query_hash(&with_clause).unwrap(),
4531            "a semantic clause is part of what the query is"
4532        );
4533        let malformed: CanonicalQuery = serde_json::from_value(serde_json::json!({
4534            "collection": "incidents",
4535            "semantic": "yes",
4536        }))
4537        .unwrap();
4538        assert_eq!(malformed.semantic, Some(Value::String("yes".into())));
4539        let null: CanonicalQuery = serde_json::from_value(serde_json::json!({
4540            "collection": "incidents",
4541            "semantic": null,
4542        }))
4543        .unwrap();
4544        assert!(null.semantic.is_none());
4545        let unrelated: CanonicalQuery = serde_json::from_value(serde_json::json!({
4546            "collection": "incidents",
4547            "limit": 10,
4548            "refill": true,
4549        }))
4550        .unwrap();
4551        assert_eq!(query_hash(&unrelated).unwrap(), query_hash(&plain).unwrap());
4552        // Serialization of a query without a clause is unchanged, so every
4553        // existing query hash and cursor binding is unchanged.
4554        assert!(!serde_json::to_string(&plain).unwrap().contains("semantic"));
4555    }
4556
4557    #[test]
4558    fn scoped_index_work_is_independent_of_unrelated_growth() {
4559        fn examined(unrelated: usize) -> usize {
4560            let db = db(&format!("scope-growth-{unrelated}"));
4561            let schema = scoped_schema();
4562            let operations = (0..20)
4563                .map(|i| (format!("target-{i}"), "tenant-a"))
4564                .chain((0..unrelated).map(|i| (format!("other-{i}"), "tenant-b")))
4565                .map(|(id, status)| TransactionOperation {
4566                    kind: TransactionOperationKind::Insert,
4567                    collection: "incidents".into(),
4568                    id: id.clone(),
4569                    value: serde_json::json!({"title": id, "status": status}),
4570                    if_version: None,
4571                })
4572                .collect();
4573            execute_transaction(
4574                &db,
4575                &schema,
4576                &TransactionRequest {
4577                    transaction_id: None,
4578                    tenant_id: "tenant".into(),
4579                    application_id: "app".into(),
4580                    revision_id: "rev".into(),
4581                    schema_version: 1,
4582                    state_namespace: None,
4583                    causal_parent: None,
4584                    authorization: auth(),
4585                    preconditions: vec![],
4586                    operations,
4587                    fences: vec![],
4588                },
4589                None,
4590            )
4591            .unwrap();
4592            let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4593            let result = execute_query(
4594                &db,
4595                &schema,
4596                &context,
4597                &CanonicalQuery {
4598                    collection: "incidents".into(),
4599                    filter: Some(QueryFilter::Eq {
4600                        field: "status".into(),
4601                        value: Value::String("tenant-a".into()),
4602                    }),
4603                    order_by: vec![],
4604                    limit: Some(20),
4605                    offset: 0,
4606                    cursor: None,
4607                    projection: vec![],
4608                    group_by: vec![],
4609                    aggregates: vec![],
4610                    references: vec![],
4611                    semantic: None,
4612                },
4613                None,
4614            )
4615            .unwrap();
4616            assert_eq!(result.records.len(), 20);
4617            result.plan.actual_rows_scanned.unwrap()
4618        }
4619        assert_eq!(
4620            [examined(100), examined(1000), examined(10_000)],
4621            [20, 20, 20]
4622        );
4623    }
4624
4625    #[test]
4626    fn managed_scope_without_schema_index_fails_closed() {
4627        let db = db("scope-missing-index");
4628        let mut schema = schema();
4629        schema.collections[0].fields[0].name = "tenant_id".into();
4630        execute_transaction(
4631            &db,
4632            &schema,
4633            &TransactionRequest {
4634                transaction_id: None,
4635                tenant_id: "tenant".into(),
4636                application_id: "app".into(),
4637                revision_id: "rev".into(),
4638                schema_version: 1,
4639                state_namespace: None,
4640                causal_parent: None,
4641                authorization: auth(),
4642                preconditions: vec![],
4643                operations: vec![TransactionOperation {
4644                    kind: TransactionOperationKind::Insert,
4645                    collection: "incidents".into(),
4646                    id: "a".into(),
4647                    value: serde_json::json!({"tenant_id":"tenant-a"}),
4648                    if_version: None,
4649                }],
4650                fences: vec![],
4651            },
4652            None,
4653        )
4654        .unwrap();
4655        let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
4656        let failure = execute_query(
4657            &db,
4658            &schema,
4659            &context,
4660            &CanonicalQuery {
4661                collection: "incidents".into(),
4662                filter: Some(QueryFilter::Eq {
4663                    field: "tenant_id".into(),
4664                    value: Value::String("tenant-a".into()),
4665                }),
4666                order_by: vec![],
4667                limit: Some(20),
4668                offset: 0,
4669                cursor: None,
4670                projection: vec![],
4671                group_by: vec![],
4672                aggregates: vec![],
4673                references: vec![],
4674                semantic: None,
4675            },
4676            None,
4677        )
4678        .unwrap_err();
4679        assert_eq!(failure.code, "INDEX_UNAVAILABLE");
4680    }
4681    #[test]
4682    #[ignore]
4683    fn gate_a_46_collection_sherpa_workload_benchmark() {
4684        use std::time::Instant;
4685        fn sherpa_schema() -> StateSchema {
4686            let mut collections = vec![];
4687            for i in 1..=46 {
4688                collections.push(CollectionSchema {
4689                    name: format!("collection_{}", i),
4690                    version: 1,
4691                    fields: vec![
4692                        FieldSchema {
4693                            name: "id".into(),
4694                            field_type: FieldType::Primitive {
4695                                primitive: PrimitiveType::String,
4696                            },
4697                            nullable: false,
4698                            required: true,
4699                            default: None,
4700                            constraints: Default::default(),
4701                            computed: None,
4702                        },
4703                        FieldSchema {
4704                            name: "name".into(),
4705                            field_type: FieldType::Primitive {
4706                                primitive: PrimitiveType::String,
4707                            },
4708                            nullable: false,
4709                            required: true,
4710                            default: None,
4711                            constraints: Default::default(),
4712                            computed: None,
4713                        },
4714                        FieldSchema {
4715                            name: "status".into(),
4716                            field_type: FieldType::Enum {
4717                                values: vec!["active".into(), "inactive".into()],
4718                            },
4719                            nullable: false,
4720                            required: false,
4721                            default: Some(Value::String("active".into())),
4722                            constraints: Default::default(),
4723                            computed: None,
4724                        },
4725                    ],
4726                    indexes: vec![
4727                        SchemaIndex {
4728                            name: "id_idx".into(),
4729                            fields: vec!["id".into()],
4730                            unique: false,
4731                            sparse: false,
4732                            version: 1,
4733                        },
4734                        SchemaIndex {
4735                            name: "status_idx".into(),
4736                            fields: vec!["status".into()],
4737                            unique: false,
4738                            sparse: false,
4739                            version: 1,
4740                        },
4741                    ],
4742                });
4743            }
4744            StateSchema {
4745                contract_version: 1,
4746                schema_version: 1,
4747                application_id: "app".into(),
4748                revision_id: "rev".into(),
4749                collections,
4750            }
4751        }
4752        let db = db("gate-a-sherpa-bench");
4753        let schema = sherpa_schema();
4754        let auth_ctx = auth();
4755        println!("Gate A Benchmark: Loading 46 collections...");
4756        let load_start = Instant::now();
4757        for col_idx in 1..=46 {
4758            let collection_name = format!("collection_{}", col_idx);
4759            let records_per_collection = 500;
4760            let mut operations = vec![];
4761            for record_idx in 1..=records_per_collection {
4762                operations.push(TransactionOperation {
4763                    kind: TransactionOperationKind::Insert,
4764                    collection: collection_name.clone(),
4765                    id: format!("rec-{}", record_idx),
4766                    value: serde_json::json!({
4767                        "id": format!("id-{}", record_idx),
4768                        "name": format!("Record {}", record_idx),
4769                        "status": if record_idx % 2 == 0 { "active" } else { "inactive" }
4770                    }),
4771                    if_version: None,
4772                });
4773            }
4774            let request = TransactionRequest {
4775                transaction_id: None,
4776                tenant_id: "tenant".into(),
4777                application_id: "app".into(),
4778                revision_id: "rev".into(),
4779                schema_version: 1,
4780                state_namespace: None,
4781                causal_parent: None,
4782                authorization: auth_ctx.clone(),
4783                preconditions: vec![],
4784                operations,
4785                fences: vec![],
4786            };
4787            execute_transaction(&db, &schema, &request, None).unwrap();
4788        }
4789        let load_ms = load_start.elapsed().as_secs_f64() * 1000.0;
4790        println!("  Loaded ~23,000 records in {:.2}ms", load_ms);
4791        println!("Gate A Benchmark: Executing 46 indexed queries...");
4792        let query_start = Instant::now();
4793        let context = begin_read(&db, &schema, "app", auth_ctx).unwrap();
4794        let mut query_times = vec![];
4795        let mut index_used_count = 0;
4796        for col_idx in 1..=46 {
4797            let collection_name = format!("collection_{}", col_idx);
4798            let q_start = Instant::now();
4799            let result = execute_query(
4800                &db,
4801                &schema,
4802                &context,
4803                &CanonicalQuery {
4804                    collection: collection_name,
4805                    filter: Some(QueryFilter::Eq {
4806                        field: "status".into(),
4807                        value: Value::String("active".into()),
4808                    }),
4809                    order_by: vec![],
4810                    limit: Some(1000),
4811                    offset: 0,
4812                    cursor: None,
4813                    projection: vec![],
4814                    group_by: vec![],
4815                    aggregates: vec![],
4816                    references: vec![],
4817                    semantic: None,
4818                },
4819                None,
4820            )
4821            .unwrap();
4822            let q_ms = q_start.elapsed().as_secs_f64() * 1000.0;
4823            query_times.push(q_ms);
4824            assert_eq!(
4825                result.plan.access_method,
4826                Some("index".to_string()),
4827                "Query {} should use index",
4828                col_idx
4829            );
4830            if result
4831                .plan
4832                .index_metrics
4833                .as_ref()
4834                .unwrap()
4835                .index_used
4836                .is_some()
4837            {
4838                index_used_count += 1;
4839            }
4840        }
4841        let total_query_time_ms = query_start.elapsed().as_secs_f64() * 1000.0;
4842        query_times.sort_by(|a, b| a.partial_cmp(b).unwrap());
4843        let median_query_time = query_times[query_times.len() / 2];
4844        println!("\n=== Gate A Verification ===");
4845        println!("Indexes used: {}/46", index_used_count);
4846        println!(
4847            "Total query time: {:.2}ms (target: <10,000ms)",
4848            total_query_time_ms
4849        );
4850        println!(
4851            "Per-query median: {:.2}ms (target: <100ms)",
4852            median_query_time
4853        );
4854        assert_eq!(index_used_count, 46, "All queries must use indexes");
4855        assert!(
4856            total_query_time_ms < 10000.0,
4857            "Total time must be <10s (was {:.2}ms)",
4858            total_query_time_ms
4859        );
4860    }
4861
4862    #[test]
4863    fn authenticated_policy_allows_query_with_capability() {
4864        let db = db("auth_read_allow");
4865        let schema = schema();
4866
4867        // Actor with "state:read" capability
4868        let mut auth_context = auth();
4869        auth_context.subject = "user:alice".into();
4870
4871        let context = begin_read(&db, &schema, "app", auth_context).unwrap();
4872        let query = CanonicalQuery {
4873            collection: "incidents".into(),
4874            filter: None,
4875            order_by: vec![],
4876            limit: Some(100),
4877            offset: 0,
4878            cursor: None,
4879            projection: vec![],
4880            group_by: vec![],
4881            aggregates: vec![],
4882            references: vec![],
4883            semantic: None,
4884        };
4885
4886        let result = execute_query(&db, &schema, &context, &query, None);
4887        assert!(
4888            result.is_ok(),
4889            "Query should succeed with state:read capability"
4890        );
4891    }
4892
4893    #[test]
4894    fn authenticated_policy_denies_query_without_capability() {
4895        let db = db("auth_read_deny");
4896        let schema = schema();
4897
4898        // Actor without "state:read" capability (unauthenticated equivalent)
4899        let auth_context = AuthorizationContext {
4900            subject: "user:bob".into(),
4901            tenant_id: "tenant".into(),
4902            application_id: "app".into(),
4903            revision_id: "rev".into(),
4904            capabilities: BTreeSet::new(), // No capabilities
4905            readable_collections: Default::default(),
4906            writable_collections: Default::default(),
4907            field_projections: Default::default(),
4908        };
4909
4910        let context = begin_read(&db, &schema, "app", auth_context).unwrap();
4911        let query = CanonicalQuery {
4912            collection: "incidents".into(),
4913            filter: None,
4914            order_by: vec![],
4915            limit: Some(100),
4916            offset: 0,
4917            cursor: None,
4918            projection: vec![],
4919            group_by: vec![],
4920            aggregates: vec![],
4921            references: vec![],
4922            semantic: None,
4923        };
4924
4925        let result = execute_query(&db, &schema, &context, &query, None);
4926        assert!(
4927            result.is_err(),
4928            "Query should fail without state:read capability"
4929        );
4930        if let Err(e) = result {
4931            assert_eq!(e.code, "AUTHORIZATION_DENIED");
4932        }
4933    }
4934
4935    #[test]
4936    fn write_policy_requires_state_write_capability() {
4937        let db = db("auth_write_deny");
4938        let schema = schema();
4939
4940        // Transaction without state:write capability
4941        let auth_context = AuthorizationContext {
4942            subject: "user:charlie".into(),
4943            tenant_id: "tenant".into(),
4944            application_id: "app".into(),
4945            revision_id: "rev".into(),
4946            capabilities: BTreeSet::new(), // No capabilities
4947            readable_collections: Default::default(),
4948            writable_collections: Default::default(),
4949            field_projections: Default::default(),
4950        };
4951
4952        let request = TransactionRequest {
4953            transaction_id: None,
4954            tenant_id: "tenant".into(),
4955            application_id: "app".into(),
4956            revision_id: "rev".into(),
4957            schema_version: 1,
4958            state_namespace: None,
4959            causal_parent: None,
4960            authorization: auth_context,
4961            operations: vec![TransactionOperation {
4962                kind: TransactionOperationKind::Insert,
4963                collection: "incidents".into(),
4964                id: "proj-1".into(),
4965                value: serde_json::json!({"title": "Test Project"}),
4966                if_version: None,
4967            }],
4968            preconditions: vec![],
4969            fences: vec![],
4970        };
4971
4972        let result = execute_transaction(&db, &schema, &request, None);
4973        assert!(
4974            result.is_err(),
4975            "Transaction should fail without state:write capability"
4976        );
4977        if let Err(e) = result {
4978            assert_eq!(e.code, "AUTHORIZATION_DENIED");
4979        }
4980    }
4981
4982    // PR #2.1 Verification Tests: Prove runtime enforcement works end-to-end
4983
4984    #[test]
4985    fn policy_enforcement_unauthenticated_read_denied() {
4986        // Test: Unauthenticated actor cannot read even with valid query
4987        let db = db("policy_unauth_read");
4988        let schema = schema();
4989
4990        // Pre-populate data
4991        let insert_request = TransactionRequest {
4992            transaction_id: None,
4993            tenant_id: "tenant".into(),
4994            application_id: "app".into(),
4995            revision_id: "rev".into(),
4996            schema_version: 1,
4997            state_namespace: None,
4998            causal_parent: None,
4999            authorization: auth(),
5000            operations: vec![TransactionOperation {
5001                kind: TransactionOperationKind::Insert,
5002                collection: "incidents".into(),
5003                id: "inc-1".into(),
5004                value: serde_json::json!({"title": "Test Incident"}),
5005                if_version: None,
5006            }],
5007            preconditions: vec![],
5008            fences: vec![],
5009        };
5010        execute_transaction(&db, &schema, &insert_request, None).unwrap();
5011
5012        // Unauthenticated read attempt (no capabilities)
5013        let unauth_context = AuthorizationContext {
5014            subject: "anonymous".into(),
5015            tenant_id: "tenant".into(),
5016            application_id: "app".into(),
5017            revision_id: "rev".into(),
5018            capabilities: BTreeSet::new(), // No capabilities
5019            readable_collections: Default::default(),
5020            writable_collections: Default::default(),
5021            field_projections: Default::default(),
5022        };
5023
5024        let context = begin_read(&db, &schema, "app", unauth_context).unwrap();
5025        let query = CanonicalQuery {
5026            collection: "incidents".into(),
5027            filter: None,
5028            order_by: vec![],
5029            limit: Some(100),
5030            offset: 0,
5031            cursor: None,
5032            projection: vec![],
5033            group_by: vec![],
5034            aggregates: vec![],
5035            references: vec![],
5036            semantic: None,
5037        };
5038
5039        let result = execute_query(&db, &schema, &context, &query, None);
5040        assert!(result.is_err(), "Unauthenticated read must be denied");
5041        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5042    }
5043
5044    #[test]
5045    fn policy_enforcement_unauthenticated_write_denied() {
5046        // Test: Unauthenticated actor cannot write
5047        let db = db("policy_unauth_write");
5048        let schema = schema();
5049
5050        let unauth_context = AuthorizationContext {
5051            subject: "anonymous".into(),
5052            tenant_id: "tenant".into(),
5053            application_id: "app".into(),
5054            revision_id: "rev".into(),
5055            capabilities: BTreeSet::new(), // No capabilities
5056            readable_collections: Default::default(),
5057            writable_collections: Default::default(),
5058            field_projections: Default::default(),
5059        };
5060
5061        let request = TransactionRequest {
5062            transaction_id: None,
5063            tenant_id: "tenant".into(),
5064            application_id: "app".into(),
5065            revision_id: "rev".into(),
5066            schema_version: 1,
5067            state_namespace: None,
5068            causal_parent: None,
5069            authorization: unauth_context,
5070            operations: vec![TransactionOperation {
5071                kind: TransactionOperationKind::Insert,
5072                collection: "incidents".into(),
5073                id: "inc-1".into(),
5074                value: serde_json::json!({"title": "Incident"}),
5075                if_version: None,
5076            }],
5077            preconditions: vec![],
5078            fences: vec![],
5079        };
5080
5081        let result = execute_transaction(&db, &schema, &request, None);
5082        assert!(result.is_err(), "Unauthenticated write must be denied");
5083        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5084    }
5085
5086    #[test]
5087    fn policy_enforcement_authenticated_read_allowed() {
5088        // Test: Authenticated actor can read with proper capability
5089        let db = db("policy_auth_read_ok");
5090        let schema = schema();
5091
5092        // Pre-populate data
5093        let insert_request = TransactionRequest {
5094            transaction_id: None,
5095            tenant_id: "tenant".into(),
5096            application_id: "app".into(),
5097            revision_id: "rev".into(),
5098            schema_version: 1,
5099            state_namespace: None,
5100            causal_parent: None,
5101            authorization: auth(),
5102            operations: vec![TransactionOperation {
5103                kind: TransactionOperationKind::Insert,
5104                collection: "incidents".into(),
5105                id: "inc-1".into(),
5106                value: serde_json::json!({"title": "Test Incident"}),
5107                if_version: None,
5108            }],
5109            preconditions: vec![],
5110            fences: vec![],
5111        };
5112        execute_transaction(&db, &schema, &insert_request, None).unwrap();
5113
5114        // Authenticated read (with state:read capability)
5115        let auth_context = AuthorizationContext {
5116            subject: "user:alice".into(),
5117            tenant_id: "tenant".into(),
5118            application_id: "app".into(),
5119            revision_id: "rev".into(),
5120            capabilities: ["state:read".into()].into(),
5121            readable_collections: Default::default(),
5122            writable_collections: Default::default(),
5123            field_projections: Default::default(),
5124        };
5125
5126        let context = begin_read(&db, &schema, "app", auth_context).unwrap();
5127        let query = CanonicalQuery {
5128            collection: "incidents".into(),
5129            filter: None,
5130            order_by: vec![],
5131            limit: Some(100),
5132            offset: 0,
5133            cursor: None,
5134            projection: vec![],
5135            group_by: vec![],
5136            aggregates: vec![],
5137            references: vec![],
5138            semantic: None,
5139        };
5140
5141        let result = execute_query(&db, &schema, &context, &query, None);
5142        assert!(result.is_ok(), "Authenticated read must be allowed");
5143        let query_result = result.unwrap();
5144        assert!(
5145            query_result.records.len() >= 1,
5146            "Query should return the inserted record"
5147        );
5148    }
5149
5150    #[test]
5151    fn policy_enforcement_authenticated_write_allowed() {
5152        // Test: Authenticated actor can write with proper capability
5153        let db = db("policy_auth_write_ok");
5154        let schema = schema();
5155
5156        let auth_context = AuthorizationContext {
5157            subject: "user:bob".into(),
5158            tenant_id: "tenant".into(),
5159            application_id: "app".into(),
5160            revision_id: "rev".into(),
5161            capabilities: ["state:write".into()].into(),
5162            readable_collections: Default::default(),
5163            writable_collections: Default::default(),
5164            field_projections: Default::default(),
5165        };
5166
5167        let request = TransactionRequest {
5168            transaction_id: None,
5169            tenant_id: "tenant".into(),
5170            application_id: "app".into(),
5171            revision_id: "rev".into(),
5172            schema_version: 1,
5173            state_namespace: None,
5174            causal_parent: None,
5175            authorization: auth_context,
5176            operations: vec![TransactionOperation {
5177                kind: TransactionOperationKind::Insert,
5178                collection: "incidents".into(),
5179                id: "inc-1".into(),
5180                value: serde_json::json!({"title": "New Incident"}),
5181                if_version: None,
5182            }],
5183            preconditions: vec![],
5184            fences: vec![],
5185        };
5186
5187        let result = execute_transaction(&db, &schema, &request, None);
5188        assert!(result.is_ok(), "Authenticated write must be allowed");
5189        assert!(
5190            result.unwrap().state_after > 0,
5191            "Transaction should have succeeded"
5192        );
5193    }
5194
5195    #[test]
5196    fn policy_enforcement_transaction_atomicity_with_auth() {
5197        // Test: Transaction authorization is checked before any operations commit
5198        let db = db("policy_tx_atomic");
5199        let schema = schema();
5200
5201        // Start with one record
5202        let insert_initial = TransactionRequest {
5203            transaction_id: None,
5204            tenant_id: "tenant".into(),
5205            application_id: "app".into(),
5206            revision_id: "rev".into(),
5207            schema_version: 1,
5208            state_namespace: None,
5209            causal_parent: None,
5210            authorization: auth(),
5211            operations: vec![TransactionOperation {
5212                kind: TransactionOperationKind::Insert,
5213                collection: "incidents".into(),
5214                id: "initial".into(),
5215                value: serde_json::json!({"title": "Initial"}),
5216                if_version: None,
5217            }],
5218            preconditions: vec![],
5219            fences: vec![],
5220        };
5221        let initial_result = execute_transaction(&db, &schema, &insert_initial, None).unwrap();
5222        let state_before_denied_tx = initial_result.state_after;
5223
5224        // Attempt transaction without authorization
5225        let unauth_context = AuthorizationContext {
5226            subject: "anonymous".into(),
5227            tenant_id: "tenant".into(),
5228            application_id: "app".into(),
5229            revision_id: "rev".into(),
5230            capabilities: BTreeSet::new(), // No capabilities
5231            readable_collections: Default::default(),
5232            writable_collections: Default::default(),
5233            field_projections: Default::default(),
5234        };
5235
5236        let denied_request = TransactionRequest {
5237            transaction_id: None,
5238            tenant_id: "tenant".into(),
5239            application_id: "app".into(),
5240            revision_id: "rev".into(),
5241            schema_version: 1,
5242            state_namespace: None,
5243            causal_parent: None,
5244            authorization: unauth_context,
5245            operations: vec![TransactionOperation {
5246                kind: TransactionOperationKind::Insert,
5247                collection: "incidents".into(),
5248                id: "denied".into(),
5249                value: serde_json::json!({"title": "Should Not Commit"}),
5250                if_version: None,
5251            }],
5252            preconditions: vec![],
5253            fences: vec![],
5254        };
5255
5256        let denied_result = execute_transaction(&db, &schema, &denied_request, None);
5257        assert!(denied_result.is_err(), "Transaction should be denied");
5258
5259        // Verify state hasn't changed
5260        let verify_auth = auth();
5261        let verify_context = begin_read(&db, &schema, "app", verify_auth).unwrap();
5262        let verify_query = CanonicalQuery {
5263            collection: "incidents".into(),
5264            filter: None,
5265            order_by: vec![],
5266            limit: Some(100),
5267            offset: 0,
5268            cursor: None,
5269            projection: vec![],
5270            group_by: vec![],
5271            aggregates: vec![],
5272            references: vec![],
5273            semantic: None,
5274        };
5275
5276        let verify_result =
5277            execute_query(&db, &schema, &verify_context, &verify_query, None).unwrap();
5278        assert_eq!(
5279            verify_result.records.len(),
5280            1,
5281            "Only initial record should exist; denied transaction must not partially commit"
5282        );
5283    }
5284
5285    #[test]
5286    fn policy_enforcement_capability_must_match_operation() {
5287        // Test: Having state:read doesn't allow writes
5288        let db = db("policy_cap_mismatch");
5289        let schema = schema();
5290
5291        let read_only_context = AuthorizationContext {
5292            subject: "user:charlie".into(),
5293            tenant_id: "tenant".into(),
5294            application_id: "app".into(),
5295            revision_id: "rev".into(),
5296            capabilities: ["state:read".into()].into(), // Only read capability
5297            readable_collections: Default::default(),
5298            writable_collections: Default::default(),
5299            field_projections: Default::default(),
5300        };
5301
5302        let write_request = TransactionRequest {
5303            transaction_id: None,
5304            tenant_id: "tenant".into(),
5305            application_id: "app".into(),
5306            revision_id: "rev".into(),
5307            schema_version: 1,
5308            state_namespace: None,
5309            causal_parent: None,
5310            authorization: read_only_context,
5311            operations: vec![TransactionOperation {
5312                kind: TransactionOperationKind::Insert,
5313                collection: "incidents".into(),
5314                id: "inc-1".into(),
5315                value: serde_json::json!({"title": "Test"}),
5316                if_version: None,
5317            }],
5318            preconditions: vec![],
5319            fences: vec![],
5320        };
5321
5322        let result = execute_transaction(&db, &schema, &write_request, None);
5323        assert!(
5324            result.is_err(),
5325            "Write must be denied even with read capability"
5326        );
5327        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5328    }
5329
5330    #[test]
5331    fn member_policy_read_allowed_when_member() {
5332        // Test: Member policy allows reads when actor is a member of the org
5333        let db = db("member_policy_read_allow");
5334        let schema = schema();
5335
5336        // Pre-populate a record with organization_id
5337        let insert_context = AuthorizationContext {
5338            subject: "user:admin".into(),
5339            tenant_id: "tenant".into(),
5340            application_id: "app".into(),
5341            revision_id: "rev".into(),
5342            capabilities: ["state:write".into()].into(),
5343            readable_collections: Default::default(),
5344            writable_collections: Default::default(),
5345            field_projections: Default::default(),
5346        };
5347
5348        let insert_request = TransactionRequest {
5349            transaction_id: None,
5350            tenant_id: "tenant".into(),
5351            application_id: "app".into(),
5352            revision_id: "rev".into(),
5353            schema_version: 1,
5354            state_namespace: None,
5355            causal_parent: None,
5356            authorization: insert_context,
5357            operations: vec![TransactionOperation {
5358                kind: TransactionOperationKind::Insert,
5359                collection: "incidents".into(),
5360                id: "proj-1".into(),
5361                value: serde_json::json!({"title": "Test Project"}),
5362                if_version: None,
5363            }],
5364            preconditions: vec![],
5365            fences: vec![],
5366        };
5367
5368        let _ = execute_transaction(&db, &schema, &insert_request, None);
5369
5370        // Now test member policy read with capability
5371        let member_context = AuthorizationContext {
5372            subject: "user:alice".into(),
5373            tenant_id: "tenant".into(),
5374            application_id: "app".into(),
5375            revision_id: "rev".into(),
5376            capabilities: ["state:read".into()].into(),
5377            readable_collections: Default::default(),
5378            writable_collections: Default::default(),
5379            field_projections: Default::default(),
5380        };
5381
5382        let read_context = begin_read(&db, &schema, "app", member_context).unwrap();
5383        let query = CanonicalQuery {
5384            collection: "incidents".into(),
5385            filter: None,
5386            order_by: vec![],
5387            limit: Some(100),
5388            offset: 0,
5389            cursor: None,
5390            projection: vec![],
5391            group_by: vec![],
5392            aggregates: vec![],
5393            references: vec![],
5394            semantic: None,
5395        };
5396
5397        let result = execute_query(&db, &schema, &read_context, &query, None);
5398        assert!(
5399            result.is_ok(),
5400            "Member policy read should be allowed with state:read"
5401        );
5402    }
5403
5404    #[test]
5405    fn member_policy_read_denied_without_capability() {
5406        // Test: Member policy denies reads without state:read capability
5407        let db = db("member_policy_read_deny");
5408        let schema = schema();
5409
5410        let empty_context = AuthorizationContext {
5411            subject: "user:bob".into(),
5412            tenant_id: "tenant".into(),
5413            application_id: "app".into(),
5414            revision_id: "rev".into(),
5415            capabilities: BTreeSet::new(), // No capabilities
5416            readable_collections: Default::default(),
5417            writable_collections: Default::default(),
5418            field_projections: Default::default(),
5419        };
5420
5421        let read_context = begin_read(&db, &schema, "app", empty_context).unwrap();
5422        let query = CanonicalQuery {
5423            collection: "incidents".into(),
5424            filter: None,
5425            order_by: vec![],
5426            limit: Some(100),
5427            offset: 0,
5428            cursor: None,
5429            projection: vec![],
5430            group_by: vec![],
5431            aggregates: vec![],
5432            references: vec![],
5433            semantic: None,
5434        };
5435
5436        let result = execute_query(&db, &schema, &read_context, &query, None);
5437        assert!(
5438            result.is_err(),
5439            "Member policy read must be denied without state:read"
5440        );
5441        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5442    }
5443
5444    #[test]
5445    fn member_policy_write_allowed_when_member() {
5446        // Test: Member policy allows writes when actor is a member of the org
5447        let db = db("member_policy_write_allow");
5448        let schema = schema();
5449
5450        let member_context = AuthorizationContext {
5451            subject: "user:alice".into(),
5452            tenant_id: "tenant".into(),
5453            application_id: "app".into(),
5454            revision_id: "rev".into(),
5455            capabilities: ["state:write".into()].into(),
5456            readable_collections: Default::default(),
5457            writable_collections: Default::default(),
5458            field_projections: Default::default(),
5459        };
5460
5461        let write_request = TransactionRequest {
5462            transaction_id: None,
5463            tenant_id: "tenant".into(),
5464            application_id: "app".into(),
5465            revision_id: "rev".into(),
5466            schema_version: 1,
5467            state_namespace: None,
5468            causal_parent: None,
5469            authorization: member_context,
5470            operations: vec![TransactionOperation {
5471                kind: TransactionOperationKind::Insert,
5472                collection: "incidents".into(),
5473                id: "inc-member-1".into(),
5474                value: serde_json::json!({"title": "Member Project"}),
5475                if_version: None,
5476            }],
5477            preconditions: vec![],
5478            fences: vec![],
5479        };
5480
5481        let result = execute_transaction(&db, &schema, &write_request, None);
5482        assert!(
5483            result.is_ok(),
5484            "Member policy write should be allowed with state:write"
5485        );
5486    }
5487
5488    #[test]
5489    fn member_policy_write_denied_without_capability() {
5490        // Test: Member policy denies writes without state:write capability
5491        let db = db("member_policy_write_deny");
5492        let schema = schema();
5493
5494        let empty_context = AuthorizationContext {
5495            subject: "user:bob".into(),
5496            tenant_id: "tenant".into(),
5497            application_id: "app".into(),
5498            revision_id: "rev".into(),
5499            capabilities: BTreeSet::new(), // No capabilities
5500            readable_collections: Default::default(),
5501            writable_collections: Default::default(),
5502            field_projections: Default::default(),
5503        };
5504
5505        let write_request = TransactionRequest {
5506            transaction_id: None,
5507            tenant_id: "tenant".into(),
5508            application_id: "app".into(),
5509            revision_id: "rev".into(),
5510            schema_version: 1,
5511            state_namespace: None,
5512            causal_parent: None,
5513            authorization: empty_context,
5514            operations: vec![TransactionOperation {
5515                kind: TransactionOperationKind::Insert,
5516                collection: "incidents".into(),
5517                id: "inc-member-2".into(),
5518                value: serde_json::json!({"title": "Denied Project"}),
5519                if_version: None,
5520            }],
5521            preconditions: vec![],
5522            fences: vec![],
5523        };
5524
5525        let result = execute_transaction(&db, &schema, &write_request, None);
5526        assert!(
5527            result.is_err(),
5528            "Member policy write must be denied without state:write"
5529        );
5530        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5531    }
5532
5533    #[test]
5534    fn member_policy_cross_org_mutation_prevented() {
5535        // Test: Cross-organization mutation attack is prevented by lack of state:write
5536        // Scenario: user:alice is member of org-1 but tries to create/modify resource in org-2
5537        // Expected: Denied because server wouldn't grant state:write capability for org-2
5538        let db = db("member_policy_cross_org");
5539        let schema = schema();
5540
5541        // Admin inserts record in org-2
5542        let admin_context = AuthorizationContext {
5543            subject: "user:admin".into(),
5544            tenant_id: "tenant".into(),
5545            application_id: "app".into(),
5546            revision_id: "rev".into(),
5547            capabilities: ["state:write".into()].into(),
5548            readable_collections: Default::default(),
5549            writable_collections: Default::default(),
5550            field_projections: Default::default(),
5551        };
5552
5553        let insert_in_org2 = TransactionRequest {
5554            transaction_id: None,
5555            tenant_id: "tenant".into(),
5556            application_id: "app".into(),
5557            revision_id: "rev".into(),
5558            schema_version: 1,
5559            state_namespace: None,
5560            causal_parent: None,
5561            authorization: admin_context,
5562            operations: vec![TransactionOperation {
5563                kind: TransactionOperationKind::Insert,
5564                collection: "incidents".into(),
5565                id: "inc-org2-1".into(),
5566                value: serde_json::json!({"title": "Org2 Resource"}),
5567                if_version: None,
5568            }],
5569            preconditions: vec![],
5570            fences: vec![],
5571        };
5572
5573        let _ = execute_transaction(&db, &schema, &insert_in_org2, None);
5574
5575        // Now user:alice (member of org-1 only) tries to modify org-2 resource
5576        // Without state:write capability (which would only be granted for org-1),
5577        // the mutation is denied
5578        let alice_context = AuthorizationContext {
5579            subject: "user:alice".into(),
5580            tenant_id: "tenant".into(),
5581            application_id: "app".into(),
5582            revision_id: "rev".into(),
5583            capabilities: BTreeSet::new(), // No state:write for org-2
5584            readable_collections: Default::default(),
5585            writable_collections: Default::default(),
5586            field_projections: Default::default(),
5587        };
5588
5589        let cross_org_mutation = 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: alice_context,
5598            operations: vec![TransactionOperation {
5599                kind: TransactionOperationKind::Update,
5600                collection: "incidents".into(),
5601                id: "inc-org2-1".into(),
5602                value: serde_json::json!({"title": "Hacked by alice"}),
5603                if_version: Some(0),
5604            }],
5605            preconditions: vec![],
5606            fences: vec![],
5607        };
5608
5609        let result = execute_transaction(&db, &schema, &cross_org_mutation, None);
5610        assert!(
5611            result.is_err(),
5612            "Cross-org mutation must be denied (member of org-1 only)"
5613        );
5614        assert_eq!(result.unwrap_err().code, "AUTHORIZATION_DENIED");
5615    }
5616
5617    #[test]
5618    fn record_authorization_owner_policy_filters_owned_records() {
5619        let db = db("record_auth_owner_filter");
5620        let schema = schema_with_owner();
5621
5622        let alice_context = AuthorizationContext {
5623            subject: "user:alice".into(),
5624            tenant_id: "tenant".into(),
5625            application_id: "app".into(),
5626            revision_id: "rev".into(),
5627            capabilities: ["state:read".into()].into(),
5628            readable_collections: Default::default(),
5629            writable_collections: Default::default(),
5630            field_projections: Default::default(),
5631        };
5632
5633        let bob_context = AuthorizationContext {
5634            subject: "user:bob".into(),
5635            tenant_id: "tenant".into(),
5636            application_id: "app".into(),
5637            revision_id: "rev".into(),
5638            capabilities: ["state:read".into()].into(),
5639            readable_collections: Default::default(),
5640            writable_collections: Default::default(),
5641            field_projections: Default::default(),
5642        };
5643
5644        let admin_context = AuthorizationContext {
5645            subject: "user:admin".into(),
5646            tenant_id: "tenant".into(),
5647            application_id: "app".into(),
5648            revision_id: "rev".into(),
5649            capabilities: ["state:write".into(), "state:read".into()].into(),
5650            readable_collections: Default::default(),
5651            writable_collections: Default::default(),
5652            field_projections: Default::default(),
5653        };
5654
5655        let insert_records = TransactionRequest {
5656            transaction_id: None,
5657            tenant_id: "tenant".into(),
5658            application_id: "app".into(),
5659            revision_id: "rev".into(),
5660            schema_version: 1,
5661            state_namespace: None,
5662            causal_parent: None,
5663            authorization: admin_context.clone(),
5664            operations: vec![
5665                TransactionOperation {
5666                    kind: TransactionOperationKind::Insert,
5667                    collection: "incidents".into(),
5668                    id: "inc-alice-1".into(),
5669                    value: serde_json::json!({"title": "Alice 1", "owner_id": "alice"}),
5670                    if_version: None,
5671                },
5672                TransactionOperation {
5673                    kind: TransactionOperationKind::Insert,
5674                    collection: "incidents".into(),
5675                    id: "inc-bob-1".into(),
5676                    value: serde_json::json!({"title": "Bob 1", "owner_id": "bob"}),
5677                    if_version: None,
5678                },
5679                TransactionOperation {
5680                    kind: TransactionOperationKind::Insert,
5681                    collection: "incidents".into(),
5682                    id: "inc-alice-2".into(),
5683                    value: serde_json::json!({"title": "Alice 2", "owner_id": "alice"}),
5684                    if_version: None,
5685                },
5686            ],
5687            preconditions: vec![],
5688            fences: vec![],
5689        };
5690
5691        execute_transaction(&db, &schema, &insert_records, None).expect("insert should succeed");
5692
5693        let alice_read = begin_read(&db, &schema, "app", alice_context).unwrap();
5694        let query = CanonicalQuery {
5695            collection: "incidents".into(),
5696            filter: None,
5697            order_by: vec![],
5698            limit: None,
5699            offset: 0,
5700            cursor: None,
5701            projection: vec!["title".into(), "owner_id".into()],
5702            group_by: vec![],
5703            aggregates: vec![],
5704            references: vec![],
5705            semantic: None,
5706        };
5707
5708        let alice_result = execute_query(
5709            &db,
5710            &schema,
5711            &alice_read,
5712            &query,
5713            Some(PolicySubject::Owner),
5714        )
5715        .expect("query should succeed");
5716
5717        assert_eq!(
5718            alice_result.records.len(),
5719            2,
5720            "Alice should see only 2 incidents she owns"
5721        );
5722        for record in &alice_result.records {
5723            assert_eq!(record.get("owner_id").unwrap(), "alice");
5724        }
5725
5726        let bob_read = begin_read(&db, &schema, "app", bob_context).unwrap();
5727        let bob_result = execute_query(&db, &schema, &bob_read, &query, Some(PolicySubject::Owner))
5728            .expect("query should succeed");
5729
5730        assert_eq!(
5731            bob_result.records.len(),
5732            1,
5733            "Bob should see only 1 incident he owns"
5734        );
5735        assert_eq!(bob_result.records[0].get("owner_id").unwrap(), "bob");
5736    }
5737
5738    #[test]
5739    fn record_authorization_owner_policy_denies_direct_access() {
5740        let db = db("record_auth_owner_direct");
5741        let schema = schema_with_owner();
5742
5743        let admin_context = AuthorizationContext {
5744            subject: "user:admin".into(),
5745            tenant_id: "tenant".into(),
5746            application_id: "app".into(),
5747            revision_id: "rev".into(),
5748            capabilities: ["state:write".into(), "state:read".into()].into(),
5749            readable_collections: Default::default(),
5750            writable_collections: Default::default(),
5751            field_projections: Default::default(),
5752        };
5753
5754        let bob_context = AuthorizationContext {
5755            subject: "user:bob".into(),
5756            tenant_id: "tenant".into(),
5757            application_id: "app".into(),
5758            revision_id: "rev".into(),
5759            capabilities: ["state:read".into()].into(),
5760            readable_collections: Default::default(),
5761            writable_collections: Default::default(),
5762            field_projections: Default::default(),
5763        };
5764
5765        let insert_request = TransactionRequest {
5766            transaction_id: None,
5767            tenant_id: "tenant".into(),
5768            application_id: "app".into(),
5769            revision_id: "rev".into(),
5770            schema_version: 1,
5771            state_namespace: None,
5772            causal_parent: None,
5773            authorization: admin_context,
5774            operations: vec![TransactionOperation {
5775                kind: TransactionOperationKind::Insert,
5776                collection: "incidents".into(),
5777                id: "inc-alice-secret".into(),
5778                value: serde_json::json!({"title": "Secret", "owner_id": "alice"}),
5779                if_version: None,
5780            }],
5781            preconditions: vec![],
5782            fences: vec![],
5783        };
5784
5785        execute_transaction(&db, &schema, &insert_request, None).expect("insert should succeed");
5786
5787        let bob_read = begin_read(&db, &schema, "app", bob_context).unwrap();
5788        let direct_query = CanonicalQuery {
5789            collection: "incidents".into(),
5790            filter: Some(QueryFilter::Eq {
5791                field: "_id".into(),
5792                value: Value::String("inc-alice-secret".into()),
5793            }),
5794            order_by: vec![],
5795            limit: None,
5796            offset: 0,
5797            cursor: None,
5798            projection: vec!["title".into(), "owner_id".into()],
5799            group_by: vec![],
5800            aggregates: vec![],
5801            references: vec![],
5802            semantic: None,
5803        };
5804
5805        let result = execute_query(
5806            &db,
5807            &schema,
5808            &bob_read,
5809            &direct_query,
5810            Some(PolicySubject::Owner),
5811        )
5812        .expect("query should succeed");
5813
5814        assert_eq!(
5815            result.records.len(),
5816            0,
5817            "Bob should not see Alice's record even with direct _id filter"
5818        );
5819    }
5820
5821    #[test]
5822    fn record_authorization_authenticated_policy_allows_all() {
5823        let db = db("record_auth_authenticated");
5824        let schema = schema();
5825
5826        let alice_context = AuthorizationContext {
5827            subject: "user:alice".into(),
5828            tenant_id: "tenant".into(),
5829            application_id: "app".into(),
5830            revision_id: "rev".into(),
5831            capabilities: ["state:write".into(), "state:read".into()].into(),
5832            readable_collections: Default::default(),
5833            writable_collections: Default::default(),
5834            field_projections: Default::default(),
5835        };
5836
5837        let bob_context = AuthorizationContext {
5838            subject: "user:bob".into(),
5839            tenant_id: "tenant".into(),
5840            application_id: "app".into(),
5841            revision_id: "rev".into(),
5842            capabilities: ["state:read".into()].into(),
5843            readable_collections: Default::default(),
5844            writable_collections: Default::default(),
5845            field_projections: Default::default(),
5846        };
5847
5848        let insert_records = TransactionRequest {
5849            transaction_id: None,
5850            tenant_id: "tenant".into(),
5851            application_id: "app".into(),
5852            revision_id: "rev".into(),
5853            schema_version: 1,
5854            state_namespace: None,
5855            causal_parent: None,
5856            authorization: alice_context,
5857            operations: vec![
5858                TransactionOperation {
5859                    kind: TransactionOperationKind::Insert,
5860                    collection: "incidents".into(),
5861                    id: "inc-alice-auth".into(),
5862                    value: serde_json::json!({"title": "Alice's incident"}),
5863                    if_version: None,
5864                },
5865                TransactionOperation {
5866                    kind: TransactionOperationKind::Insert,
5867                    collection: "incidents".into(),
5868                    id: "inc-bob-auth".into(),
5869                    value: serde_json::json!({"title": "Bob's incident"}),
5870                    if_version: None,
5871                },
5872            ],
5873            preconditions: vec![],
5874            fences: vec![],
5875        };
5876
5877        execute_transaction(&db, &schema, &insert_records, None).expect("insert should succeed");
5878
5879        let bob_read = begin_read(&db, &schema, "app", bob_context).unwrap();
5880        let query = CanonicalQuery {
5881            collection: "incidents".into(),
5882            filter: None,
5883            order_by: vec![],
5884            limit: None,
5885            offset: 0,
5886            cursor: None,
5887            projection: vec!["title".into()],
5888            group_by: vec![],
5889            aggregates: vec![],
5890            references: vec![],
5891            semantic: None,
5892        };
5893
5894        let result = execute_query(
5895            &db,
5896            &schema,
5897            &bob_read,
5898            &query,
5899            Some(PolicySubject::Authenticated),
5900        )
5901        .expect("query should succeed");
5902
5903        assert_eq!(
5904            result.records.len(),
5905            2,
5906            "Authenticated policy should allow access to all records for authenticated actor"
5907        );
5908    }
5909
5910    #[test]
5911    fn record_authorization_pagination_respects_authorization() {
5912        let db = db("record_auth_pagination");
5913        let schema = schema_with_owner();
5914
5915        let admin_context = AuthorizationContext {
5916            subject: "user:admin".into(),
5917            tenant_id: "tenant".into(),
5918            application_id: "app".into(),
5919            revision_id: "rev".into(),
5920            capabilities: ["state:write".into(), "state:read".into()].into(),
5921            readable_collections: Default::default(),
5922            writable_collections: Default::default(),
5923            field_projections: Default::default(),
5924        };
5925
5926        let alice_context = AuthorizationContext {
5927            subject: "user:alice".into(),
5928            tenant_id: "tenant".into(),
5929            application_id: "app".into(),
5930            revision_id: "rev".into(),
5931            capabilities: ["state:read".into()].into(),
5932            readable_collections: Default::default(),
5933            writable_collections: Default::default(),
5934            field_projections: Default::default(),
5935        };
5936
5937        let mut ops = vec![];
5938        for i in 0..5 {
5939            ops.push(TransactionOperation {
5940                kind: TransactionOperationKind::Insert,
5941                collection: "incidents".into(),
5942                id: format!("incident-{}", i),
5943                value: serde_json::json!({
5944                    "title": format!("Incident {}", i),
5945                    "owner_id": if i % 2 == 0 { "alice" } else { "bob" }
5946                }),
5947                if_version: None,
5948            });
5949        }
5950
5951        let insert_request = TransactionRequest {
5952            transaction_id: None,
5953            tenant_id: "tenant".into(),
5954            application_id: "app".into(),
5955            revision_id: "rev".into(),
5956            schema_version: 1,
5957            state_namespace: None,
5958            causal_parent: None,
5959            authorization: admin_context,
5960            operations: ops,
5961            preconditions: vec![],
5962            fences: vec![],
5963        };
5964
5965        execute_transaction(&db, &schema, &insert_request, None).expect("insert should succeed");
5966
5967        let alice_read = begin_read(&db, &schema, "app", alice_context).unwrap();
5968        let query_no_limit = CanonicalQuery {
5969            collection: "incidents".into(),
5970            filter: None,
5971            order_by: vec![QueryOrder {
5972                field: "_id".into(),
5973                direction: SortDirection::Asc,
5974            }],
5975            limit: None,
5976            offset: 0,
5977            cursor: None,
5978            projection: vec!["title".into(), "owner_id".into()],
5979            group_by: vec![],
5980            aggregates: vec![],
5981            references: vec![],
5982            semantic: None,
5983        };
5984
5985        let result = execute_query(
5986            &db,
5987            &schema,
5988            &alice_read,
5989            &query_no_limit,
5990            Some(PolicySubject::Owner),
5991        )
5992        .expect("query should succeed");
5993
5994        assert_eq!(
5995            result.records.len(),
5996            3,
5997            "Alice should see 3 incidents she owns (0, 2, 4)"
5998        );
5999        for record in &result.records {
6000            assert_eq!(record.get("owner_id").unwrap(), "alice");
6001        }
6002    }
6003}