create-feltdb 0.11.7 → 0.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,12 +21,12 @@
21
21
  //! fell back to scan without updating these fields.
22
22
 
23
23
  use crate::{
24
+ AtomicMutation, AtomicPrecondition, FeltDb, FlowError, PreconditionFailure, RecordPrecondition,
25
+ StoredRow,
24
26
  application::{ApplicationManifest, ApplicationRevision, FieldDefinition},
25
27
  policy_evaluation::{
26
28
  Actor, AuthorizationState, PolicyEvaluator, PolicySubject, RecordAuthorizationContext,
27
29
  },
28
- AtomicMutation, AtomicPrecondition, FeltDb, FlowError, PreconditionFailure, RecordPrecondition,
29
- StoredRow,
30
30
  };
31
31
  use serde::{Deserialize, Serialize};
32
32
  use serde_json::{Map, Value};
@@ -625,7 +625,7 @@ pub fn begin_query_read(
625
625
  state_namespace: &str,
626
626
  authorization: AuthorizationContext,
627
627
  ) -> Result<ReadContext, StateFailure> {
628
- begin_read_internal(db, schema, state_namespace, authorization, false)
628
+ build_read_context(db, schema, state_namespace, authorization, Vec::new(), true)
629
629
  }
630
630
 
631
631
  fn begin_read_internal(
@@ -649,6 +649,24 @@ fn begin_read_internal(
649
649
  } else {
650
650
  Vec::new()
651
651
  };
652
+ build_read_context(
653
+ db,
654
+ schema,
655
+ state_namespace,
656
+ authorization,
657
+ snapshot_rows,
658
+ !capture_snapshot,
659
+ )
660
+ }
661
+
662
+ fn 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> {
652
670
  Ok(ReadContext {
653
671
  application_id: schema.application_id.clone(),
654
672
  revision_id: schema.revision_id.clone(),
@@ -662,10 +680,131 @@ fn begin_read_internal(
662
680
  state_namespace: state_namespace.into(),
663
681
  authorization,
664
682
  snapshot_rows,
665
- live_query: !capture_snapshot,
683
+ live_query,
666
684
  })
667
685
  }
668
686
 
687
+ fn namespace_capability(state_namespace: &str, collection: &str) -> String {
688
+ format!("{state_namespace}:{collection}")
689
+ }
690
+
691
+ fn membership_collection_name(schema: &StateSchema) -> Option<String> {
692
+ AuthorizationState::new(Arc::new(schema.clone()), "", Arc::new(Vec::new()))
693
+ .find_membership_collection()
694
+ }
695
+
696
+ fn 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
+
708
+ fn 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
+
746
+ fn 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
+
669
808
  #[derive(Debug, Clone, Serialize, Deserialize)]
670
809
  #[serde(tag = "operator", rename_all = "camelCase")]
671
810
  pub enum QueryFilter {
@@ -1231,14 +1370,39 @@ pub fn execute_query(
1231
1370
  "read context does not match schema",
1232
1371
  ));
1233
1372
  }
