@feltdb/core 0.8.3 → 0.8.5

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.
Files changed (193) hide show
  1. package/dist/cli/commands.js +4 -1
  2. package/dist/cli/provisioning-neutrality.js +79 -0
  3. package/dist/collection.d.ts +43 -1
  4. package/dist/collection.d.ts.map +1 -1
  5. package/dist/collection.js +192 -22
  6. package/dist/create/create.js +25 -21
  7. package/dist/create/managed-account.js +11 -0
  8. package/dist/create/package-versions.js +1 -1
  9. package/dist/create/server-source/Cargo.lock +165 -0
  10. package/dist/create/server-source/Cargo.toml +9 -0
  11. package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
  28. package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
  29. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
  30. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
  31. package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
  32. package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
  33. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
  34. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
  35. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
  36. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
  37. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
  38. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
  39. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
  40. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
  41. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
  42. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
  43. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
  44. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
  45. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
  46. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
  47. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
  48. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
  49. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
  50. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
  51. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +595 -0
  52. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
  53. package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
  54. package/dist/create/server-source/crates/feltdb/src/lib.rs +1050 -117
  55. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
  56. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
  57. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
  58. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
  59. package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
  60. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
  61. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
  62. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
  63. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
  64. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
  65. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
  66. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
  67. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
  68. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
  69. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
  70. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +90 -72
  71. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
  72. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
  73. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
  74. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
  75. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
  76. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
  77. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
  78. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
  79. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
  80. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
  81. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
  82. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
  83. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
  84. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
  85. package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
  86. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +126 -0
  87. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
  88. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
  89. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
  90. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
  91. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
  92. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
  93. package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
  94. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
  95. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +305 -6
  96. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
  97. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
  98. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
  99. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
  100. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
  101. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
  102. package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
  103. package/dist/create/server-source/crates/feltdb/src/sync.rs +12 -0
  104. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
  105. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
  106. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
  107. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
  108. package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
  109. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
  110. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  111. package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
  112. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +442 -0
  113. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
  114. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
  115. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
  116. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
  117. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
  118. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
  119. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
  120. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +234 -0
  121. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +947 -0
  122. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
  123. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
  124. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
  125. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
  126. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
  127. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
  128. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
  129. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
  130. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
  131. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
  132. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
  133. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
  134. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
  135. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
  136. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
  137. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
  138. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
  139. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
  140. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
  141. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
  142. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
  143. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
  144. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
  145. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
  146. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
  147. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
  149. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +1137 -29
  150. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
  151. package/dist/create/server-source/crates/feltdb-server/src/main.rs +800 -34
  152. package/dist/db.d.ts +33 -34
  153. package/dist/db.d.ts.map +1 -1
  154. package/dist/db.js +74 -20
  155. package/dist/deployment.d.ts +30 -0
  156. package/dist/deployment.d.ts.map +1 -0
  157. package/dist/deployment.js +130 -0
  158. package/dist/embedded-transaction.d.ts +22 -4
  159. package/dist/embedded-transaction.d.ts.map +1 -1
  160. package/dist/embedded-transaction.js +51 -5
  161. package/dist/feltdb.d.ts +14 -2
  162. package/dist/feltdb.d.ts.map +1 -1
  163. package/dist/file-db.js +1 -1
  164. package/dist/http-client.d.ts +14 -0
  165. package/dist/http-client.d.ts.map +1 -1
  166. package/dist/http-client.js +23 -5
  167. package/dist/http-db.d.ts +119 -1
  168. package/dist/http-db.d.ts.map +1 -1
  169. package/dist/http-db.js +346 -31
  170. package/dist/index-core.d.ts +2 -0
  171. package/dist/index-core.d.ts.map +1 -1
  172. package/dist/index-core.js +2 -0
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.js +9 -0
  175. package/dist/indexeddb-db.d.ts.map +1 -1
  176. package/dist/indexeddb-db.js +35 -21
  177. package/dist/managed-recovery.d.ts +192 -0
  178. package/dist/managed-recovery.d.ts.map +1 -0
  179. package/dist/managed-recovery.js +242 -0
  180. package/dist/memory-db.js +1 -1
  181. package/dist/studio-app/assets/{feltdb_wasm-DB8cX151.js → feltdb_wasm-DaNwCLRX.js} +1 -1
  182. package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
  183. package/dist/studio-app/assets/index-j8IlhNqJ.js +29 -0
  184. package/dist/studio-app/index.html +1 -1
  185. package/dist/transaction.d.ts +30 -0
  186. package/dist/transaction.d.ts.map +1 -1
  187. package/dist/transaction.js +41 -0
  188. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  189. package/dist/workload.d.ts +2 -1
  190. package/dist/workload.d.ts.map +1 -1
  191. package/package.json +1 -1
  192. package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
  193. package/dist/studio-app/assets/index-B0k4UAlI.js +0 -29
@@ -41,6 +41,7 @@ pub mod durable_dedup_set;
41
41
  pub mod durable_operation_identity;
42
42
  pub mod durable_operation_log;
43
43
  pub mod durable_sync;
44
+ pub mod equality_index;
44
45
  mod execution;
45
46
  pub mod in_process_transport;
46
47
  pub mod indexing;
@@ -98,6 +99,7 @@ mod phase_5_soak;
98
99
  pub mod policy_evaluation;
99
100
  pub mod production_api;
100
101
  mod provenance;
102
+ pub mod query_execution_diagnostics;
101
103
  pub mod query_performance;
102
104
  mod references;
103
105
  pub mod replay_fuzzing;
@@ -107,11 +109,14 @@ pub mod replication_manager;
107
109
  pub mod replication_protocol;
108
110
  mod routing;
109
111
  pub mod sharding;
112
+ pub mod state_conflict_contract;
110
113
  pub mod state_contract;
114
+ pub mod state_diff_contract;
111
115
  pub mod state_facade;
112
116
  pub mod state_hash;
113
117
  pub mod state_model;
114
118
  pub mod state_transition_store;
119
+ pub mod state_trigger;
115
120
  mod storage;
116
121
  pub mod submission;
117
122
  mod sync;
@@ -124,6 +129,7 @@ mod transaction_invariants;
124
129
  pub mod transaction_preconditions;
125
130
  pub mod transactions;
126
131
  mod trigger;
132
+ pub mod trigger_contract;
127
133
  pub mod worker_mesh;
128
134
  mod workflow;
129
135
  #[cfg(test)]
@@ -131,6 +137,7 @@ mod workflow_acceptance_tests;
131
137
  mod workflow_integration;
132
138
  mod workflow_runtime;
133
139
  pub mod workload;
140
+ pub mod workload_diagnostics;
134
141
 
135
142
  pub use acquisition::{
136
143
  AcquisitionDeduplicator, AcquisitionPolicy, AcquisitionResult, ContentRequest, ContentResponse,
@@ -182,6 +189,7 @@ pub use distributed_transactions::{
182
189
  DistributedTransactionExecutor, EnvelopeId, ReplicaState, ReplicationMessage,
183
190
  TransactionEnvelope,
184
191
  };
192
+ pub use equality_index::{EqualityIndex, EqualityIndexStats, IndexKey};
185
193
  pub use execution::{Execution, ExecutionQueue, ExecutionStatus, RetryPolicy};
186
194
  pub use indexing::{IndexConfig, IndexManager, IndexStats, IndexType};
187
195
  pub use materialization::{MaterializationHandler, ReactiveCollectionIntegration};
@@ -218,14 +226,27 @@ pub use sharding::{
218
226
  HotspotAlert, RebalanceOperation, ShardDistributionSummary, ShardId, ShardKey, ShardManager,
219
227
  ShardMetrics, ShardRange, ShardingStrategy,
220
228
  };
229
+ pub use state_conflict_contract::{
230
+ classify_states, StateConflictInput, StateConflictOutput, STATE_CONFLICT_CONTRACT_ID,
231
+ STATE_CONFLICT_CONTRACT_VERSION,
232
+ };
233
+ pub use state_diff_contract::{
234
+ diff_states, StateDiffInput, StateDiffOutput, STATE_DIFF_CONTRACT_ID,
235
+ STATE_DIFF_CONTRACT_VERSION,
236
+ };
221
237
  pub use state_facade::FeltDBStateSystem;
222
238
  pub use state_hash::{CanonicalState, StateHash};
223
239
  pub use state_model::{
224
- ChangeKind, ConflictClass, ConflictClassification, PathComponent, PathConflict,
240
+ apply_reconciliation_plan, path_relation, paths_overlap, reconcile, resolve_path, ChangeKind,
241
+ ConflictClass, ConflictClassification, PathComponent, PathConflict, PathRelation,
225
242
  ReconciliationPlan, Relationship, SemanticChange, SemanticDiff, StateId,
226
243
  StateReconciliationResult, StateRevision, StateStore, StateTopology, STATE_MODEL_VERSION,
227
244
  };
228
245
  pub use state_transition_store::{MemoryStateTransitionStore, StateTransitionRecord};
246
+ pub use state_trigger::{
247
+ StateTrigger, StateTriggerChangeKind, StateTriggerError, StateTriggerPredicate,
248
+ StateTriggerStore, TriggeredWorkload,
249
+ };
229
250
  pub use storage::{CheckpointData, FileStorage, MemoryStorage, Storage};
230
251
  pub use submission::{SubmissionManager, SubmissionMetrics};
231
252
  pub use sync::{ChangeLog, Conflict, ConflictDetector, PeerState, SyncMessage, SyncState};
@@ -235,6 +256,12 @@ pub use transactions::{
235
256
  StateTransition, TransactionExecutor, TransitionResult,
236
257
  };
237
258
  pub use trigger::{Trigger, TriggerFilter, TriggerRegistry};
259
+ pub use trigger_contract::{
260
+ canonical_input_bytes, canonical_output_bytes, evaluate_canonical, evaluate_trigger,
261
+ ContractChange, ContractChangeKind, ContractErrorBody, ContractErrorCode, ContractPredicate,
262
+ ContractRuntime, ContractTrigger, MatchOutcome, MatchReason, TriggerMatchInput,
263
+ TriggerMatchOutput, TRIGGER_MATCH_CONTRACT_ID, TRIGGER_MATCH_CONTRACT_VERSION,
264
+ };
238
265
  pub use workflow::{
239
266
  BlockedReason, WorkflowGraph, WorkflowInstance, WorkflowNode, WorkflowOperation, WorkflowRef,
240
267
  WorkflowState, WorkflowStepRef, WorkflowStepState,
@@ -251,7 +278,7 @@ use serde_json::Value;
251
278
  use sha2::{Digest, Sha256};
252
279
  use std::any::type_name;
253
280
  use std::collections::hash_map::DefaultHasher;
254
- use std::collections::{BTreeMap, HashMap, HashSet};
281
+ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
255
282
  use std::fmt::{Display, Formatter};
256
283
  use std::fs::{self, OpenOptions};
257
284
  use std::hash::{Hash, Hasher};
@@ -281,6 +308,12 @@ pub enum FlowError {
281
308
  expected: u64,
282
309
  actual: u64,
283
310
  },
311
+ /// The durable database is in a format this build does not understand.
312
+ ///
313
+ /// Returned by [`FeltDb::open`] **before any record is interpreted**, and
314
+ /// before anything is written. A database that returns this error is
315
+ /// untouched.
316
+ IncompatibleFormat(Box<FormatIncompatibility>),
284
317
  }
285
318
 
286
319
  impl Display for FlowError {
@@ -295,6 +328,9 @@ impl Display for FlowError {
295
328
  f,
296
329
  "REVISION_CONFLICT: expected authority revision {expected}, current revision is {actual}"
297
330
  ),
331
+ FlowError::IncompatibleFormat(incompatibility) => {
332
+ write!(f, "INCOMPATIBLE_DURABLE_FORMAT: {incompatibility}")
333
+ }
298
334
  }
299
335
  }
