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