1234
- let definition = collection(schema, &query.collection)?;
1235
- if !context.authorization.can("state:read")
1236
- || (!context.authorization.readable_collections.is_empty()
1237
- && !context
1238
- .authorization
1239
- .readable_collections
1240
- .contains(&query.collection))
1241
- {
1373
+ // Substrate collections carry no application schema: their records are
1374
+ // validated by the substrate that writes them, and they are read only
1375
+ // under `composition:read`.
1376
+ let substrate = is_substrate_collection(&query.collection);
1377
+ let substrate_definition;
1378
+ let definition = if substrate {
1379
+ if !query.references.is_empty() {
1380
+ return Err(StateFailure::new(
1381
+ "INVALID_QUERY",
1382
+ "substrate collections do not expand references",
1383
+ ));
1384
+ }
1385
+ substrate_definition = CollectionSchema {
1386
+ name: query.collection.clone(),
1387
+ version: 1,
1388
+ fields: vec![],
1389
+ indexes: vec![],
1390
+ };
1391
+ &substrate_definition
1392
+ } else {
1393
+ collection(schema, &query.collection)?
1394
+ };
1395
+ let readable = if substrate {
1396
+ context.authorization.can("composition:read")
1397
+ } else {
1398
+ context.authorization.can("state:read")
1399
+ && (context.authorization.readable_collections.is_empty()
1400
+ || context
1401
+ .authorization
1402
+ .readable_collections
1403
+ .contains(&query.collection))
1404
+ };
1405
+ if !readable {
1242
1406
  return Err(StateFailure::new(
1243
1407
  "AUTHORIZATION_DENIED",
1244
1408
  "collection is not readable",
@@ -1271,7 +1435,7 @@ pub fn execute_query(
1271
1435
  for reference in &query.references {
1272
1436
  requested.insert(reference.field.clone());
1273
1437
  }
1274
- if requested.iter().any(|f| !available.contains(f)) {
1438
+ if !substrate && requested.iter().any(|f| !available.contains(f)) {
1275
1439
  return Err(StateFailure::new(
1276
1440
  "UNKNOWN_FIELD",
1277
1441
  "query references an unknown field",
@@ -1341,14 +1505,10 @@ pub fn execute_query(
1341
1505
  }
1342
1506
  }
1343
1507
 
1344
- let query_snapshot;
1345
- let snapshot_rows = if read_policy.is_some() || !query.references.is_empty() {
1346
- query_snapshot = _db
1347
- .state_rows_for_namespace(&context.state_namespace)
1348
- .map_err(StateFailure::storage)?;
1349
- &query_snapshot
1508
+ let policy_snapshot = if matches!(read_policy, Some(PolicySubject::Member)) {
1509
+ member_policy_snapshot_rows(_db, schema, &context.state_namespace)?
1350
1510
  } else {
1351
- &context.snapshot_rows
1511
+ Vec::new()
1352
1512
  };
1353
1513
 
1354
1514
  // Detect unfiltered COUNT(*) for maintained cardinality optimization
@@ -1433,7 +1593,10 @@ pub fn execute_query(
1433
1593
  (false, None, None)
1434
1594
  };
1435
1595
  let used_index = planned_index && context.live_query;
1596
+ // Substrate collections declare no indexes and already live inside the
1597
+ // application's tenant-scoped namespace; their bounded queries are exempt.
1436
1598
  if context.live_query
1599
+ && !substrate
1437
1600
  && query.filter.as_ref().is_some_and(contains_managed_scope)
1438
1601
  && !used_index
1439
1602
  {
@@ -1452,7 +1615,8 @@ pub fn execute_query(
1452
1615
  let execution_before = crate::query_execution_diagnostics::counters();
1453
1616
  let index_lookup_start = Instant::now();
1454
1617
  let selected_rows = if !context.live_query {
1455
- snapshot_rows
1618
+ context
1619
+ .snapshot_rows
1456
1620
  .iter()
1457
1621
  .filter(|row| {
1458
1622
  row.capability == capability
@@ -1523,22 +1687,26 @@ pub fn execute_query(
1523
1687
  .map(|id| Actor::new(id))
1524
1688
  };
1525
1689
 
1526
- // Create authorization state for policies that need to verify relationships
1527
- let auth_state = AuthorizationState::new(
1528
- Arc::new(schema.clone()),
1529
- context.state_namespace.clone(),
1530
- Arc::new(snapshot_rows.clone()),
1531
- );
1690
+ let auth_state = matches!(policy, &PolicySubject::Member).then(|| {
1691
+ AuthorizationState::new(
1692
+ Arc::new(schema.clone()),
1693
+ context.state_namespace.clone(),
1694
+ Arc::new(policy_snapshot.clone()),
1695
+ )
1696
+ });
1532
1697
 
1533
1698
  values.retain(|record_value| {
1534
1699
  if let Some(record_id) = record_value.get("_id").and_then(Value::as_str) {
1535
- let record_context = RecordAuthorizationContext {
1700
+ let mut record_context = RecordAuthorizationContext {
1536
1701
  actor: actor.clone(),
1537
1702
  collection: query.collection.clone(),
1538
1703
  record_id: record_id.to_string(),
1539
1704
  record_value: record_value.clone(),
1540
- authorization_state: Some(auth_state.clone()),
1705
+ authorization_state: None,
1541
1706
  };
1707
+ if let Some(state) = &auth_state {
1708
+ record_context.authorization_state = Some(state.clone());
1709
+ }
1542
1710
 
1543
1711
  PolicyEvaluator::evaluate_record(policy.clone(), &record_context).is_ok()
1544
1712
  } else {
@@ -1547,19 +1715,18 @@ pub fn execute_query(
1547
1715
  });
1548
1716
  }
1549
1717
 
1718
+ let reference_rows =
1719
+ load_reference_rows(_db, &context.state_namespace, &values, &query.references)?;
1550
1720
  for value in &mut values {
1551
1721
  for reference in &query.references {
1552
1722
  let id = value.get(&reference.field).and_then(Value::as_str);
1553
1723
  let related = id
1554
1724
  .and_then(|id| {
1555
- snapshot_rows.iter().find(|row| {
1556
- row.capability
1557
- == format!("{}:{}", context.state_namespace, reference.collection)
1558
- && row.key == id
1559
- && !row.deleted
1560
- })
1725
+ reference_rows
1726
+ .get(&(reference.collection.clone(), id.to_string()))
1727
+ .filter(|value| !value.is_null())
1561
1728
  })
1562
- .map(|row| projected(&row.value, &reference.projection))
1729
+ .map(|value| projected(value, &reference.projection))
1563
1730
  .unwrap_or(Value::Null);
1564
1731
  if let Some(object) = value.as_object_mut() {
1565
1732
  object.insert(reference.as_name.clone(), related);
@@ -1826,6 +1993,98 @@ pub struct TransactionRequest {
1826
1993
  pub operations: Vec<TransactionOperation>,
1827
1994
  #[serde(default)]
1828
1995
  pub preconditions: Vec<TransactionPrecondition>,
1996
+ /// Read bases this authority issued for this state namespace
1997
+ /// (`capture_read_basis`), re-evaluated inside the commit lock.
1998
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
1999
+ pub fences: Vec<crate::ReadBasis>,
2000
+ }
2001
+
2002
+ /// Collections the composition substrate keeps inside an application's state
2003
+ /// namespace. They are not part of the application contract: their records
2004
+ /// are validated by the substrate, and reading or writing them requires the
2005
+ /// dedicated `composition:read` / `composition:write` capabilities rather
2006
+ /// than `state:*`. Every other `_feltdb` name remains unwritable here.
2007
+ pub const SUBSTRATE_COLLECTIONS: &[&str] = &[
2008
+ "_feltdb.CompositionContract",
2009
+ "_feltdb.Composition",
2010
+ "_feltdb.CompositionVersion",
2011
+ "_feltdb.CompositionTopology",
2012
+ "_feltdb.StateOwnership",
2013
+ "_feltdb.StateDependency",
2014
+ "_feltdb.StateChange",
2015
+ "_feltdb.StateAttribution",
2016
+ "_feltdb.StateObservation",
2017
+ "_feltdb.StateProposal",
2018
+ "_feltdb.StateImpact",
2019
+ "_feltdb.Intent",
2020
+ "_feltdb.IntentEvent",
2021
+ "_feltdb.ProposalEvent",
2022
+ ];
2023
+
2024
+ pub fn is_substrate_collection(name: &str) -> bool {
2025
+ SUBSTRATE_COLLECTIONS.contains(&name)
2026
+ }
2027
+
2028
+ /// Capture a read basis over an application state namespace.
2029
+ ///
2030
+ /// Every collection the read set names must be one the caller may read in
2031
+ /// full: an application collection readable under `state:read` whose read
2032
+ /// policy does not depend on the record, or a substrate collection under
2033
+ /// `composition:read`. A token is a digest over every row it covers, so a
2034
+ /// basis over rows the caller could not read would disclose them; such a
2035
+ /// read set is refused, never digested.
2036
+ pub fn capture_read_basis(
2037
+ db: &FeltDb,
2038
+ schema: &StateSchema,
2039
+ state_namespace: &str,
2040
+ reads: &crate::ReadSet,
2041
+ readable: &dyn Fn(&str) -> bool,
2042
+ ) -> Result<crate::ReadBasis, StateFailure> {
2043
+ let reads = reads
2044
+ .normalised()
2045
+ .map_err(|error| StateFailure::new("VALIDATION_FAILED", &error.to_string()))?;
2046
+ authorize_read_set(schema, &reads, readable)?;
2047
+ db.read_basis_in(
2048
+ &reads,
2049
+ &crate::ReadLayout::Namespace(state_namespace.to_string()),
2050
+ )
2051
+ .map_err(StateFailure::storage)
2052
+ }
2053
+
2054
+ /// The refusal for a read basis over a collection the caller cannot read in full.
2055
+ pub fn read_basis_denied(collection: &str) -> StateFailure {
2056
+ let mut failure = StateFailure::new(
2057
+ "AUTHORIZATION_DENIED",
2058
+ "a read basis may only cover collections the caller can read in full",
2059
+ );
2060
+ failure.resource = Some(collection.to_string());
2061
+ failure
2062
+ }
2063
+
2064
+ fn authorize_read_set(
2065
+ schema: &StateSchema,
2066
+ reads: &crate::ReadSet,
2067
+ readable: &dyn Fn(&str) -> bool,
2068
+ ) -> Result<(), StateFailure> {
2069
+ let collections = reads
2070
+ .records
2071
+ .iter()
2072
+ .map(|record| record.collection.as_str())
2073
+ .chain(
2074
+ reads
2075
+ .predicates
2076
+ .iter()
2077
+ .map(|predicate| predicate.collection.as_str()),
2078
+ );
2079
+ for name in collections {
2080
+ if !is_substrate_collection(name) {
2081
+ collection(schema, name)?;
2082
+ }
2083
+ if !readable(name) {
2084
+ return Err(read_basis_denied(name));
2085
+ }
2086
+ }
2087
+ Ok(())
1829
2088
  }
1830
2089
  #[derive(Debug, Clone, Serialize, Deserialize)]
1831
2090
  pub struct TransactionAudit {
@@ -2103,10 +2362,12 @@ fn validate_record(collection: &CollectionSchema, value: &mut Value) -> Result<(
2103
2362
  }
2104
2363
  }
2105
2364
  }
2106
- if object
2107
- .keys()
2108
- .any(|name| name != "__version" && !collection.fields.iter().any(|f| &f.name == name))
2109
- {
2365
+ // `__version` and `id` are record identity, carried by every client
2366
+ // record whether or not the contract declares them; a declared `id` is
2367
+ // validated by its declaration above.
2368
+ if object.keys().any(|name| {
2369
+ name != "__version" && name != "id" && !collection.fields.iter().any(|f| &f.name == name)
2370
+ }) {
2110
2371
  return Err(StateFailure::new(
2111
2372
  "VALIDATION_FAILED",
2112
2373
  "record contains an unknown field",
@@ -2162,17 +2423,34 @@ fn execute_transaction_with_policies(
2162
2423
  "state:write is required",
2163
2424
  ));
2164
2425
  }
2165
- let canonical = serde_json::to_vec(&(
2166
- request.tenant_id.as_str(),
2167
- request.application_id.as_str(),
2168
- request.revision_id.as_str(),
2169
- request.schema_version,
2170
- request.state_namespace.as_deref(),
2171
- request.causal_parent,
2172
- &request.authorization.subject,
2173
- &request.preconditions,
2174
- &request.operations,
2175
- ))
2426
+ let canonical = if request.fences.is_empty() {
2427
+ serde_json::to_vec(&(
2428
+ request.tenant_id.as_str(),
2429
+ request.application_id.as_str(),
2430
+ request.revision_id.as_str(),
2431
+ request.schema_version,
2432
+ request.state_namespace.as_deref(),
2433
+ request.causal_parent,
2434
+ &request.authorization.subject,
2435
+ &request.preconditions,
2436
+ &request.operations,
2437
+ ))
2438
+ } else {
2439
+ // Fences are part of what the transaction means, so they bind its
2440
+ // identity. Unfenced transactions hash exactly as they always have.
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
+ &request.fences,
2452
+ ))
2453
+ }
2176
2454
  .map_err(StateFailure::storage)?;
2177
2455
  let transaction_id = request
2178
2456
  .transaction_id
@@ -2252,9 +2530,13 @@ fn execute_transaction_with_policies(
2252
2530
  .as_deref()
2253
2531
  .unwrap_or(&request.application_id)
2254
2532
  .to_string();
2255
- let snapshot = db
2256
- .state_rows_for_namespace(&namespace)
2257
- .map_err(StateFailure::storage)?;
2533
+ let snapshot = transaction_snapshot_rows(
2534
+ db,
2535
+ schema,
2536
+ request,
2537
+ uniform_write_policy,
2538
+ collection_write_policies,
2539
+ )?;
2258
2540
  // Record preconditions are collected here and evaluated by the store,
2259
2541
  // inside the same lock as the writes. Evaluating them against `snapshot`
2260
2542
  // alongside the counters below would reintroduce exactly the gap they
@@ -2371,7 +2653,22 @@ fn execute_transaction_with_policies(
2371
2653
  "collection is not writable",
2372
2654
  ));
2373
2655
  }
2374
- let definition = collection(schema, &operation.collection)?;
2656
+ // Substrate collections are validated by the substrate that owns them,
2657
+ // and written only under the dedicated capability.
2658
+ let substrate = is_substrate_collection(&operation.collection);
2659
+ if substrate && !request.authorization.can("composition:write") {
2660
+ let mut failure = StateFailure::new(
2661
+ "AUTHORIZATION_DENIED",
2662
+ "composition:write is required for substrate collections",
2663
+ );
2664
+ failure.resource = Some(operation.collection.clone());
2665
+ return Err(failure);
2666
+ }
2667
+ let definition = if substrate {
2668
+ None
2669
+ } else {
2670
+ Some(collection(schema, &operation.collection)?)
2671
+ };
2375
2672
  let capability = format!(
2376
2673
  "{}:{}",
2377
2674
  request
@@ -2392,7 +2689,8 @@ fn execute_transaction_with_policies(
2392
2689
  // Per-operation record authorization
2393
2690
  let write_policy = collection_write_policies
2394
2691
  .and_then(|policies| policies.get(&operation.collection))
2395
- .or(uniform_write_policy);
2692
+ .or(uniform_write_policy)
2693
+ .filter(|_| !substrate);
2396
2694
  if let Some(policy) = write_policy {
2397
2695
  let actor = if request.authorization.subject.is_empty()
2398
2696
  || request.authorization.subject == ":"
@@ -2458,7 +2756,16 @@ fn execute_transaction_with_policies(
2458
2756
  return Err(StateFailure::new("CONFLICT", "record already exists"));
2459
2757
  }
2460
2758
  let mut value = operation.value.clone();
2461
- validate_record(definition, &mut value)?;
2759
+ match definition {
2760
+ Some(definition) => validate_record(definition, &mut value)?,
2761
+ None if value.is_object() => {}
2762
+ None => {
2763
+ return Err(StateFailure::new(
2764
+ "VALIDATION_FAILED",
2765
+ "record must be an object",
2766
+ ));
2767
+ }
2768
+ }
2462
2769
  Some(value)
2463
2770
  }
2464
2771
  TransactionOperationKind::Update => {
@@ -2474,7 +2781,9 @@ fn execute_transaction_with_policies(
2474
2781
  .expect("existing object")
2475
2782
  .insert(k.clone(), v.clone());
2476
2783
  }
2477
- validate_record(definition, &mut value)?;
2784
+ if let Some(definition) = definition {
2785
+ validate_record(definition, &mut value)?;
2786
+ }
2478
2787
  Some(value)
2479
2788
  }
2480
2789
  TransactionOperationKind::Delete => {
@@ -2509,6 +2818,7 @@ fn execute_transaction_with_policies(
2509
2818
  .unwrap_or(&request.application_id),
2510
2819
  definition.name
2511
2820
  );
2821
+ let mut referenced_records = BTreeMap::new();
2512
2822
  for ((cap, key), value) in overlay
2513
2823
  .iter()
2514
2824
  .filter(|((cap, _), value)| cap == &capability && value.is_some())
@@ -2525,16 +2835,29 @@ fn execute_transaction_with_policies(
2525
2835
  .unwrap_or(&request.application_id),
2526
2836
  target
2527
2837
  );
2528
- let present = overlay
2529
- .get(&(target_capability.clone(), reference.into()))
2530
- .map(|v| v.is_some())
2531
- .unwrap_or_else(|| {
2532
- snapshot.iter().any(|row| {
2533
- row.capability == target_capability
2534
- && row.key == reference
2535
- && !row.deleted
2536
- })
2537
- });
2838
+ let present = if let Some(value) =
2839
+ overlay.get(&(target_capability.clone(), reference.into()))
2840
+ {
2841
+ value.is_some()
2842
+ } else if snapshot.iter().any(|row| {
2843
+ row.capability == target_capability
2844
+ && row.key == reference
2845
+ && !row.deleted
2846
+ }) {
2847
+ true
2848
+ } else {
2849
+ let cache_key = (target_capability.clone(), reference.to_string());
2850
+ if let Some(present) = referenced_records.get(&cache_key) {
2851
+ *present
2852
+ } else {
2853
+ let present = db
2854
+ .get_collection_record(&target_capability, reference)
2855
+ .map_err(StateFailure::storage)?
2856
+ .is_some();
2857
+ referenced_records.insert(cache_key, present);
2858
+ present
2859
+ }
2860
+ };
2538
2861
  if !present {
2539
2862
  let mut failure = StateFailure::new(
2540
2863
  "VALIDATION_FAILED",
@@ -2583,12 +2906,14 @@ fn execute_transaction_with_policies(
2583
2906
  }
2584
2907
  }
2585
2908
  let commit = db
2586
- .apply_atomic_transaction_guarded(
2909
+ .apply_atomic_transaction_fenced_in(
2587
2910
  &storage_transaction_id,
2588
2911
  Some(&payload_hash),
2589
2912
  request.causal_parent,
2590
2913
  &preconditions,
2591
2914
  &record_preconditions,
2915
+ &crate::ReadLayout::Namespace(namespace.clone()),
2916
+ &request.fences,
2592
2917
  &mutations,
2593
2918
  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"})),
2594
2919
  )
@@ -2625,6 +2950,12 @@ fn execute_transaction_with_policies(
2625
2950
  return failure;
2626
2951
  }
2627
2952
  let text = error.to_string();
2953
+ if text.contains("INVALID_READ_SET:") {
2954
+ // A malformed or foreign basis is a caller error, never a lost race.
2955
+ let mut failure = StateFailure::new("VALIDATION_FAILED", &text);
2956
+ failure.transaction = Some(transaction_id.clone());
2957
+ return failure;
2958
+ }
2628
2959
  if text.contains("PRECONDITION_FAILED") {
2629
2960
  let mut failure = StateFailure::new("PRECONDITION_FAILED", &text);
2630
2961
  failure.transaction = Some(transaction_id.clone());
@@ -2923,6 +3254,7 @@ mod tests {
2923
3254
  if_version: None,
2924
3255
  },
2925
3256
  ],
3257
+ fences: vec![],
2926
3258
  };
2927
3259
  let result = execute_transaction(&db, &schema, &request, None).unwrap();
2928
3260
  assert_eq!(result.state_after, 2);
@@ -2974,6 +3306,7 @@ mod tests {
2974
3306
  value: serde_json::json!({ "title": title }),
2975
3307
  if_version: None,
2976
3308
  }],
3309
+ fences: vec![],
2977
3310
  },
2978
3311
  None,
2979
3312
  )
@@ -3122,6 +3455,7 @@ mod tests {
3122
3455
  value: serde_json::json!({ "title": "First" }),
3123
3456
  if_version: None,
3124
3457
  }],
3458
+ fences: vec![],
3125
3459
  },
3126
3460
  None,
3127
3461
  )
@@ -3160,6 +3494,7 @@ mod tests {
3160
3494
  if_version: None,
3161
3495
  },
3162
3496
  ],
3497
+ fences: vec![],
3163
3498
  };
3164
3499
  assert_eq!(
3165
3500
  execute_transaction(&db, &schema, &request, None)
@@ -3194,6 +3529,7 @@ mod tests {
3194
3529
  value: serde_json::json!({"title":title,"status":status}),
3195
3530
  if_version: None,
3196
3531
  }],
3532
+ fences: vec![],
3197
3533
  },
3198
3534
  None,
3199
3535
  )
@@ -3285,6 +3621,7 @@ mod tests {
3285
3621
  value: serde_json::json!({"title":"First"}),
3286
3622
  if_version: None,
3287
3623
  }],
3624
+ fences: vec![],
3288
3625
  };
3289
3626
  let first = execute_transaction(&db, &schema, &request, None).unwrap();
3290
3627
  let duplicate = execute_transaction(&db, &schema, &request, None).unwrap();
@@ -3324,6 +3661,7 @@ mod tests {
3324
3661
  value: serde_json::json!({"title":"First"}),
3325
3662
  if_version: None,
3326
3663
  }],
3664
+ fences: vec![],
3327
3665
  };
3328
3666
  execute_transaction(&db, &schema, &request, None).unwrap();
3329
3667
  request.transaction_id = Some("stale-parent".into());
@@ -3361,6 +3699,7 @@ mod tests {
3361
3699
  value: serde_json::json!({"title":"Original"}),
3362
3700
  if_version: None,
3363
3701
  }],
3702
+ fences: vec![],
3364
3703
  };
3365
3704
  {
3366
3705
  let db = crate::open(&path).unwrap();
@@ -3417,6 +3756,7 @@ mod tests {
3417
3756
  value: serde_json::json!({"title":format!("{app}-{record}")}),
3418
3757
  if_version: None,
3419
3758
  }],
3759
+ fences: vec![],
3420
3760
  };
3421
3761
  (schema, request)
3422
3762
  }
@@ -3501,6 +3841,7 @@ mod tests {
3501
3841
  value: serde_json::json!({"title":id}),
3502
3842
  if_version: None,
3503
3843
  }],
3844
+ fences: vec![],
3504
3845
  };
3505
3846
  execute_transaction(&db, &schema, &insert("one"), None).unwrap();
3506
3847
  let context = begin_read(&db, &schema, "app", auth()).unwrap();
@@ -3561,6 +3902,7 @@ mod tests {
3561
3902
  if_version: None,
3562
3903
  },
3563
3904
  ],