300
336
  }
@@ -352,6 +388,140 @@ struct Inner {
352
388
  transaction_payload_hashes: HashMap<String, String>,
353
389
  transaction_revisions: HashMap<String, (u64, u64)>,
354
390
  collection_cardinality: HashMap<String, u64>,
391
+ /// Derived equality index over `rows`. Never durable, never replicated, and
392
+ /// never consulted for anything but candidate selection: it is maintained
393
+ /// inside the same state mutation boundary as `rows` and rebuilt from
394
+ /// `rows` after recovery.
395
+ equality_index: EqualityIndex,
396
+ /// Each resource's newest revision, as `(id, sequence)`.
397
+ ///
398
+ /// Derived from the `state` rows exactly like `equality_index` is derived
399
+ /// from `rows`: never durable, rebuilt after recovery. It exists so that
400
+ /// minting a revision does not have to scan a resource's history to find
401
+ /// the parent it descends from.
402
+ revision_heads: HashMap<String, (state_model::StateId, u64)>,
403
+ }
404
+
405
+ /// The state lock, held with its wait and hold time attributable.
406
+ ///
407
+ /// FeltDB serializes every read and every write behind one mutex, so "how long
408
+ /// did an operation take" and "how long did it make everything else wait" are
409
+ /// different questions. PR34 showed the second one mattered: a query holding the
410
+ /// lock while it cloned a collection inflated the cost of unrelated writes.
411
+ ///
412
+ /// This guard answers both. It records the time spent *waiting* to acquire —
413
+ /// which is pure contention, the one span where no work happens at all — and the
414
+ /// time spent *holding*, which bounds how long anything else can be forced to
415
+ /// wait. Both are off unless `FELTDB_WORKLOAD_DIAGNOSTICS=1`, where acquisition
416
+ /// costs one relaxed atomic load and no clock read.
417
+ ///
418
+ /// It dereferences to [`Inner`], so every existing use site reads unchanged.
419
+ struct StateGuard<'a> {
420
+ inner: std::sync::MutexGuard<'a, Inner>,
421
+ held_since: Option<std::time::Instant>,
422
+ }
423
+
424
+ impl std::ops::Deref for StateGuard<'_> {
425
+ type Target = Inner;
426
+ fn deref(&self) -> &Inner {
427
+ &self.inner
428
+ }
429
+ }
430
+
431
+ impl std::ops::DerefMut for StateGuard<'_> {
432
+ fn deref_mut(&mut self) -> &mut Inner {
433
+ &mut self.inner
434
+ }
435
+ }
436
+
437
+ impl Drop for StateGuard<'_> {
438
+ fn drop(&mut self) {
439
+ if let Some(since) = self.held_since {
440
+ workload_diagnostics::record(
441
+ workload_diagnostics::Phase::StateLockHold,
442
+ since.elapsed().as_nanos().min(u64::MAX as u128) as u64,
443
+ );
444
+ }
445
+ }
446
+ }
447
+
448
+ /// The single state mutation boundary for authoritative records.
449
+ ///
450
+ /// Every path that changes a record — insert, update, delete, CAS, transaction
451
+ /// commit, replicated operation apply, log replay, snapshot install — goes
452
+ /// through these three methods, and they change the record map and the derived
453
+ /// equality index together while the caller still holds the state lock. That is
454
+ /// the whole atomicity argument: there is no window in which a committed record
455
+ /// exists without its index entries, or an index entry survives its record,
456
+ /// because no code path can write one without the other.
457
+ ///
458
+ /// The index is deliberately maintained here rather than through an independent
459
+ /// index API. An independent API would have to be called, correctly, by nine
460
+ /// separate mutation paths; a missed call would be a silent divergence that only
461
+ /// a query would reveal. Routing every write through one boundary makes the
462
+ /// omission impossible to write in the first place.
463
+ impl Inner {
464
+ /// Write one record, replacing any record already at its key.
465
+ ///
466
+ /// Returns `true` when this created a record rather than replacing one, which
467
+ /// is what collection cardinality accounting needs.
468
+ fn put_row(&mut self, row: StoredRow) -> bool {
469
+ let capability = row.capability.clone();
470
+ let key = row.key.clone();
471
+ let indexed = self.equality_index.indexes_collection(&capability);
472
+ let mutation = workload_diagnostics::span(workload_diagnostics::Phase::RecordMutation);
473
+ let bucket = self.rows.entry(capability.clone()).or_default();
474
+ let previous = bucket.insert(key.clone(), row);
475
+ drop(mutation);
476
+ if !indexed {
477
+ return previous.is_none();
478
+ }
479
+ // From here on the work exists only because an index does. Measuring it
480
+ // apart from the record write is what makes "did index maintenance
481
+ // become a write-side cost?" a question with an answer.
482
+ let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
483
+ // Old entries first, then new ones. Doing it in this order means a
484
+ // record whose indexed field did not change is removed from and
485
+ // re-added to the same bucket, which is a no-op rather than a gap.
486
+ if let Some(previous) = &previous {
487
+ self.equality_index
488
+ .remove_record(&capability, &key, &previous.value);
489
+ }
490
+ // Disjoint field borrows: the record map is read while the index is
491
+ // written. Nothing is cloned to maintain the index.
492
+ let Inner {
493
+ rows,
494
+ equality_index,
495
+ ..
496
+ } = self;
497
+ if let Some(current) = rows.get(&capability).and_then(|rows| rows.get(&key)) {
498
+ equality_index.insert_record(&capability, &key, &current.value);
499
+ }
500
+ previous.is_none()
501
+ }
502
+
503
+ /// Remove one record. Returns `true` when a record was actually removed.
504
+ fn remove_row(&mut self, capability: &str, key: &str) -> bool {
505
+ let mutation = workload_diagnostics::span(workload_diagnostics::Phase::RecordMutation);
506
+ let Some(removed) = self
507
+ .rows
508
+ .get_mut(capability)
509
+ .and_then(|bucket| bucket.remove(key))
510
+ else {
511
+ return false;
512
+ };
513
+ drop(mutation);
514
+ let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
515
+ self.equality_index
516
+ .remove_record(capability, key, &removed.value);
517
+ true
518
+ }
519
+
520
+ /// Drop all records, as a snapshot install does. The index follows.
521
+ fn clear_rows(&mut self) {
522
+ self.rows.clear();
523
+ self.equality_index.clear();
524
+ }
355
525
  }
356
526
 
357
527
  #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -601,11 +771,107 @@ pub struct StoredRow {
601
771
  pub operation: Option<Operation>,
602
772
  }
603
773
 
774
+ /// The durable format this build writes and understands.
775
+ ///
776
+ /// Independent of the package version on purpose: an application release that
777
+ /// changes nothing about persistence must not appear to change the database.
778
+ ///
779
+ /// - **1** — everything before revisions became resource-scoped. Never written
780
+ /// by any build; it names the era that predates format versioning.
781
+ /// - **2** — resource-scoped revisions: a `StateRevision` carries `resource`,
782
+ /// `content_id` and `sequence`.
783
+ pub const DURABLE_FORMAT_VERSION: u32 = 2;
784
+
785
+ /// The log record that carries the format version.
786
+ const FORMAT_RECORD_TYPE: &str = "feltdb.format.v1";
787
+
788
+ #[derive(Debug, Serialize, Deserialize)]
789
+ struct FormatRecord {
790
+ record_type: String,
791
+ format_version: u32,
792
+ }
793
+
794
+ /// What format a durable database is in.
795
+ #[derive(Debug, Clone, PartialEq, Eq)]
796
+ pub enum DurableFormat {
797
+ /// A format record states the version.
798
+ Versioned(u32),
799
+ /// No format record: the database predates format versioning.
800
+ ///
801
+ /// This is a version, not an absence of one. It is the only case where the
802
+ /// records themselves are examined to decide compatibility, and that
803
+ /// examination is a deliberate one-time probe rather than a deserialization
804
+ /// attempt whose failure is discarded.
805
+ Unversioned,
806
+ }
807
+
808
+ impl Display for DurableFormat {
809
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
810
+ match self {
811
+ DurableFormat::Versioned(version) => write!(f, "version {version}"),
812
+ DurableFormat::Unversioned => write!(f, "unversioned (predates format versioning)"),
813
+ }
814
+ }
815
+ }
816
+
817
+ /// Why a durable database cannot be opened, and what to do about it.
818
+ #[derive(Debug, Clone, PartialEq, Eq)]
819
+ pub struct FormatIncompatibility {
820
+ /// What the database is.
821
+ pub found: DurableFormat,
822
+ /// What this build requires.
823
+ pub required: u32,
824
+ /// What is wrong, in terms of the data rather than the code.
825
+ pub reason: String,
826
+ /// What the operator can do.
827
+ pub action: String,
828
+ }
829
+
830
+ impl Display for FormatIncompatibility {
831
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
832
+ write!(
833
+ f,
834
+ "durable format {} is incompatible with this FeltDB, which requires \
835
+ format version {}; {}. No data was read or modified. {}",
836
+ self.found, self.required, self.reason, self.action
837
+ )
838
+ }
839
+ }
840
+
841
+ /// The result of examining a durable database without opening it.
842
+ #[derive(Debug, Clone, PartialEq, Eq)]
843
+ pub enum FormatCompatibility {
844
+ /// Safe to open.
845
+ Compatible(DurableFormat),
846
+ /// Must not be opened.
847
+ Incompatible(FormatIncompatibility),
848
+ }
849
+
850
+ impl FormatCompatibility {
851
+ /// Whether opening this database is safe.
852
+ pub fn is_compatible(&self) -> bool {
853
+ matches!(self, FormatCompatibility::Compatible(_))
854
+ }
855
+ }
856
+
604
857
  #[derive(Debug, Clone, Serialize, Deserialize)]
605
858
  pub struct DatabaseSnapshot {
606
859
  pub rows: Vec<StoredRow>,
607
860
  pub versions: HashMap<String, u64>,
608
861
  pub content_hash: String,
862
+ /// The durable format the rows are in.
863
+ ///
864
+ /// A snapshot carries whole `StoredRow`s, revisions included, so it is a
865
+ /// durable interchange artifact and not merely a protocol value: installing
866
+ /// one from a different format would reintroduce exactly the silent
867
+ /// misinterpretation the on-disk version exists to prevent.
868
+ ///
869
+ /// Defaults to 0 so a snapshot serialized before versioning still
870
+ /// deserializes; 0 is treated as unversioned and refused rather than
871
+ /// assumed current. `content_hash` deliberately does not cover this field,
872
+ /// so hashes computed before it existed remain valid.
873
+ #[serde(default)]
874
+ pub format_version: u32,
609
875
  }