3905
+ fences: vec![],
3564
3906
  };
3565
3907
  execute_transaction(&db, &schema, &request, None).unwrap();
3566
3908
  }
@@ -3594,6 +3936,7 @@ mod tests {
3594
3936
  if_version: None,
3595
3937
  })
3596
3938
  .collect(),
3939
+ fences: vec![],
3597
3940
  };
3598
3941
  execute_transaction(&db, &schema, &request, None).unwrap();
3599
3942
  let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
@@ -3657,6 +4000,7 @@ mod tests {
3657
4000
  if_version: None,
3658
4001
  })
3659
4002
  .collect(),
4003
+ fences: vec![],
3660
4004
  };
3661
4005
  execute_transaction(&db, &schema, &request, None).unwrap();
3662
4006
  let context = begin_read(&db, &schema, "app", auth()).unwrap();
@@ -3719,6 +4063,7 @@ mod tests {
3719
4063
  value: serde_json::json!({"title": "Test", "status": "open"}),
3720
4064
  if_version: None,
3721
4065
  }],
4066
+ fences: vec![],
3722
4067
  };
3723
4068
  execute_transaction(&db, &schema, &request, None).unwrap();
3724
4069
  let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
@@ -3779,6 +4124,7 @@ mod tests {
3779
4124
  if_version: None,
3780
4125
  })
3781
4126
  .collect(),
4127
+ fences: vec![],
3782
4128
  };
3783
4129
  execute_transaction(&db, &schema, &request, None).unwrap();
3784
4130
  let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
@@ -3841,6 +4187,7 @@ mod tests {
3841
4187
  authorization: auth(),
3842
4188
  preconditions: vec![],
3843
4189
  operations,
4190
+ fences: vec![],
3844
4191
  },
3845
4192
  None,
3846
4193
  )
@@ -3867,10 +4214,11 @@ mod tests {
3867
4214
  let first_cursor = loop {
3868
4215
  let page = execute_query(&db, &schema, &context, &query, None).unwrap();
3869
4216
  assert!(page.records.len() <= 25);
3870
- assert!(page
3871
- .records
3872
- .iter()
3873
- .all(|record| record["status"] == "tenant-a"));
4217
+ assert!(
4218
+ page.records
4219
+ .iter()
4220
+ .all(|record| record["status"] == "tenant-a")
4221
+ );
3874
4222
  ids.extend(
3875
4223
  page.records
3876
4224
  .iter()
@@ -3946,6 +4294,7 @@ mod tests {
3946
4294
  value: serde_json::json!({ "title": "First", "status": "open" }),
3947
4295
  if_version: None,
3948
4296
  }],
4297
+ fences: vec![],
3949
4298
  },
3950
4299
  None,
3951
4300
  )