610
876
 
611
877
  impl DatabaseSnapshot {
@@ -633,6 +899,10 @@ struct LogHeader {
633
899
  record_type: String,
634
900
  local_sequence: u64,
635
901
  observed_versions: HashMap<String, u64>,
902
+ /// Present since format versioning. Absent in headers written before it,
903
+ /// which deserialize as 0 and are classified as unversioned.
904
+ #[serde(default)]
905
+ format_version: u32,
636
906
  }
637
907
 
638
908
  #[derive(Debug, Default, Serialize, Deserialize)]
@@ -646,14 +916,222 @@ pub fn open<P: AsRef<Path>>(path: P) -> Result<FeltDb> {
646
916
  FeltDb::open(path)
647
917
  }
648
918
 
919
+ /// Capabilities whose rows are not themselves versioned.
920
+ ///
921
+ /// `state` holds the revisions, and `_retention` holds the watermark that says
922
+ /// how much of each resource's history survives. Minting a revision of either
923
+ /// would mint a revision of the mint.
924
+ const UNVERSIONED_CAPABILITIES: &[&str] = &["state", "_retention"];
925
+
926
+ /// Whether a write to this capability creates a revision.
927
+ fn is_versioned(capability: &str) -> bool {
928
+ !UNVERSIONED_CAPABILITIES.contains(&capability)
929
+ }
930
+
931
+ /// Rebuild the derived head index from the durable revisions.
932
+ ///
933
+ /// Called after recovery, for the same reason `equality_index` is: the index is
934
+ /// a convenience over `rows` and must never be the authority for what `rows`
935
+ /// contains.
936
+ fn rebuild_revision_heads(inner: &mut Inner) {
937
+ let mut heads: HashMap<String, (state_model::StateId, u64)> = HashMap::new();
938
+ if let Some(bucket) = inner.rows.get("state") {
939
+ for row in bucket.values() {
940
+ if row.deleted || !row.key.starts_with("state:revision:") {
941
+ continue;
942
+ }
943
+ let Ok(revision) =
944
+ serde_json::from_value::<state_model::StateRevision>(row.value.clone())
945
+ else {
946
+ continue;
947
+ };
948
+ let entry = heads
949
+ .entry(revision.resource.clone())
950
+ .or_insert_with(|| (revision.id.clone(), revision.sequence));
951
+ if revision.sequence >= entry.1 {
952
+ *entry = (revision.id.clone(), revision.sequence);
953
+ }
954
+ }
955
+ }
956
+ inner.revision_heads = heads;
957
+ }
958
+
959
+ /// Write one derived row inside an already-held state lock.
960
+ ///
961
+ /// Deliberately **not** a mutation: no sequence number, no vector clock tick,
962
+ /// no entry in the change log. A revision is a *derived* record of a mutation
963
+ /// that already has all three, and giving it its own would make every write
964
+ /// advance replication twice and double the operation stream.
965
+ ///
966
+ /// It is still durable — the row is appended to the log and enters `rows`
967
+ /// inside the same lock as the state it records, so a reader never observes a
968
+ /// mutation whose revision is missing, and a reopened database has both.
969
+ ///
970
+ /// The consequence is that revisions are **local and derived**, not replicated.
971
+ /// That is sound in principle, because a revision's identity is a function of
972
+ /// the resource, the content, the parent and the sequence — all of which a peer
973
+ /// that applies the same operation stream has. Whether the sync-apply path
974
+ /// actually mints is a separate question this change does not answer.
975
+ fn append_derived_row_locked(
976
+ inner: &mut Inner,
977
+ capability: &str,
978
+ key: &str,
979
+ value: Value,
980
+ ) -> Result<()> {
981
+ let row = StoredRow {
982
+ capability: capability.to_string(),
983
+ key: key.to_string(),
984
+ rust_type: "feltdb::state_model::StateRevision".to_string(),
985
+ value,
986
+ unix_ms: now_ms(),
987
+ content_hash: None,
988
+ flow_ref: None,
989
+ deleted: false,
990
+ operation: None,
991
+ };
992
+ append_event(&inner.path, &row)?;
993
+ inner.put_row(row);
994
+ Ok(())
995
+ }
996
+
997
+ /// Mint a revision for an authoritative write, under the caller's lock.
998
+ ///
999
+ /// This is where the resource-scoped model meets the write path. The resource
1000
+ /// is the record's key, the parent is that resource's current head, and the
1001
+ /// sequence is allocated from the parent — so a resource that returns to a value
1002
+ /// it previously held records a *new* revision rather than resurrecting the old
1003
+ /// one.
1004
+ ///
1005
+ /// Retention is applied here too, by the same decision function the explicit
1006
+ /// store API uses. Bounding history at the moment history is created is what
1007
+ /// keeps automatic minting from being an unbounded-history architecture.
1008
+ fn mint_revision_locked(inner: &mut Inner, resource: &str, content: String) -> Result<()> {
1009
+ let parent = inner.revision_heads.get(resource).cloned();
1010
+ let (parent_id, sequence) = match &parent {
1011
+ Some((id, sequence)) => (Some(id.clone()), sequence + 1),
1012
+ None => (None, 0),
1013
+ };
1014
+ let authority = inner.instance_id.clone();
1015
+ let revision = state_model::StateRevision::at(
1016
+ resource.to_string(),
1017
+ content,
1018
+ parent_id,
1019
+ sequence,
1020
+ authority,
1021
+ );
1022
+ let id = revision.id.clone();
1023
+ let key = format!("state:revision:{}", id.as_hex());
1024
+ let value = serde_json::to_value(&revision)?;
1025
+ append_derived_row_locked(inner, "state", &key, value)?;
1026
+ inner
1027
+ .revision_heads
1028
+ .insert(resource.to_string(), (id, sequence));
1029
+ apply_retention_locked(inner, resource)
1030
+ }
1031
+
1032
+ /// Expire whatever the resource's configured policy no longer retains.
1033
+ ///
1034
+ /// The common path is one map lookup: with no policy configured there is
1035
+ /// nothing to scan. A resource's history is only walked once a policy exists to
1036
+ /// bound it.
1037
+ fn apply_retention_locked(inner: &mut Inner, resource: &str) -> Result<()> {
1038
+ let retention_key = format!("_retention:{resource}");
1039
+ let Some(state_row) = inner
1040
+ .rows
1041
+ .get("_retention")
1042
+ .and_then(|bucket| bucket.get(&retention_key))
1043
+ else {
1044
+ return Ok(());
1045
+ };
1046
+ let Ok(mut state) = serde_json::from_value::<RetentionRecord>(state_row.value.clone()) else {
1047
+ return Ok(());
1048
+ };
1049
+ if state.policy.keep_last.is_none() {
1050
+ return Ok(());
1051
+ }
1052
+
1053
+ let mut history: Vec<(state_model::StateId, u64)> = Vec::new();
1054
+ if let Some(bucket) = inner.rows.get("state") {
1055
+ for row in bucket.values() {
1056
+ if row.deleted || !row.key.starts_with("state:revision:") {
1057
+ continue;
1058
+ }
1059
+ if let Ok(revision) =
1060
+ serde_json::from_value::<state_model::StateRevision>(row.value.clone())
1061
+ {
1062
+ if revision.resource == resource {
1063
+ history.push((revision.id, revision.sequence));
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+ history.sort_by_key(|(_, sequence)| *sequence);
1069
+
1070
+ let (expire, horizon) = state_model::revisions_to_expire(&history, state.policy.keep_last);
1071
+ if expire.is_empty() {
1072
+ return Ok(());
1073
+ }
1074
+ for id in &expire {
1075
+ let key = format!("state:revision:{}", id.as_hex());
1076
+ if let Some(bucket) = inner.rows.get_mut("state") {
1077
+ bucket.remove(&key);
1078
+ }
1079
+ let tombstone = StoredRow {
1080
+ capability: "state".to_string(),
1081
+ key,
1082
+ rust_type: "feltdb::state_model::StateRevision".to_string(),
1083
+ value: Value::Null,
1084
+ unix_ms: now_ms(),
1085
+ content_hash: None,
1086
+ flow_ref: None,
1087
+ deleted: true,
1088
+ operation: None,
1089
+ };
1090
+ append_event(&inner.path, &tombstone)?;
1091
+ }
1092
+ if let Some(horizon) = horizon {
1093
+ state.horizon = horizon;
1094
+ }
1095
+ let value = serde_json::to_value(&state)?;
1096
+ append_derived_row_locked(inner, "_retention", &retention_key, value)
1097
+ }
1098
+
1099
+ /// The durable shape of a resource's retention state.
1100
+ ///
1101
+ /// Mirrors `state_model`'s private record so the write boundary can read and
1102
+ /// advance the horizon without the store type.
1103
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1104
+ struct RetentionRecord {
1105
+ policy: state_model::RetentionPolicy,
1106
+ horizon: u64,
1107
+ }
1108
+
649
1109
  impl FeltDb {
650
1110
  pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
651
1111
  let path = path.as_ref().to_path_buf();
1112
+
1113
+ // Fail closed. The format is decided before any record is interpreted
1114
+ // and before anything is written, so a database this build does not
1115
+ // understand is refused with its bytes untouched rather than opened
1116
+ // with whatever happened to deserialize.
1117
+ if let FormatCompatibility::Incompatible(incompatibility) = inspect_durable_format(&path)? {
1118
+ return Err(FlowError::IncompatibleFormat(Box::new(incompatibility)));
1119
+ }
1120
+
652
1121
  if let Some(parent) = path.parent() {
653
1122
  fs::create_dir_all(parent)?;
654
1123
  }
655
1124
  if !path.exists() {
656
- OpenOptions::new().create(true).append(true).open(&path)?;
1125
+ let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
1126
+ // A new database states its format in its first record, so it never
1127
+ // has to be inferred again.
1128
+ write_json_line(
1129
+ &mut file,
1130
+ &FormatRecord {
1131
+ record_type: FORMAT_RECORD_TYPE.to_string(),
1132
+ format_version: DURABLE_FORMAT_VERSION,
1133
+ },
1134
+ )?;
657
1135
  }
658
1136
 
659
1137
  // Generate instance ID from path hash
@@ -673,6 +1151,7 @@ impl FeltDb {
673
1151
  inner.sync_state = SyncState::new(instance_id);
674
1152
  replay_log(&mut inner)?;
675
1153
  load_sync_metadata(&mut inner)?;
1154
+ rebuild_revision_heads(&mut inner);
676
1155
 
677
1156
  let (event_tx, _) = broadcast::channel(1024);
678
1157
 
@@ -722,7 +1201,7 @@ impl FeltDb {
722
1201
  .to_string();
723
1202
  let rust_type = type_name::<Value>().to_string();
724
1203
  let (result, event) = {
725
- let mut inner = self.inner.lock().expect("lock poisoned");
1204
+ let mut inner = self.state();
726
1205
  let Some(current) = inner.rows.get(&capability).and_then(|rows| rows.get(key)) else {
727
1206
  return Ok(JsonCasResult::NotFound);
728
1207
  };
@@ -822,11 +1301,10 @@ impl FeltDb {
822
1301
  append_event(&inner.path, &row)?;
823
1302
  inner.authority_revision += 1;
824
1303
  inner.change_log.add_operation(operation);
825
- inner
826
- .rows
827
- .entry(capability.clone())
828
- .or_default()
829
- .insert(key.to_string(), row.clone());
1304
+ inner.put_row(row.clone());
1305
+ if is_versioned(&capability) {
1306
+ mint_revision_locked(&mut inner, key, value.to_string())?;
1307
+ }
830
1308
  let event = ChangeEvent {
831
1309
  capability,
832
1310
  key: key.to_string(),
@@ -975,7 +1453,7 @@ impl FeltDb {
975
1453
  .map(|(cap, _)| cap)
976
1454
  .unwrap_or("default")
977
1455
  .to_string();
978
- let inner = self.inner.lock().expect("lock poisoned");
1456
+ let inner = self.state();
979
1457
  let value = inner
980
1458
  .rows
981
1459
  .get(&capability)
@@ -991,7 +1469,7 @@ impl FeltDb {
991
1469
  F: Fn(&T) -> bool,
992
1470
  {
993
1471
  let rust_type = type_name::<T>().to_string();
994
- let mut inner = self.inner.lock().expect("lock poisoned");
1472
+ let mut inner = self.state();
995
1473
  *inner
996
1474
  .query_count_by_type
997
1475
  .entry(rust_type.clone())
@@ -1024,19 +1502,19 @@ impl FeltDb {
1024
1502
 
1025
1503
  pub fn is_auto_indexed<T>(&self) -> bool {
1026
1504
  let rust_type = type_name::<T>().to_string();
1027
- let inner = self.inner.lock().expect("lock poisoned");
1505
+ let inner = self.state();
1028
1506
  inner.adaptive_indexes.contains(&rust_type)
1029
1507
  }
1030
1508
 
1031
1509
  /// Get the instance ID for this FeltDB instance
1032
1510
  pub fn instance_id(&self) -> Result<String> {
1033
- let inner = self.inner.lock().expect("lock poisoned");
1511
+ let inner = self.state();
1034
1512
  Ok(inner.instance_id.clone())
1035
1513
  }
1036
1514
 
1037
1515
  /// Get the current sequence number for this instance
1038
1516
  pub fn sequence(&self) -> Result<u64> {
1039
- let inner = self.inner.lock().expect("lock poisoned");
1517
+ let inner = self.state();
1040
1518
  Ok(inner.sequence)
1041
1519
  }
1042
1520
 
@@ -1044,13 +1522,13 @@ impl FeltDb {
1044
1522
  /// In multi-process topology callers must use `AuthorityClient::read` to
1045
1523
  /// cross the authority boundary and obtain a current authoritative value.
1046
1524
  pub fn current_revision(&self) -> Result<u64> {
1047
- Ok(self.inner.lock().expect("lock poisoned").authority_revision)
1525
+ Ok(self.state().authority_revision)
1048
1526
  }
1049
1527
 
1050
1528
  /// Capture revision and rows under one lock. Authority reads must never
1051
1529
  /// pair metadata from one commit with state from another.
1052
1530
  pub(crate) fn authority_state(&self) -> (u64, Vec<StoredRow>) {
1053
- let inner = self.inner.lock().expect("lock poisoned");
1531
+ let inner = self.state();
1054
1532
  let rows = inner
1055
1533
  .rows
1056
1534
  .values()
@@ -1062,7 +1540,7 @@ impl FeltDb {
1062
1540
  /// Get the maintained cardinality (record count) for a collection capability.
1063
1541
  /// Returns the count of non-deleted records in the collection.
1064
1542
  pub fn collection_cardinality(&self, capability: &str) -> Result<u64> {
1065
- let inner = self.inner.lock().expect("lock poisoned");
1543
+ let inner = self.state();
1066
1544
  Ok(inner
1067
1545
  .collection_cardinality
1068
1546
  .get(capability)
@@ -1073,7 +1551,7 @@ impl FeltDb {
1073
1551
  /// Diagnostic: List all collection capabilities and their cardinalities.
1074
1552
  /// Used to detect capability format mismatches.
1075
1553
  pub fn list_cardinalities(&self) -> Result<Vec<(String, u64)>> {
1076
- let inner = self.inner.lock().expect("lock poisoned");
1554
+ let inner = self.state();
1077
1555
  let mut result: Vec<_> = inner
1078
1556
  .collection_cardinality
1079
1557
  .iter()
@@ -1086,7 +1564,7 @@ impl FeltDb {
1086
1564
  /// Diagnostic: Count actual rows for a given capability pattern.
1087
1565
  /// Used to verify that rows exist even if cardinality is 0.
1088
1566
  pub fn diagnostic_row_count(&self, capability_pattern: &str) -> Result<usize> {
1089
- let inner = self.inner.lock().expect("lock poisoned");
1567
+ let inner = self.state();
1090
1568
  Ok(inner
1091
1569
  .rows
1092
1570
  .iter()
@@ -1098,7 +1576,7 @@ impl FeltDb {
1098
1576
  /// Diagnostic: Get actual persisted capability strings matching a pattern.
1099
1577
  /// Used to identify capability identity formats in use.
1100
1578
  pub fn diagnostic_capability_keys(&self, capability_pattern: &str) -> Result<Vec<String>> {
1101
- let inner = self.inner.lock().expect("lock poisoned");
1579
+ let inner = self.state();
1102
1580
  let mut keys: Vec<_> = inner
1103
1581
  .rows
1104
1582
  .keys()
@@ -1195,7 +1673,7 @@ impl FeltDb {
1195
1673
  audit: Option<Value>,
1196
1674
  ) -> Result<AtomicCommit> {
1197
1675
  let (commit, events) = {
1198
- let mut inner = self.inner.lock().expect("lock poisoned");
1676
+ let mut inner = self.state();
1199
1677
  if inner.applied_transactions.contains(transaction_id) {
1200
1678
  if let Some(expected_hash) = payload_hash {
1201
1679
  if inner
@@ -1385,6 +1863,14 @@ impl FeltDb {
1385
1863
  &advanced
1386
1864
  };
1387
1865
 
1866
+ // Transaction bookkeeping is measured as one span per commit even
1867
+ // though it brackets two regions: everything before the durable
1868
+ // append, and the revision, dedup and payload-hash accounting after
1869
+ // the rows are applied. The append itself is `Persistence` and the
1870
+ // apply loop is `RecordMutation` + `IndexMaintenance`, so summing
1871
+ // this with those does not double-count any of them.
1872
+ let mut bookkeeping_nanos = 0u64;
1873
+ let prepare_began = workload_diagnostics::enabled().then(std::time::Instant::now);
1388
1874
  let base_revision = inner.authority_revision;
1389
1875
  if let Some(expected) = expected_parent {
1390
1876
  if expected != base_revision {
@@ -1480,6 +1966,9 @@ impl FeltDb {
1480
1966
  rows: rows.clone(),
1481
1967
  audit,
1482
1968
  };
1969
+ if let Some(began) = prepare_began {
1970
+ bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
1971
+ }
1483
1972
  append_transaction(&inner.path, &record)?;
1484
1973
  inner.authority_revision = base_revision + 1;
1485
1974
  inner.sequence = next_sequence;
@@ -1488,9 +1977,7 @@ impl FeltDb {
1488
1977
  inner.change_log.add_operation(operation);
1489
1978
  }
1490
1979
  if row.deleted {
1491
- if let Some(bucket) = inner.rows.get_mut(&row.capability) {
1492
- bucket.remove(&row.key);
1493
- }
1980
+ inner.remove_row(&row.capability, &row.key);
1494
1981
  // Decrement cardinality on delete
1495
1982
  let count = inner
1496
1983
  .collection_cardinality
@@ -1498,17 +1985,10 @@ impl FeltDb {
1498
1985
  .or_default();
1499
1986
  *count = count.saturating_sub(1);
1500
1987
  } else {
1501
- // Track whether this is an insert (new row) or update (existing row)
1502
- let is_insert = !inner
1503
- .rows
1504
- .get(&row.capability)
1505
- .is_some_and(|bucket| bucket.contains_key(&row.key));
1506
-
1507
- inner
1508
- .rows
1509
- .entry(row.capability.clone())
1510
- .or_default()
1511
- .insert(row.key.clone(), row.clone());
1988
+ // Record and index change together, inside the commit that
1989
+ // is already holding the state lock: a transaction that is
1990
+ // refused leaves neither, and one that commits publishes both.
1991
+ let is_insert = inner.put_row(row.clone());
1512
1992
 
1513
1993
  // Increment cardinality only on insert, not update
1514
1994
  if is_insert {
@@ -1520,6 +2000,7 @@ impl FeltDb {
1520
2000
  }
1521
2001
  }
1522
2002
  }
2003
+ let tail_began = workload_diagnostics::enabled().then(std::time::Instant::now);
1523
2004
  inner.sync_state.merge_vector_clock(&vector_clock);
1524
2005
  inner.applied_transactions.insert(transaction_id.into());
1525
2006
  let commit_revision = inner.authority_revision;
@@ -1531,6 +2012,13 @@ impl FeltDb {
1531
2012
  .transaction_payload_hashes
1532
2013
  .insert(transaction_id.into(), hash.into());
1533
2014
  }
2015
+ if let Some(began) = tail_began {
2016
+ bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
2017
+ }
2018
+ workload_diagnostics::record(
2019
+ workload_diagnostics::Phase::TransactionCommit,
2020
+ bookkeeping_nanos,
2021
+ );
1534
2022
  let events = rows
1535
2023
  .iter()
1536
2024
  .map(|row| ChangeEvent {
@@ -1603,6 +2091,9 @@ impl FeltDb {
1603
2091
  ) -> Result<()> {
1604
2092
  let rust_type = type_name::<T>().to_string();
1605
2093
  let value_json = serde_json::to_value(value)?;
2094
+ // Canonical because `serde_json` is built without `preserve_order`, so
2095
+ // `Map` is a `BTreeMap` and `to_string` emits keys in sorted order.
2096
+ let canonical = value_json.to_string();
1606
2097
  let mut row = StoredRow {
1607
2098
  capability: capability.clone(),
1608
2099
  key: key.clone(),
@@ -1616,7 +2107,7 @@ impl FeltDb {
1616
2107
  };
1617
2108
 
1618
2109
  {
1619
- let mut inner = self.inner.lock().expect("lock poisoned");
2110
+ let mut inner = self.state();
1620
2111
 
1621
2112
  // Increment sequence number
1622
2113
  inner.sequence += 1;
@@ -1645,11 +2136,10 @@ impl FeltDb {
1645
2136
 
1646
2137
  append_event(&inner.path, &row)?;
1647
2138
  inner.authority_revision += 1;
1648
- inner
1649
- .rows
1650
- .entry(capability.clone())
1651
- .or_default()
1652
- .insert(key.clone(), row);
2139
+ inner.put_row(row);
2140
+ if is_versioned(&capability) {
2141
+ mint_revision_locked(&mut inner, &key, canonical)?;
2142
+ }
1653
2143
  let suffix = key.rsplit_once(':').map(|(_, right)| right);
1654
2144
  if let Some(id) = suffix.and_then(|s| s.parse::<u64>().ok()) {
1655
2145
  let entry = inner.key_counters.entry(capability.clone()).or_default();
@@ -1676,6 +2166,9 @@ impl FeltDb {
1676
2166
  ) -> Result<()> {
1677
2167
  let rust_type = type_name::<T>().to_string();
1678
2168
  let value_json = serde_json::to_value(value)?;
2169
+ // Canonical because `serde_json` is built without `preserve_order`, so
2170
+ // `Map` is a `BTreeMap` and `to_string` emits keys in sorted order.
2171
+ let canonical = value_json.to_string();
1679
2172
  let mut row = StoredRow {
1680
2173
  capability: capability.clone(),
1681
2174
  key: key.clone(),
@@ -1689,7 +2182,7 @@ impl FeltDb {
1689
2182
  };
1690
2183
 
1691
2184
  {
1692
- let mut inner = self.inner.lock().expect("lock poisoned");
2185
+ let mut inner = self.state();
1693
2186
 
1694
2187
  // Increment sequence number
1695
2188
  inner.sequence += 1;
@@ -1718,11 +2211,10 @@ impl FeltDb {
1718
2211
 
1719
2212
  append_event(&inner.path, &row)?;
1720
2213
  inner.authority_revision += 1;
1721
- inner
1722
- .rows
1723
- .entry(capability.clone())
1724
- .or_default()
1725
- .insert(key.clone(), row);
2214
+ inner.put_row(row);
2215
+ if is_versioned(&capability) {
2216
+ mint_revision_locked(&mut inner, &key, canonical)?;
2217
+ }
1726
2218
  }
1727
2219
 
1728
2220
  let event = ChangeEvent {
@@ -1738,7 +2230,7 @@ impl FeltDb {
1738
2230
 
1739
2231
  fn delete_internal(&self, capability: String, key: String) -> Result<()> {
1740
2232
  {
1741
- let mut inner = self.inner.lock().expect("lock poisoned");
2233
+ let mut inner = self.state();
1742
2234
 
1743
2235
  // Get the row to determine its type
1744
2236
  let rust_type = inner
@@ -1788,10 +2280,8 @@ impl FeltDb {
1788
2280
  append_event(&inner.path, &tombstone)?;
1789
2281
  inner.authority_revision += 1;
1790
2282
 
1791
- // Remove from in-memory store
1792
- if let Some(bucket) = inner.rows.get_mut(&capability) {
1793
- bucket.remove(&key);
1794
- }
2283
+ // Remove from in-memory store, index entries included.
2284
+ inner.remove_row(&capability, &key);
1795
2285
  }
1796
2286
 
1797
2287
  let event = ChangeEvent {
@@ -1831,7 +2321,7 @@ impl FeltDb {
1831
2321
  let rust_type = type_name::<T>().to_string();
1832
2322
 
1833
2323
  let initial_results = {
1834
- let mut inner = self.inner.lock().expect("lock poisoned");
2324
+ let mut inner = self.state();
1835
2325
  *inner
1836
2326
  .query_count_by_type
1837
2327
  .entry(rust_type.clone())
@@ -1870,7 +2360,7 @@ impl FeltDb {
1870
2360
 
1871
2361
  /// Get current sync state information
1872
2362
  pub fn sync_info(&self) -> Result<SyncState> {
1873
- let inner = self.inner.lock().expect("lock poisoned");
2363
+ let inner = self.state();
1874
2364
  Ok(inner.sync_state.clone())
1875
2365
  }
1876
2366
 
@@ -1880,13 +2370,330 @@ impl FeltDb {
1880
2370
  }
1881
2371
 
1882
2372
  /// List the canonical values in one collection/capability namespace.
2373
+ ///
2374
+ /// This clones the whole collection, so its cost is linear in collection
2375
+ /// size. Readers that discard most of what they receive — a filtered query,
2376
+ /// for example — should use [`FeltDb::query_collection`] instead, which
2377
+ /// evaluates borrowed rows and clones only what it keeps.
1883
2378
  pub fn list_collection(&self, capability: &str) -> Result<Vec<StoredRow>> {
1884
- let inner = self.inner.lock().expect("lock poisoned");
1885
- Ok(inner
2379
+ let inner = self.state();
2380
+ let rows: Vec<StoredRow> = inner
1886
2381
  .rows
1887
2382
  .get(capability)
1888
2383
  .map(|rows| rows.values().cloned().collect())
1889
- .unwrap_or_default())
2384
+ .unwrap_or_default();
2385
+ query_execution_diagnostics::record_full_materialization(rows.len());
2386
+ Ok(rows)
2387
+ }
2388
+
2389
+ /// Evaluate a predicate over one collection without materializing it.
2390
+ ///
2391
+ /// The predicate observes each live row *borrowed* from state, so a record
2392
+ /// the predicate rejects is never cloned. Only retained rows are cloned,
2393
+ /// which happens while state protection is still held: the returned rows are
2394
+ /// owned, so nothing the caller keeps can reference storage a concurrent
2395
+ /// mutation may move or drop after the lock is released.
2396
+ ///
2397
+ /// Traversal is the collection's own record-key order, the same order
2398
+ /// [`FeltDb::list_collection_page`] walks. `limit` bounds accumulation and
2399
+ /// stops the traversal as soon as it is satisfied; because early stopping
2400
+ /// truncates in record-key order, a caller that must order results by some
2401
+ /// other field has to pass `None` and bound its own already-filtered set.
2402
+ ///
2403
+ /// The predicate runs under the state lock, so it must not call back into
2404
+ /// the database or block.
2405
+ pub fn query_collection<F>(
2406
+ &self,
2407
+ capability: &str,
2408
+ limit: Option<usize>,
2409
+ mut predicate: F,
2410
+ ) -> Result<Vec<StoredRow>>
2411
+ where
2412
+ F: FnMut(&StoredRow) -> bool,
2413
+ {
2414
+ let mut matched = Vec::new();
2415
+ let mut visited = 0usize;
2416
+ {
2417
+ let inner = self.state();
2418
+ let _query = workload_diagnostics::span(workload_diagnostics::Phase::ScanQuery);
2419
+ if let Some(rows) = inner.rows.get(capability) {
2420
+ for row in rows.values() {
2421
+ if limit.is_some_and(|bound| matched.len() >= bound) {
2422
+ break;
2423
+ }
2424
+ visited += 1;
2425
+ // The same predicate phase the indexed execution reports to,
2426
+ // so the two executions' predicate cost is directly
2427
+ // comparable rather than being two different measurements.
2428
+ let _predicate = workload_diagnostics::span(
2429
+ workload_diagnostics::Phase::PredicateEvaluation,
2430
+ );
2431
+ if predicate(row) {
2432
+ drop(_predicate);
2433
+ matched.push(row.clone());
2434
+ }
2435
+ }
2436
+ }
2437
+ }
2438
+ query_execution_diagnostics::record_scan(visited, matched.len());
2439
+ Ok(matched)
2440
+ }
2441
+
2442
+ // ----------------------------------------------------------------------
2443
+ // Equality index — lifecycle and execution.
2444
+ //
2445
+ // The index is derived state. Every method below either declares what is
2446
+ // derived, re-derives it from authoritative records, or reads candidate
2447
+ // record keys out of it. None of them is a source of truth, and none of
2448
+ // them can answer a query on its own: `query_collection_by_equality` still
2449
+ // evaluates the caller's full predicate against authoritative records.
2450
+ // ----------------------------------------------------------------------
2451
+
2452
+ /// Acquire the state lock, attributing the wait and the hold.
2453
+ ///
2454
+ /// Every path in this file that touches authoritative state goes through
2455
+ /// here, which is what makes lock contention a measured quantity rather than
2456
+ /// a residual inferred from wall-clock time.
2457
+ fn state(&self) -> StateGuard<'_> {
2458
+ if !workload_diagnostics::enabled() {
2459
+ return StateGuard {
2460
+ inner: self.inner.lock().expect("lock poisoned"),
2461
+ held_since: None,
2462
+ };
2463
+ }
2464
+ let requested = std::time::Instant::now();
2465
+ let inner = self.inner.lock().expect("lock poisoned");
2466
+ let acquired = std::time::Instant::now();
2467
+ workload_diagnostics::record(
2468
+ workload_diagnostics::Phase::StateLockWait,
2469
+ acquired
2470
+ .duration_since(requested)
2471
+ .as_nanos()
2472
+ .min(u64::MAX as u128) as u64,
2473
+ );
2474
+ StateGuard {
2475
+ inner,
2476
+ held_since: Some(acquired),
2477
+ }
2478
+ }
2479
+
2480
+ /// Declare an equality index on one collection field and populate it from
2481
+ /// authoritative state. Returns whether this call created the index.
2482
+ ///
2483
+ /// Creation and population happen under one lock, so an index is never
2484
+ /// observable in a half-built state. Declaring an index that already exists
2485
+ /// is not an error and does not rebuild it.
2486
+ ///
2487
+ /// `recordId` is refused: on the query surface it is authority metadata that
2488
+ /// shadows a document field of the same name on object-valued records only,
2489
+ /// so an index over the document field would answer a different question
2490
+ /// from the one the predicate asks.
2491
+ pub fn create_equality_index(&self, collection: &str, field: &str) -> Result<bool> {
2492
+ let mut inner = self.state();
2493
+ let created = inner
2494
+ .equality_index
2495
+ .declare(collection, field)
2496
+ .map_err(FlowError::CapabilityError)?;
2497
+ if created {
2498
+ let Inner {
2499
+ rows,
2500
+ equality_index,
2501
+ ..
2502
+ } = &mut *inner;
2503
+ equality_index.rebuild_field(collection, field, rows);
2504
+ }
2505
+ Ok(created)
2506
+ }
2507
+
2508
+ /// Drop an equality index and its buckets. Returns whether one existed.
2509
+ pub fn drop_equality_index(&self, collection: &str, field: &str) -> Result<bool> {
2510
+ let mut inner = self.state();
2511
+ Ok(inner.equality_index.undeclare(collection, field))
2512
+ }
2513
+
2514
+ /// Every declared index, as `(collection, field)` in a stable order.
2515
+ pub fn equality_indexes(&self) -> Result<Vec<(String, String)>> {
2516
+ let inner = self.state();
2517
+ Ok(inner.equality_index.declarations())
2518
+ }
2519
+
2520
+ /// Re-derive every declared index from authoritative records.
2521
+ ///
2522
+ /// This is the recovery path. An index is never read from disk and never
2523
+ /// received from a peer: after a restart, a snapshot install, or any other
2524
+ /// reload of authoritative state, the index is whatever this produces.
2525
+ pub fn rebuild_equality_indexes(&self) -> Result<EqualityIndexStats> {
2526
+ let mut inner = self.state();
2527
+ let Inner {
2528
+ rows,
2529
+ equality_index,
2530
+ ..
2531
+ } = &mut *inner;
2532
+ equality_index.rebuild(rows);
2533
+ Ok(equality_index.stats())
2534
+ }
2535
+
2536
+ /// Index size, as counts. Never a value, never a record id.
2537
+ pub fn equality_index_stats(&self) -> Result<EqualityIndexStats> {
2538
+ let inner = self.state();
2539
+ Ok(inner.equality_index.stats())
2540
+ }
2541
+
2542
+ /// A fully ordered rendering of the live index.
2543
+ ///
2544
+ /// Test instrumentation. It exists so a test can compare the live index
2545
+ /// against one rebuilt from authoritative state; nothing in execution reads
2546
+ /// it, and it is not part of any product surface.
2547
+ #[doc(hidden)]
2548
+ pub fn equality_index_snapshot(
2549
+ &self,
2550
+ ) -> Result<BTreeMap<String, BTreeMap<String, BTreeMap<String, Vec<String>>>>> {
2551
+ let inner = self.state();
2552
+ Ok(inner.equality_index.snapshot())
2553
+ }
2554
+
2555
+ /// The index invariant: the live index equals one derived from records now.
2556
+ ///
2557
+ /// Returns a description of the first divergence rather than a bare bool, so
2558
+ /// a failing assertion says what diverged. This is the strongest statement
2559
+ /// available about index maintenance, because it compares the maintained
2560
+ /// structure against the authoritative state it claims to describe rather
2561
+ /// than against a second copy of its own bookkeeping.
2562
+ pub fn verify_equality_index(&self) -> Result<std::result::Result<(), String>> {
2563
+ let inner = self.state();
2564
+ let live = inner.equality_index.snapshot();
2565
+ let mut expected = inner.equality_index.clone();
2566
+ expected.clear();
2567
+ expected.rebuild(&inner.rows);
2568
+ let expected = expected.snapshot();
2569
+ if live == expected {
2570
+ return Ok(Ok(()));
2571
+ }
2572
+ for (collection, fields) in expected.iter() {
2573
+ for (field, buckets) in fields {
2574
+ let observed = live.get(collection).and_then(|fields| fields.get(field));
2575
+ if observed != Some(buckets) {
2576
+ return Ok(Err(format!(
2577
+ "index for {collection}.{field} diverges from authoritative state: \
2578
+ {} value buckets held, {} derived",
2579
+ observed.map(BTreeMap::len).unwrap_or(0),
2580
+ buckets.len(),
2581
+ )));
2582
+ }
2583
+ }
2584
+ }
2585
+ Ok(Err(
2586
+ "the live index holds entries no authoritative record derives".to_string(),
2587
+ ))
2588
+ }
2589
+
2590
+ /// Candidate record keys for one equality condition, copied out of the index.
2591
+ ///
2592
+ /// `None` means the index cannot answer that condition — the field is not
2593
+ /// indexed, or the value is not an indexable scalar — and the caller must
2594
+ /// fall back to a scan. `Some(empty)` is an answer, not a failure.
2595
+ ///
2596
+ /// The keys are copied while the state lock is held. Nothing borrowed from
2597
+ /// the index or from the record map escapes this call.
2598
+ pub fn equality_index_candidates(
2599
+ &self,
2600
+ collection: &str,
2601
+ field: &str,
2602
+ value: &Value,
2603
+ ) -> Result<Option<Vec<String>>> {
2604
+ let inner = self.state();
2605
+ Ok(inner
2606
+ .equality_index
2607
+ .candidates(collection, field, value)
2608
+ .map(|keys| keys.iter().cloned().collect()))
2609
+ }
2610
+
2611
+ /// Execute a bounded query through the equality index when one applies.
2612
+ ///
2613
+ /// `equalities` are the query's equality conditions, in the caller's order.
2614
+ /// At least one of them must be indexed and hold an indexable value, or this
2615
+ /// returns `Ok(None)` and the caller falls back to [`FeltDb::query_collection`].
2616
+ /// Returning `None` rather than an empty result is the difference between
2617
+ /// "the index does not apply" and "nothing matches", and conflating the two
2618
+ /// would silently drop records.
2619
+ ///
2620
+ /// When the index does apply, every applicable condition contributes a
2621
+ /// candidate set and the sets are intersected smallest-first. The result is
2622
+ /// still only *candidates*: `predicate` — the caller's existing, unmodified
2623
+ /// predicate — decides what matches, evaluated against authoritative records.
2624
+ /// The index therefore cannot change what a query returns, only how many
2625
+ /// records the predicate has to look at.
2626
+ ///
2627
+ /// Candidate keys, the record lookups they drive and the predicate all run
2628
+ /// under one acquisition of the state lock, so a query observes one coherent
2629
+ /// committed state exactly as a scan does. Records are cloned only when the
2630
+ /// predicate keeps them, and nothing borrowed from state outlives the lock.
2631
+ pub fn query_collection_by_equality<F>(
2632
+ &self,
2633
+ capability: &str,
2634
+ equalities: &[(&str, &Value)],
2635
+ mut predicate: F,
2636
+ ) -> Result<Option<Vec<StoredRow>>>
2637
+ where
2638
+ F: FnMut(&StoredRow) -> bool,
2639
+ {
2640
+ let mut matched = Vec::new();
2641
+ let mut examined = 0usize;
2642
+ let mut evaluated = 0usize;
2643
+ {
2644
+ let inner = self.state();
2645
+ let _query = workload_diagnostics::span(workload_diagnostics::Phase::IndexedQuery);
2646
+ // Candidate selection, timed apart from record access: PR36 has to
2647
+ // be able to say whether the index lookup itself has become a cost,
2648
+ // and "index query time" alone could not distinguish a slow lookup
2649
+ // from a large candidate set.
2650
+ let lookup = workload_diagnostics::span(workload_diagnostics::Phase::IndexLookup);
2651
+ let mut applicable: Vec<&BTreeSet<String>> = Vec::new();
2652
+ for (field, value) in equalities {
2653
+ if let Some(keys) = inner.equality_index.candidates(capability, field, value) {
2654
+ applicable.push(keys);
2655
+ }
2656
+ }
2657
+ if applicable.is_empty() {
2658
+ drop(lookup);
2659
+ drop(_query);
2660
+ drop(inner);
2661
+ query_execution_diagnostics::record_index_miss();
2662
+ return Ok(None);
2663
+ }
2664
+ // Smallest first. This is not cost estimation: a set's length is
2665
+ // already held by the set, so intersecting from the smallest is
2666
+ // simply the cheaper order to do the same work.
2667
+ applicable.sort_by_key(|keys| keys.len());
2668
+ drop(lookup);
2669
+ let (smallest, rest) = applicable.split_first().expect("non-empty");
2670
+ let bucket = inner.rows.get(capability);
2671
+ for key in smallest.iter() {
2672
+ if !rest.iter().all(|keys| keys.contains(key)) {
2673
+ continue;
2674
+ }
2675
+ examined += 1;
2676
+ let retrieval =
2677
+ workload_diagnostics::span(workload_diagnostics::Phase::CandidateRetrieval);
2678
+ let row = bucket.and_then(|rows| rows.get(key));
2679
+ drop(retrieval);
2680
+ let Some(row) = row else {
2681
+ // An index entry with no record behind it would be a
2682
+ // maintenance defect. Skipping it keeps the query correct
2683
+ // rather than surfacing a record that does not exist.
2684
+ continue;
2685
+ };
2686
+ evaluated += 1;
2687
+ let _predicate =
2688
+ workload_diagnostics::span(workload_diagnostics::Phase::PredicateEvaluation);
2689
+ if predicate(row) {
2690
+ drop(_predicate);
2691
+ matched.push(row.clone());
2692
+ }
2693
+ }
2694
+ }
2695
+ query_execution_diagnostics::record_indexed_query(examined, evaluated, matched.len());
2696
+ Ok(Some(matched))
1890
2697
  }
1891
2698
 
1892
2699
  /// Traverse one collection in immutable record-key order without materializing it.
@@ -1896,7 +2703,7 @@ impl FeltDb {
1896
2703
  after: Option<&str>,
1897
2704
  limit: usize,
1898
2705
  ) -> Result<Vec<StoredRow>> {
1899
- let inner = self.inner.lock().expect("lock poisoned");
2706
+ let inner = self.state();
1900
2707
  let Some(rows) = inner.rows.get(capability) else {
1901
2708
  return Ok(vec![]);
1902
2709
  };
@@ -1916,7 +2723,7 @@ impl FeltDb {
1916
2723
 
1917
2724
  /// Fetch one live collection record without materializing collection state.
1918
2725
  pub fn get_collection_record(&self, capability: &str, key: &str) -> Result<Option<StoredRow>> {
1919
- let inner = self.inner.lock().expect("lock poisoned");
2726
+ let inner = self.state();
1920
2727
  Ok(inner
1921
2728
  .rows
1922
2729
  .get(capability)
@@ -1932,14 +2739,14 @@ impl FeltDb {
1932
2739
 
1933
2740
  /// Add a peer for synchronization
1934
2741
  pub fn add_sync_peer(&self, peer_id: String) -> Result<()> {
1935
- let mut inner = self.inner.lock().expect("lock poisoned");
2742
+ let mut inner = self.state();
1936
2743
  inner.sync_state.add_peer(peer_id);
1937
2744
  Ok(())
1938
2745
  }
1939
2746
 
1940
2747
  /// Remove a peer from synchronization
1941
2748
  pub fn remove_sync_peer(&self, peer_id: &str) -> Result<()> {
1942
- let mut inner = self.inner.lock().expect("lock poisoned");
2749
+ let mut inner = self.state();
1943
2750
  inner.sync_state.remove_peer(peer_id);
1944
2751
  Ok(())
1945
2752
  }
@@ -1950,7 +2757,7 @@ impl FeltDb {
1950
2757
  _peer_id: &str,
1951
2758
  since_sequence: u64,
1952
2759
  ) -> Result<Vec<Operation>> {
1953
- let inner = self.inner.lock().expect("lock poisoned");
2760
+ let inner = self.state();
1954
2761
  let pending: Vec<Operation> = inner
1955
2762
  .change_log
1956
2763
  .pending_operations()
@@ -1965,18 +2772,18 @@ impl FeltDb {
1965
2772
  &self,
1966
2773
  versions: &std::collections::HashMap<String, u64>,
1967
2774
  ) -> Result<Vec<Operation>> {
1968
- let inner = self.inner.lock().expect("lock poisoned");
2775
+ let inner = self.state();
1969
2776
  Ok(inner.change_log.operations_since(versions))
1970
2777
  }
1971
2778
 
1972
2779
  /// Return the highest durable operation sequence observed from each origin.
1973
2780
  pub fn operation_versions(&self) -> Result<std::collections::HashMap<String, u64>> {
1974
- let inner = self.inner.lock().expect("lock poisoned");
2781
+ let inner = self.state();
1975
2782
  Ok(inner.change_log.versions())
1976
2783
  }
1977
2784
 
1978
2785
  pub fn export_snapshot(&self) -> Result<DatabaseSnapshot> {
1979
- let inner = self.inner.lock().expect("lock poisoned");
2786
+ let inner = self.state();
1980
2787
  let mut rows: Vec<StoredRow> = inner
1981
2788
  .rows
1982
2789
  .values()
@@ -1992,13 +2799,14 @@ impl FeltDb {
1992
2799
  rows,
1993
2800
  versions,
1994
2801
  content_hash,
2802
+ format_version: DURABLE_FORMAT_VERSION,
1995
2803
  })
1996
2804
  }
1997
2805
  /// Current materialized rows including their causal operation envelopes.
1998
2806
  /// State-contract execution uses this for record-version preconditions;
1999
2807
  /// portable snapshots intentionally continue to strip those envelopes.
2000
2808
  pub fn state_rows(&self) -> Result<Vec<StoredRow>> {
2001
- let inner = self.inner.lock().expect("lock poisoned");
2809
+ let inner = self.state();
2002
2810
  Ok(inner
2003
2811
  .rows
2004
2812
  .values()
@@ -2012,7 +2820,7 @@ impl FeltDb {
2012
2820
  /// Selecting the capability buckets before cloning prevents one application's
2013
2821
  /// read snapshot from materializing every other application's durable rows.
2014
2822
  pub fn state_rows_for_namespace(&self, state_namespace: &str) -> Result<Vec<StoredRow>> {
2015
- let inner = self.inner.lock().expect("lock poisoned");
2823
+ let inner = self.state();
2016
2824
  let prefix = format!("{state_namespace}:");
2017
2825
  Ok(inner
2018
2826
  .rows
@@ -2023,15 +2831,33 @@ impl FeltDb {
2023
2831
  }
2024
2832
 
2025
2833
  pub fn can_install_snapshot(&self) -> Result<bool> {
2026
- let inner = self.inner.lock().expect("lock poisoned");
2834
+ let inner = self.state();
2027
2835
  Ok(!inner.bootstrapped && inner.rows.is_empty() && inner.change_log.pending.is_empty())
2028
2836
  }
2029
2837
 
2030
2838
  /// Install a verified bootstrap snapshot into a pristine node. A node with
2031
2839
  /// local history must converge through operations rather than overwrite it.
2032
2840
  pub fn install_snapshot(&self, snapshot: DatabaseSnapshot) -> Result<()> {
2841
+ // A snapshot carries whole rows, revisions included, so installing one
2842
+ // from another format would reintroduce exactly the silent
2843
+ // misinterpretation the on-disk version prevents.
2844
+ if snapshot.format_version != DURABLE_FORMAT_VERSION {
2845
+ return Err(FlowError::IncompatibleFormat(Box::new(
2846
+ FormatIncompatibility {
2847
+ found: if snapshot.format_version == 0 {
2848
+ DurableFormat::Unversioned
2849
+ } else {
2850
+ DurableFormat::Versioned(snapshot.format_version)
2851
+ },
2852
+ required: DURABLE_FORMAT_VERSION,
2853
+ reason: "the snapshot was produced in a different durable format".to_string(),
2854
+ action: "Take a fresh snapshot from a FeltDB build matching this one."
2855
+ .to_string(),
2856
+ },
2857
+ )));
2858
+ }
2033
2859
  snapshot.verify()?;
2034
- let mut inner = self.inner.lock().expect("lock poisoned");
2860
+ let mut inner = self.state();
2035
2861
  if !inner.rows.is_empty() || !inner.change_log.pending.is_empty() {
2036
2862
  return Err(FlowError::CapabilityError(
2037
2863
  "snapshot bootstrap requires a pristine node".to_string(),
@@ -2058,6 +2884,7 @@ impl FeltDb {
2058
2884
  &mut file,
2059
2885
  &LogHeader {
2060
2886
  record_type: "feltdb.snapshot.v1".to_string(),
2887
+ format_version: DURABLE_FORMAT_VERSION,
2061
2888
  local_sequence: inner.sequence,
2062
2889
  observed_versions: snapshot.versions.clone(),
2063
2890
  },
@@ -2069,7 +2896,7 @@ impl FeltDb {
2069
2896
  file.sync_all()?;
2070
2897
  fs::rename(temporary, &inner.path)?;
2071
2898
 
2072
- inner.rows.clear();
2899
+ inner.clear_rows();
2073
2900
  inner.key_counters.clear();
2074
2901
  for row in snapshot.rows {
2075
2902
  if let Some(id) = row
@@ -2083,11 +2910,7 @@ impl FeltDb {
2083
2910
  .or_default();
2084
2911
  *counter = (*counter).max(id + 1);
2085
2912
  }
2086
- inner
2087
- .rows
2088
- .entry(row.capability.clone())
2089
- .or_default()
2090
- .insert(row.key.clone(), row);
2913
+ inner.put_row(row);
2091
2914
  }
2092
2915
  inner.change_log.observed_versions = snapshot.versions;
2093
2916
  inner.bootstrapped = true;
@@ -2096,7 +2919,7 @@ impl FeltDb {
2096
2919
 
2097
2920
  /// Acknowledge receipt of operations from a peer
2098
2921
  pub fn acknowledge_peer_operations(&self, peer_id: String, sequence: u64) -> Result<()> {
2099
- let mut inner = self.inner.lock().expect("lock poisoned");
2922
+ let mut inner = self.state();
2100
2923
  inner.change_log.acknowledge_peer(peer_id, sequence);
2101
2924
  persist_sync_metadata(&inner)?;
2102
2925
  Ok(())
@@ -2108,25 +2931,31 @@ impl FeltDb {
2108
2931
  peer_id: String,
2109
2932
  versions: HashMap<String, u64>,
2110
2933
  ) -> Result<()> {
2111
- let mut inner = self.inner.lock().expect("lock poisoned");
2934
+ let mut inner = self.state();
2112
2935
  inner.change_log.acknowledge_versions(peer_id, versions);
2113
2936
  persist_sync_metadata(&inner)
2114
2937
  }
2115
2938
 
2116
2939
  pub fn acknowledged_peer_versions(&self, peer_id: &str) -> Result<HashMap<String, u64>> {
2117
- let inner = self.inner.lock().expect("lock poisoned");
2940
+ let inner = self.state();
2118
2941
  Ok(inner.change_log.acknowledged_versions(peer_id))
2119
2942
  }
2120
2943
 
2944
+ /// Earliest operation still available for incremental replay, by origin.
2945
+ pub fn retained_operation_floors(&self) -> Result<HashMap<String, u64>> {
2946
+ let inner = self.state();
2947
+ Ok(inner.change_log.retained_from())
2948
+ }
2949
+
2121
2950
  pub fn acknowledged_operation_count(&self, active_peers: &[String]) -> Result<usize> {
2122
- let inner = self.inner.lock().expect("lock poisoned");
2951
+ let inner = self.state();
2123
2952
  Ok(inner.change_log.acknowledged_operation_count(active_peers))
2124
2953
  }
2125
2954
 
2126
2955
  /// Atomically replace acknowledged history with a state snapshot and any
2127
2956
  /// operations still needed by at least one configured peer.
2128
2957
  pub fn compact_operation_log(&self, active_peers: &[String]) -> Result<usize> {
2129
- let mut inner = self.inner.lock().expect("lock poisoned");
2958
+ let mut inner = self.state();
2130
2959
  let removed = inner.change_log.prune_acknowledged(active_peers);
2131
2960
  if removed == 0 {
2132
2961
  return Ok(0);
@@ -2143,6 +2972,7 @@ impl FeltDb {
2143
2972
  &mut file,
2144
2973
  &LogHeader {
2145
2974
  record_type: "feltdb.snapshot.v1".to_string(),
2975
+ format_version: DURABLE_FORMAT_VERSION,
2146
2976
  local_sequence: inner.sequence,
2147
2977
  observed_versions: inner.change_log.versions(),
2148
2978
  },
@@ -2166,7 +2996,7 @@ impl FeltDb {
2166
2996
 
2167
2997
  /// Register a peer in the distributed fabric
2168
2998
  pub fn register_peer(&self, advertisement: PeerAdvertisement) -> Result<()> {
2169
- let inner = self.inner.lock().expect("lock poisoned");
2999
+ let inner = self.state();
2170
3000
  inner
2171
3001
  .peer_registry
2172
3002
  .register(advertisement)
@@ -2175,7 +3005,7 @@ impl FeltDb {
2175
3005
 
2176
3006
  /// Get a peer's advertisement
2177
3007
  pub fn get_peer(&self, peer_id: &PeerId) -> Result<Option<PeerAdvertisement>> {
2178
- let inner = self.inner.lock().expect("lock poisoned");
3008
+ let inner = self.state();
2179
3009
  inner
2180
3010
  .peer_registry
2181
3011
  .get(peer_id)
@@ -2184,7 +3014,7 @@ impl FeltDb {
2184
3014
 
2185
3015
  /// Find all peers providing a namespace
2186
3016
  pub fn peers_for_namespace(&self, namespace: &str) -> Result<Vec<PeerAdvertisement>> {
2187
- let inner = self.inner.lock().expect("lock poisoned");
3017
+ let inner = self.state();
2188
3018
  inner
2189
3019
  .peer_registry
2190
3020
  .peers_for_namespace(namespace)
@@ -2193,7 +3023,7 @@ impl FeltDb {
2193
3023
 
2194
3024
  /// Find all peers providing a capability
2195
3025
  pub fn peers_for_capability(&self, capability: &str) -> Result<Vec<PeerAdvertisement>> {
2196
- let inner = self.inner.lock().expect("lock poisoned");
3026
+ let inner = self.state();
2197
3027
  inner
2198
3028
  .peer_registry
2199
3029
  .peers_for_capability(capability)
@@ -2202,7 +3032,7 @@ impl FeltDb {
2202
3032
 
2203
3033
  /// Get all registered peers
2204
3034
  pub fn all_peers(&self) -> Result<Vec<PeerAdvertisement>> {
2205
- let inner = self.inner.lock().expect("lock poisoned");
3035
+ let inner = self.state();
2206
3036
  inner
2207
3037
  .peer_registry
2208
3038
  .all_peers()
@@ -2218,14 +3048,14 @@ impl FeltDb {
2218
3048
 
2219
3049
  /// Get current sync state
2220
3050
  pub fn sync_state(&self) -> Result<SyncState> {
2221
- let inner = self.inner.lock().expect("lock poisoned");
3051
+ let inner = self.state();
2222
3052
  Ok(inner.sync_state.clone())
2223
3053
  }
2224
3054
 
2225
3055
  /// Apply an operation from a remote peer
2226
3056
  /// This merges the vector clock, resolves conflicts, and applies the mutation
2227
3057
  pub fn apply_remote_operation(&self, op: Operation) -> Result<()> {
2228
- let mut inner = self.inner.lock().expect("lock poisoned");
3058
+ let mut inner = self.state();
2229
3059
 
2230
3060
  if op.content_hash != op.compute_content_hash() {
2231
3061
  return Err(FlowError::CapabilityError(
@@ -2324,18 +3154,12 @@ impl FeltDb {
2324
3154
  OperationType::Insert | OperationType::Update => {
2325
3155
  append_event(&inner.path, &row)?;
2326
3156
  inner.authority_revision += 1;
2327
- inner
2328
- .rows
2329
- .entry(op_to_apply.capability.clone())
2330
- .or_default()
2331
- .insert(op_to_apply.key.clone(), row);
3157
+ inner.put_row(row);
2332
3158
  }
2333
3159
  OperationType::Delete => {
2334
3160
  append_event(&inner.path, &row)?;
2335
3161
  inner.authority_revision += 1;
2336
- if let Some(bucket) = inner.rows.get_mut(&op_to_apply.capability) {
2337
- bucket.remove(&op_to_apply.key);
2338
- }
3162
+ inner.remove_row(&op_to_apply.capability, &op_to_apply.key);
2339
3163
  }
2340
3164
  }
2341
3165
  if let Some(id) = op_to_apply
@@ -2366,7 +3190,7 @@ impl FeltDb {
2366
3190
 
2367
3191
  /// Get the current vector clock state
2368
3192
  pub fn get_vector_clock(&self) -> Result<Option<std::collections::HashMap<String, u64>>> {
2369
- let inner = self.inner.lock().expect("lock poisoned");
3193
+ let inner = self.state();
2370
3194
  Ok(inner.sync_state.vector_clock.clone())
2371
3195
  }
2372
3196
  }
@@ -2380,7 +3204,7 @@ pub struct Capability {
2380
3204
  impl Capability {
2381
3205
  pub fn insert<T: Serialize>(&self, value: T) -> Result<String> {
2382
3206
  let key = {
2383
- let mut inner = self.db.inner.lock().expect("lock poisoned");
3207
+ let mut inner = self.db.state();
2384
3208
  let next = inner.key_counters.entry(self.name.clone()).or_default();
2385
3209
  let key = format!("{}:{}", self.name, *next);
2386
3210
  *next += 1;
@@ -2433,7 +3257,7 @@ impl Capability {
2433
3257
  F: Fn(&T) -> bool,
2434
3258
  {
2435
3259
  let rust_type = type_name::<T>().to_string();
2436
- let mut inner = self.db.inner.lock().expect("lock poisoned");
3260
+ let mut inner = self.db.state();
2437
3261
  *inner
2438
3262
  .query_count_by_type
2439
3263
  .entry(rust_type.clone())
@@ -2487,7 +3311,7 @@ impl Capability {
2487
3311
  let capability_name = self.name.clone();
2488
3312
 
2489
3313
  let initial_results = {
2490
- let mut inner = self.db.inner.lock().expect("lock poisoned");
3314
+ let mut inner = self.db.state();
2491
3315
  *inner
2492
3316
  .query_count_by_type
2493
3317
  .entry(rust_type.clone())
@@ -2568,7 +3392,7 @@ where
2568
3392
  }
2569
3393
 
2570
3394
  let results = {
2571
- let inner = self.db.inner.lock().expect("lock poisoned");
3395
+ let inner = self.db.state();
2572
3396
  let mut out = Vec::new();
2573
3397
  for bucket in inner.rows.values() {
2574
3398
  for row in bucket.values() {
@@ -2639,7 +3463,7 @@ where
2639
3463
  }
2640
3464
 
2641
3465
  let results = {
2642
- let inner = self.db.inner.lock().expect("lock poisoned");
3466
+ let inner = self.db.state();
2643
3467
  let mut out = Vec::new();
2644
3468
  if let Some(bucket) = inner.rows.get(&self.capability_name) {
2645
3469
  for row in bucket.values() {
@@ -2677,6 +3501,115 @@ where
2677
3501
  ///
2678
3502
  /// Damage anywhere earlier is not an interrupted write. Recovery refuses it
2679
3503
  /// rather than silently dropping committed history.
3504
+ /// Examine a durable database's format **without opening or modifying it**.
3505
+ ///
3506
+ /// This runs before any record is interpreted, and reads the file only. A
3507
+ /// database this reports as incompatible is left exactly as it was found.
3508
+ ///
3509
+ /// The order matters: an explicit format record decides, and the records
3510
+ /// themselves are examined *only* when no format record exists — the
3511
+ /// unversioned era. Compatibility is never inferred from whether a
3512
+ /// deserialization happened to succeed.
3513
+ pub fn inspect_durable_format(path: &Path) -> Result<FormatCompatibility> {
3514
+ if !path.exists() {
3515
+ // A database that does not exist yet will be created in the current
3516
+ // format.
3517
+ return Ok(FormatCompatibility::Compatible(DurableFormat::Versioned(
3518
+ DURABLE_FORMAT_VERSION,
3519
+ )));
3520
+ }
3521
+
3522
+ let lines = read_log_lines(path)?;
3523
+ let mut declared: Option<u32> = None;
3524
+ let mut legacy_revision = false;
3525
+
3526
+ for line in &lines {
3527
+ if line.trim().is_empty() {
3528
+ continue;
3529
+ }
3530
+ let Ok(value) = serde_json::from_str::<Value>(line) else {
3531
+ continue;
3532
+ };
3533
+ match value.get("record_type").and_then(Value::as_str) {
3534
+ Some(FORMAT_RECORD_TYPE) => {
3535
+ if let Some(version) = value.get("format_version").and_then(Value::as_u64) {
3536
+ declared = Some(version as u32);
3537
+ }
3538
+ }
3539
+ Some("feltdb.snapshot.v1" | "flowdb.snapshot.v1") => {
3540
+ match value.get("format_version").and_then(Value::as_u64) {
3541
+ Some(version) if version > 0 => declared = Some(version as u32),
3542
+ _ => {}
3543
+ }
3544
+ }
3545
+ _ => {
3546
+ // The unversioned-era probe: a revision record that predates
3547
+ // resource-scoped identity. Deliberate and specific — it looks
3548
+ // for a named absence rather than trying to parse and shrugging
3549
+ // at failure.
3550
+ let is_revision = value
3551
+ .get("key")
3552
+ .and_then(Value::as_str)
3553
+ .is_some_and(|key| key.starts_with("state:revision:"));
3554
+ if is_revision
3555
+ && value
3556
+ .get("value")
3557
+ .and_then(Value::as_object)
3558
+ .is_some_and(|revision| !revision.contains_key("resource"))
3559
+ {
3560
+ legacy_revision = true;
3561
+ }
3562
+ }
3563
+ }
3564
+ }
3565
+
3566
+ if let Some(version) = declared {
3567
+ return Ok(match version.cmp(&DURABLE_FORMAT_VERSION) {
3568
+ std::cmp::Ordering::Equal => {
3569
+ FormatCompatibility::Compatible(DurableFormat::Versioned(version))
3570
+ }
3571
+ std::cmp::Ordering::Greater => {
3572
+ FormatCompatibility::Incompatible(FormatIncompatibility {
3573
+ found: DurableFormat::Versioned(version),
3574
+ required: DURABLE_FORMAT_VERSION,
3575
+ reason: "the database was written by a newer FeltDB".to_string(),
3576
+ action: "Run a FeltDB build that understands this format, or restore \
3577
+ a backup taken in an older format."
3578
+ .to_string(),
3579
+ })
3580
+ }
3581
+ std::cmp::Ordering::Less => FormatCompatibility::Incompatible(FormatIncompatibility {
3582
+ found: DurableFormat::Versioned(version),
3583
+ required: DURABLE_FORMAT_VERSION,
3584
+ reason: "the database was written in an older durable format".to_string(),
3585
+ action: "Migrate the database with a FeltDB build that supports both \
3586
+ formats, or restore a backup."
3587
+ .to_string(),
3588
+ }),
3589
+ });
3590
+ }
3591
+
3592
+ if legacy_revision {
3593
+ return Ok(FormatCompatibility::Incompatible(FormatIncompatibility {
3594
+ found: DurableFormat::Unversioned,
3595
+ required: DURABLE_FORMAT_VERSION,
3596
+ reason: "it holds revision records written before revisions belonged to a \
3597
+ resource, and those records cannot be migrated without inventing \
3598
+ information they never contained: a revision recorded no resource, \
3599
+ so which history each one belongs to is not recoverable from the data"
3600
+ .to_string(),
3601
+ action: "Export the application state you need from a FeltDB build that \
3602
+ understands the old format, then load it into a new database. The \
3603
+ existing file has not been modified."
3604
+ .to_string(),
3605
+ }));
3606
+ }
3607
+
3608
+ // No format record and no legacy revision: nothing in this database depends
3609
+ // on the difference.
3610
+ Ok(FormatCompatibility::Compatible(DurableFormat::Unversioned))
3611
+ }
3612
+
2680
3613
  fn read_log_lines(path: &Path) -> Result<Vec<String>> {
2681
3614
  let mut file = BufReader::new(OpenOptions::new().read(true).open(path)?);
2682
3615
  let mut lines: Vec<(String, bool)> = Vec::new();
@@ -2733,6 +3666,10 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
2733
3666
  snapshot_log = true;
2734
3667
  continue;
2735
3668
  }
3669
+ if value.get("record_type").and_then(Value::as_str) == Some(FORMAT_RECORD_TYPE) {
3670
+ // Already honoured by `inspect_durable_format` before the open.
3671
+ continue;
3672
+ }
2736
3673
  if value.get("record_type").and_then(Value::as_str) == Some("feltdb.transaction.v1") {
2737
3674
  let transaction: TransactionLogRecord = serde_json::from_value(value)?;
2738
3675
  // Older records did not carry a commit revision. Their durable
@@ -2772,15 +3709,9 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
2772
3709
  inner.change_log.add_operation(operation);
2773
3710
  }
2774
3711
  if row.deleted {
2775
- if let Some(bucket) = inner.rows.get_mut(&row.capability) {
2776
- bucket.remove(&row.key);
2777
- }
3712
+ inner.remove_row(&row.capability, &row.key);
2778
3713
  } else {
2779
- inner
2780
- .rows
2781
- .entry(row.capability.clone())
2782
- .or_default()
2783
- .insert(row.key.clone(), row);
3714
+ inner.put_row(row);
2784
3715
  }
2785
3716
  }
2786
3717
  }
@@ -2817,15 +3748,9 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
2817
3748
  *counter = (*counter).max(id + 1);
2818
3749
  }
2819
3750
  if row.deleted {
2820
- if let Some(bucket) = inner.rows.get_mut(&row.capability) {
2821
- bucket.remove(&row.key);
2822
- }
3751
+ inner.remove_row(&row.capability, &row.key);
2823
3752
  } else {
2824
- inner
2825
- .rows
2826
- .entry(row.capability.clone())
2827
- .or_default()
2828
- .insert(row.key.clone(), row);
3753
+ inner.put_row(row);
2829
3754
  }
2830
3755
  }
2831
3756
 
@@ -2904,6 +3829,10 @@ fn persist_sync_metadata(inner: &Inner) -> Result<()> {
2904
3829
  }
2905
3830
 
2906
3831
  fn append_event(path: &Path, event: &StoredRow) -> Result<()> {
3832
+ // Single-record durability: the line is written and flushed to the OS, but
3833
+ // not fsynced. `append_transaction` below does fsync. The asymmetry is
3834
+ // pre-existing and PR36 measures it rather than changing it.
3835
+ let _span = workload_diagnostics::span(workload_diagnostics::Phase::Persistence);
2907
3836
  let mut file = OpenOptions::new().create(true).append(true).open(path)?;
2908
3837
  let mut line = serde_json::to_string(event)?;
2909
3838
  line.push('\n');
@@ -2913,6 +3842,10 @@ fn append_event(path: &Path, event: &StoredRow) -> Result<()> {
2913
3842
  }
2914
3843
 
2915
3844
  fn append_transaction(path: &Path, transaction: &TransactionLogRecord) -> Result<()> {
3845
+ // A transaction commit fsyncs. This span therefore includes the fsync, which
3846
+ // is the point: it is the durability cost of a commit, measured rather than
3847
+ // assumed.
3848
+ let _span = workload_diagnostics::span(workload_diagnostics::Phase::Persistence);
2916
3849
  let mut file = OpenOptions::new().create(true).append(true).open(path)?;
2917
3850
  write_json_line(&mut file, transaction)?;
2918
3851
  file.sync_data()?;