@@ -4126,6 +4475,7 @@ mod tests {
4126
4475
  authorization: auth(),
4127
4476
  preconditions: vec![],
4128
4477
  operations,
4478
+ fences: vec![],
4129
4479
  },
4130
4480
  None,
4131
4481
  )
@@ -4188,6 +4538,7 @@ mod tests {
4188
4538
  value: serde_json::json!({"tenant_id":"tenant-a"}),
4189
4539
  if_version: None,
4190
4540
  }],
4541
+ fences: vec![],
4191
4542
  },
4192
4543
  None,
4193
4544
  )
@@ -4322,6 +4673,7 @@ mod tests {
4322
4673
  authorization: auth_ctx.clone(),
4323
4674
  preconditions: vec![],
4324
4675
  operations,
4676
+ fences: vec![],
4325
4677
  };
4326
4678
  execute_transaction(&db, &schema, &request, None).unwrap();
4327
4679
  }
@@ -4505,6 +4857,7 @@ mod tests {
4505
4857
  if_version: None,
4506
4858
  }],
4507
4859
  preconditions: vec![],
4860
+ fences: vec![],
4508
4861
  };
4509
4862
 
4510
4863
  let result = execute_transaction(&db, &schema, &request, None);
@@ -4543,6 +4896,7 @@ mod tests {
4543
4896
  if_version: None,
4544
4897
  }],
4545
4898
  preconditions: vec![],
4899
+ fences: vec![],
4546
4900
  };
4547
4901
  execute_transaction(&db, &schema, &insert_request, None).unwrap();
4548
4902
 
@@ -4612,6 +4966,7 @@ mod tests {
4612
4966
  if_version: None,
4613
4967
  }],
4614
4968
  preconditions: vec![],
4969
+ fences: vec![],
4615
4970
  };
4616
4971
 
4617
4972
  let result = execute_transaction(&db, &schema, &request, None);
@@ -4643,6 +4998,7 @@ mod tests {
4643
4998
  if_version: None,
4644
4999
  }],
4645
5000
  preconditions: vec![],
5001
+ fences: vec![],
4646
5002
  };
4647
5003
  execute_transaction(&db, &schema, &insert_request, None).unwrap();
4648
5004
 
@@ -4716,6 +5072,7 @@ mod tests {
4716
5072
  if_version: None,
4717
5073
  }],
4718
5074
  preconditions: vec![],
5075
+ fences: vec![],
4719
5076
  };
4720
5077
 
4721
5078
  let result = execute_transaction(&db, &schema, &request, None);
@@ -4750,6 +5107,7 @@ mod tests {
4750
5107
  if_version: None,
4751
5108
  }],
4752
5109
  preconditions: vec![],
5110
+ fences: vec![],
4753
5111
  };
4754
5112
  let initial_result = execute_transaction(&db, &schema, &insert_initial, None).unwrap();
4755
5113
  let state_before_denied_tx = initial_result.state_after;
@@ -4783,6 +5141,7 @@ mod tests {
4783
5141
  if_version: None,
4784
5142
  }],
4785
5143
  preconditions: vec![],
5144
+ fences: vec![],
4786
5145
  };
4787
5146
 
4788
5147
  let denied_result = execute_transaction(&db, &schema, &denied_request, None);
@@ -4848,6 +5207,7 @@ mod tests {
4848
5207
  if_version: None,
4849
5208
  }],
4850
5209
  preconditions: vec![],
5210
+ fences: vec![],
4851
5211
  };
4852
5212
 
4853
5213
  let result = execute_transaction(&db, &schema, &write_request, None);
@@ -4893,6 +5253,7 @@ mod tests {
4893
5253
  if_version: None,
4894
5254
  }],
4895
5255
  preconditions: vec![],
5256
+ fences: vec![],
4896
5257
  };
4897
5258
 
4898
5259
  let _ = execute_transaction(&db, &schema, &insert_request, None);
@@ -5005,6 +5366,7 @@ mod tests {
5005
5366
  if_version: None,
5006
5367
  }],
5007
5368
  preconditions: vec![],
5369
+ fences: vec![],
5008
5370
  };
5009
5371
 
5010
5372
  let result = execute_transaction(&db, &schema, &write_request, None);
@@ -5048,6 +5410,7 @@ mod tests {
5048
5410
  if_version: None,
5049
5411
  }],
5050
5412
  preconditions: vec![],
5413
+ fences: vec![],
5051
5414
  };
5052
5415
 
5053
5416
  let result = execute_transaction(&db, &schema, &write_request, None);
@@ -5095,6 +5458,7 @@ mod tests {
5095
5458
  if_version: None,
5096
5459
  }],
5097
5460
  preconditions: vec![],
5461
+ fences: vec![],
5098
5462
  };
5099
5463
 
5100
5464
  let _ = execute_transaction(&db, &schema, &insert_in_org2, None);
@@ -5130,6 +5494,7 @@ mod tests {
5130
5494
  if_version: Some(0),
5131
5495
  }],
5132
5496
  preconditions: vec![],
5497
+ fences: vec![],
5133
5498
  };
5134
5499
 
5135
5500
  let result = execute_transaction(&db, &schema, &cross_org_mutation, None);
@@ -5211,6 +5576,7 @@ mod tests {
5211
5576
  },
5212
5577
  ],
5213
5578
  preconditions: vec![],
5579
+ fences: vec![],
5214
5580
  };
5215
5581
 
5216
5582
  execute_transaction(&db, &schema, &insert_records, None).expect("insert should succeed");
@@ -5304,6 +5670,7 @@ mod tests {
5304
5670
  if_version: None,
5305
5671
  }],
5306
5672
  preconditions: vec![],
5673
+ fences: vec![],
5307
5674
  };
5308
5675
 
5309
5676
  execute_transaction(&db, &schema, &insert_request, None).expect("insert should succeed");
@@ -5395,6 +5762,7 @@ mod tests {
5395
5762
  },
5396
5763
  ],
5397
5764
  preconditions: vec![],
5765
+ fences: vec![],
5398
5766
  };
5399
5767
 
5400
5768
  execute_transaction(&db, &schema, &insert_records, None).expect("insert should succeed");
@@ -5482,6 +5850,7 @@ mod tests {
5482
5850
  authorization: admin_context,
5483
5851
  operations: ops,
5484
5852
  preconditions: vec![],
5853
+ fences: vec![],
5485
5854
  };
5486
5855
 
5487
5856
  execute_transaction(&db, &schema, &insert_request, None).expect("insert should succeed");