1macro_rules! unstable_internal_modules {
126 ($($name:ident),* $(,)?) => {
127 $(
128 #[cfg(feature = "unstable-internals")]
129 pub mod $name;
130 #[cfg(not(feature = "unstable-internals"))]
131 #[allow(dead_code, unused_imports)]
132 mod $name;
133 )*
134 };
135}
136
137unstable_internal_modules! {
138 adversarial_transport,
139 concurrency_fuzzing,
140 crash_injection,
141 replay_fuzzing,
142 permutation_scheduler,
143 persistence_reality,
144 in_process_transport,
145 query_execution_diagnostics,
146 workload_diagnostics,
147 cardinality_endpoint,
148}
149
150#[cfg(test)]
151mod acceptance_tests;
152mod acquisition;
153pub mod admission;
154#[cfg(test)]
155mod admission_contract_tests;
156pub mod analytics;
157pub mod application;
158pub mod application_runtime;
159#[cfg(not(target_arch = "wasm32"))]
160pub mod authority;
161pub mod authority_failover;
162pub mod authorization;
163#[cfg(test)]
164mod authorization_security_tests;
165pub mod capabilities;
166mod capability;
167mod cardinality_diagnostics;
168#[cfg(test)]
169mod causal_backlog_bound;
170pub mod causal_dependency_barrier;
171#[cfg(test)]
172mod causal_dependency_barrier_phase_7_1;
173pub mod consistency_contract;
174mod content_distribution;
175pub mod convergence;
176pub mod crash_atomic_boundary;
177#[cfg(test)]
178mod crash_recovery_tests;
179mod cron;
180#[cfg(test)]
181mod dedup_bound_investigation;
182pub mod distributed_indexing;
183#[cfg(test)]
184mod distributed_tests;
185pub mod distributed_transactions;
186pub mod durability_guarantees;
187pub mod durable_dedup_set;
188#[cfg(test)]
189mod durable_operation_identity;
190pub mod durable_operation_log;
191pub mod durable_sync;
192pub mod equality_index;
193mod execution;
194pub mod indexing;
195#[cfg(test)]
196mod managed_cas_tests;
197mod materialization;
198pub mod metrics;
199pub mod multi_node_convergence;
200pub mod multi_operation_transaction;
201pub mod observability;
202mod operation;
203pub mod operation_algebra;
204pub mod operation_log;
205#[cfg(test)]
206mod p1_application_atomicity;
207#[cfg(test)]
208mod p1_atomicity_acceptance;
209pub mod partition_reconciliation;
210mod peer_registry;
211#[cfg(test)]
212mod phase1b_acceptance;
213#[cfg(test)]
214mod phase1c1_acceptance;
215#[cfg(test)]
216mod phase1c2_acceptance;
217#[cfg(test)]
218mod phase1c3_acceptance;
219#[cfg(test)]
220mod phase1c_atomicity_proof;
221#[cfg(test)]
222mod phase5_integration;
223#[cfg(test)]
224mod phase5_scenarios;
225#[cfg(test)]
226mod phase6_adversarial_scenarios;
227#[cfg(test)]
228mod phase6_convergence_validator;
229#[cfg(test)]
230mod phase6_persistence;
231#[cfg(test)]
232#[cfg(test)]
233mod phase_1c_real_tcp;
234#[cfg(test)]
235mod phase_2a_failures;
236#[cfg(test)]
237mod phase_2b_network;
238#[cfg(test)]
239mod phase_2c_cascading;
240#[cfg(test)]
241mod phase_3_durability;
242#[cfg(test)]
243mod phase_4_baseline;
244#[cfg(test)]
245mod phase_5_soak;
246pub mod policy_evaluation;
247pub mod production_api;
248mod provenance;
249pub mod query_performance;
250#[cfg(test)]
251mod read_basis;
252mod references;
253pub mod replica_acknowledgements;
254pub mod replica_membership;
255pub mod replication_manager;
256pub mod replication_protocol;
257mod routing;
258pub mod semantic_decision;
259pub mod semantic_query;
260pub mod sharding;
261pub mod agent_state;
262pub mod agent_transition_contract;
263pub mod state_conflict_contract;
264pub mod state_contract;
265pub mod state_diff_contract;
266pub mod state_facade;
267pub mod state_hash;
268pub mod state_model;
269pub mod state_transition_store;
270pub mod state_trigger;
271mod storage;
272pub mod submission;
273mod sync;
274pub mod sync_contract;
275#[cfg(not(target_arch = "wasm32"))]
276pub mod tcp_transport;
277pub mod transaction_api;
278#[cfg(test)]
279mod transaction_invariants;
280#[cfg(test)]
281mod transaction_preconditions;
282pub mod transactions;
283mod trigger;
284pub mod trigger_contract;
285pub mod versioned_object_substrate;
286pub mod worker_mesh;
287mod workflow;
288#[cfg(test)]
289mod workflow_acceptance_tests;
290mod workflow_integration;
291mod workflow_runtime;
292pub mod workload;
293
294pub use acquisition::{
295 AcquisitionDeduplicator, AcquisitionPolicy, AcquisitionResult, ContentRequest, ContentResponse,
296 ContentTransferError,
297};
298pub use admission::{ProductionRetryPolicy, RejectionReason, SubmissionOutcome};
299#[cfg(feature = "unstable-internals")]
300pub use adversarial_transport::{
301 AdversarialTransport, ConnectionId, ConnectivityMatrix, FaultSeed, InMemoryTransport,
302 LinkState, MessageId, NetworkEvent, NetworkSchedule, NodeId, ReplicationTransport,
303};
304pub use analytics::{
305 AutoTuner, IndexAnalytics, IndexMetrics, PerformanceReport, QueryTrace, TuningRecommendation,
306};
307pub use capability::{
308 CapabilityAccessModel, CapabilityCheckpoint, CapabilityContext, CapabilityExecutionContext,
309 CapabilityExecutionResult, CapabilityId, CapabilityLifecycleState, CapabilityMetadata,
310 CapabilityOperationResult, CapabilityQuery, CapabilityQueryResult, CapabilityRef,
311 CapabilityRegistry, CapabilityRequirements, CapabilityResolver, CapabilityRole,
312 DistributedCapabilityAdvertisement, ExecutionRef, FlowCapability, StateRequirement,
313};
314pub use causal_dependency_barrier::{
315 ApplyResult, CausalDependencyBarrier, OperationState, PendingOperation, ReceiveResult,
316 StateCounters,
317};
318pub use consistency_contract::{
319 CommutativeVerificationResult, ConsensusEnforcementResult, ConsensusRequirementChecker,
320 ConsistencyContractEnforcer, DeterministicMergeVerificationResult,
321 NoDowngradeVerificationResult, OperationApplication,
322};
323pub use content_distribution::{
324 CapabilityLocationRegistry, ContentAcquisitionManager, ContentAddressableStore,
325 DistributedReferenceResolver, ProjectionDistribution,
326};
327pub use convergence::{
328 CausalHistory, ConflictResolution, ConflictResolutionStrategy, MergeResult, StateVersion,
329 VectorClock,
330};
331pub use crash_atomic_boundary::{
332 CrashAtomicExecutor, CrashBoundaryPoint, CrashRecoveryState, InvariantCheckResult,
333 RecoveryDecision,
334};
335pub use cron::{CronSchedule, CronScheduler};
336pub use distributed_indexing::{
337 ConsistencyLevel, ConsistencyProtocol, DistributedIndexManager, DistributedSyncStatus,
338 IndexOperation, IndexReplicationState, IndexSyncMessage, IndexVectorClock, RemoteIndexMetadata,
339 RemoteIndexState, ReplicationStatus, VersionConflict,
340};
341pub use distributed_transactions::{
342 DistributedTransactionExecutor, EnvelopeId, ReplicaState, ReplicationMessage,
343 TransactionEnvelope,
344};
345pub use equality_index::{EqualityIndex, EqualityIndexStats, IndexKey};
346pub use execution::{Execution, ExecutionQueue, ExecutionStatus, RetryPolicy};
347pub use indexing::{IndexConfig, IndexManager, IndexStats, IndexType};
348pub use materialization::{MaterializationHandler, ReactiveCollectionIntegration};
349pub use multi_node_convergence::{
350 ConvergenceAggregation, ConvergenceResult, MultiNodeConvergenceSimulator, NodeExecutionResult,
351};
352pub use operation::{MutationOrder, Operation, OperationType, DEFAULT_MUTATION_ORDER};
353pub use operation_algebra::{
354 ConflictType, ConvergenceSemantic, OperationAlgebra, OperationAlgebraRegistry,
355};
356pub use operation_log::{MemoryOperationLog, OperationLogEntry};
357pub use partition_reconciliation::{
358 DeterminismCheckResult, PartitionDeterminismChecker, PartitionTrace, PartitionedReplica,
359 ReconciliationResult, ReplicaPartition,
360};
361pub use peer_registry::PeerRegistry;
362#[cfg(feature = "unstable-internals")]
363pub use permutation_scheduler::{OperationSchedule, PermutationScheduler, ScheduleStrategy};
364#[cfg(feature = "unstable-internals")]
365pub use persistence_reality::{
366 CorruptionDetectionResult, DurablePersistenceLayer, FsyncDurabilityResult,
367 PersistenceContractVerifier, PersistenceCrashPoint, RecoveryResult, WALEntry, WriteResult,
368};
369pub use provenance::{
370 AcquisitionMetadata, DerivedState, DerivedStateProvenance, DerivedValue, OperationSource,
371};
372pub use references::{
373 ContentIdentity, ContentRange, FlowRef, PeerAdvertisement, PeerId, ReferenceResolver,
374 ResolutionMetadata, ResolutionResult,
375};
376pub use routing::{
377 CapabilityRouter, ExecutionClaim, ExecutionClaimer, ExecutionResultPublisher, PeerStateVersion,
378 RouteDecision,
379};
380pub use semantic_decision::{
381 authorize_context, configured_decision_runtime, decide_record, decision_cache_key,
382 deterministic_request_key, reuse_record_judgment,
383 execute_decision_batch, execute_decision_transport, execute_inline_decision_transport, jev,
384 run_choice_diagnostics, run_decision_benchmark, validate_decision_definition,
385 validate_decision_result, BinaryDecisionResult, ChoiceDecisionResult, ChoiceDiagnosticsReport,
386 ChoiceExpansionObservation, ChoicePermutationObservation, DecisionAuthorizationConfig,
387 DecisionAuthorizationDecision, DecisionAuthorizationGate, DecisionBatch, DecisionBatchDecision,
388 DecisionBatchEvaluation, DecisionBenchmarkCase, DecisionBenchmarkReport, DecisionContextPacket,
389 DecisionDefinition, DecisionError, DecisionEvaluation, DecisionEvidence, DecisionEvidenceField,
390 DecisionExecutionMethod, DecisionJudgmentRecord, DecisionKind, DecisionOptions,
391 DecisionPrincipal, DecisionProvenance, DecisionRecordRef, DecisionRequest, DecisionResult,
392 DecisionRuntime, DecisionRuntimeCapabilities, DecisionRuntimeMetadata, DecisionStateReference,
393 DecisionStatus, DecisionTransportAuthorization, DecisionTransportExecutionRequest,
394 DecisionTransportExecutionResponse, DecisionTransportRuntime, DeterministicDecisionRuntime,
395 ScoreDecisionResult, WeightedOption, DEFAULT_DECISION_COLLECTION,
396};
397pub use semantic_query::{
398 compose_semantic_page, semantic_clause_hash, semantic_definition_hash,
399 validate_semantic_clause, SemanticAnnotation, SemanticCandidate, SemanticOrderDirection,
400 SemanticPage, SemanticQueryClause, SemanticQueryError, SemanticQueryFilter,
401 SemanticQueryMode, SemanticQueryOrder, SemanticStageSummary, SemanticUnevaluated,
402 SemanticUnevaluatedReason, MAX_SEMANTIC_CANDIDATES,
403};
404pub use sharding::{
405 HotspotAlert, RebalanceOperation, ShardDistributionSummary, ShardId, ShardKey, ShardManager,
406 ShardMetrics, ShardRange, ShardingStrategy,
407};
408pub use state_conflict_contract::{
409 classify_states, StateConflictInput, StateConflictOutput, STATE_CONFLICT_CONTRACT_ID,
410 STATE_CONFLICT_CONTRACT_VERSION,
411};
412pub use state_diff_contract::{
413 diff_states, StateDiffInput, StateDiffOutput, STATE_DIFF_CONTRACT_ID,
414 STATE_DIFF_CONTRACT_VERSION,
415};
416pub use state_facade::FeltDBStateSystem;
417pub use state_hash::{CanonicalState, StateHash};
418pub use state_model::{
419 apply_reconciliation_plan, path_relation, paths_overlap, reconcile, resolve_path, ChangeKind,
420 ConflictClass, ConflictClassification, GarbageCollectionError, GarbageCollectionReport,
421 MaterializationError, MaterializationProjection, PathComponent, PathConflict, PathRelation,
422 ReconciliationPlan, RefError, Relationship, SemanticChange, SemanticDiff, StateId, StateObject,
423 StateReconciliationResult, StateRef, StateRevision, StateStore, StateTopology,
424 STATE_MODEL_VERSION,
425};
426pub use state_transition_store::{MemoryStateTransitionStore, StateTransitionRecord};
427pub use state_trigger::{
428 StateTrigger, StateTriggerChangeKind, StateTriggerError, StateTriggerPredicate,
429 StateTriggerStore, TriggeredWorkload,
430};
431pub use storage::{CheckpointData, FileStorage, MemoryStorage, Storage};
432pub use submission::{SubmissionManager, SubmissionMetrics};
433pub use sync::{ChangeLog, Conflict, ConflictDetector, PeerState, SyncMessage, SyncState};
434pub use transaction_api::{TransactionManager, TxError, TxResult};
435pub use transactions::{
436 CommitId, CommitValidator, ConsistencyContract, OperationCommand, OperationId, StateDependency,
437 StateTransition, TransactionExecutor, TransitionResult,
438};
439pub use trigger::{Trigger, TriggerFilter, TriggerRegistry};
440pub use trigger_contract::{
441 canonical_input_bytes, canonical_output_bytes, evaluate_canonical, evaluate_trigger,
442 ContractChange, ContractChangeKind, ContractErrorBody, ContractErrorCode, ContractPredicate,
443 ContractRuntime, ContractTrigger, MatchOutcome, MatchReason, TriggerMatchInput,
444 TriggerMatchOutput, TRIGGER_MATCH_CONTRACT_ID, TRIGGER_MATCH_CONTRACT_VERSION,
445};
446pub use versioned_object_substrate::{
447 DurableObjectStore, DurableRef, LocalObjectStore, Reachability, S3CompatibleBucket,
448 S3CompatibleObjectStore, SubstrateError, VersionedObjectSubstrate,
449};
450pub use workflow::{
451 BlockedReason, WorkflowGraph, WorkflowInstance, WorkflowNode, WorkflowOperation, WorkflowRef,
452 WorkflowState, WorkflowStepRef, WorkflowStepState,
453};
454pub use workflow_integration::{
455 WorkflowCronTrigger, WorkflowIntegration, WorkflowProvenance, WorkflowTrigger,
456};
457pub use workflow_runtime::{WorkflowOperationEntry, WorkflowRuntime};
458
459use crate::state_contract::AuthorizationContext;
460use serde::de::DeserializeOwned;
461use serde::{Deserialize, Serialize};
462use serde_json::Value;
463use sha2::{Digest, Sha256};
464use std::any::type_name;
465use std::collections::hash_map::DefaultHasher;
466use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
467use std::fmt::{Display, Formatter};
468use std::fs::{self, File, OpenOptions};
469use std::hash::{Hash, Hasher};
470use std::io::{BufRead, BufReader, Write};
471use std::ops::Bound::{Excluded, Unbounded};
472use std::path::{Path, PathBuf};
473use std::sync::{Arc, Mutex, OnceLock, Weak};
474use std::time::{SystemTime, UNIX_EPOCH};
475use tokio::sync::broadcast;
476
477pub type Result<T> = std::result::Result<T, FlowError>;
478
479#[derive(Debug)]
486#[non_exhaustive]
487pub enum FlowError {
488 Io(std::io::Error),
489 Serde(serde_json::Error),
490 CorruptLogLine(Box<LogCorruption>),
491 CapabilityError(String),
492 PreconditionFailed(Box<PreconditionFailure>),
498 TransactionIdPayloadMismatch {
506 transaction_id: String,
507 },
508 RevisionConflict {
510 expected: u64,
511 actual: u64,
512 },
513 BackupRejected(Box<BackupProblem>),
515 IncompatibleFormat(Box<FormatIncompatibility>),
521 Durability(Box<DurabilityFailure>),
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
532#[serde(rename_all = "snake_case")]
533#[non_exhaustive]
534pub enum DurabilityFailureKind {
535 StorageFull,
538 WriteFailed,
540 SyncFailed,
543 Indeterminate,
547 RecoveryRequired,
550}
551
552impl DurabilityFailureKind {
553 pub fn code(self) -> &'static str {
555 match self {
556 DurabilityFailureKind::StorageFull => "STORAGE_FULL",
557 DurabilityFailureKind::WriteFailed => "STORAGE_WRITE_FAILED",
558 DurabilityFailureKind::SyncFailed => "STORAGE_SYNC_FAILED",
559 DurabilityFailureKind::Indeterminate => "DURABILITY_FAILURE",
560 DurabilityFailureKind::RecoveryRequired => "RECOVERY_REQUIRED",
561 }
562 }
563}
564
565#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567pub struct DurabilityFailure {
568 pub kind: DurabilityFailureKind,
569 pub committed: Option<bool>,
572 pub detail: String,
574}
575
576impl DurabilityFailure {
577 pub fn code(&self) -> &'static str {
578 self.kind.code()
579 }
580}
581
582impl Display for DurabilityFailure {
583 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
584 let outcome = match self.committed {
585 Some(false) => "nothing was committed",
586 Some(true) => "the write was committed",
587 None => "whether the write was committed is unknown until the database is reopened",
588 };
589 write!(f, "{}: {}; {outcome}", self.code(), self.detail)
590 }
591}
592
593impl Display for FlowError {
594 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
595 match self {
596 FlowError::Io(e) => write!(f, "io error: {e}"),
597 FlowError::Serde(e) => write!(f, "serde error: {e}"),
598 FlowError::CorruptLogLine(corruption) => write!(f, "CORRUPT_DURABLE_LOG: {corruption}"),
599 FlowError::CapabilityError(msg) => write!(f, "capability error: {msg}"),
600 FlowError::PreconditionFailed(failure) => write!(f, "PRECONDITION_FAILED: {failure}"),
601 FlowError::TransactionIdPayloadMismatch { transaction_id } => write!(
602 f,
603 "IDEMPOTENCY_CONFLICT: transaction {transaction_id} was already committed with a different payload"
604 ),
605 FlowError::RevisionConflict { expected, actual } => write!(
606 f,
607 "REVISION_CONFLICT: expected authority revision {expected}, current revision is {actual}"
608 ),
609 FlowError::IncompatibleFormat(incompatibility) => {
610 write!(f, "INCOMPATIBLE_DURABLE_FORMAT: {incompatibility}")
611 }
612 FlowError::BackupRejected(problem) => write!(f, "BACKUP_REJECTED: {problem}"),
613 FlowError::Durability(failure) => write!(f, "{failure}"),
614 }
615 }
616}
617
618impl std::error::Error for FlowError {}
619
620impl From<std::io::Error> for FlowError {
621 fn from(value: std::io::Error) -> Self {
622 FlowError::Io(value)
623 }
624}
625
626impl From<serde_json::Error> for FlowError {
627 fn from(value: serde_json::Error) -> Self {
628 FlowError::Serde(value)
629 }
630}
631
632impl From<String> for FlowError {
633 fn from(value: String) -> Self {
634 FlowError::CapabilityError(value)
635 }
636}
637
638#[derive(Clone)]
639pub struct FeltDb {
640 inner: Arc<Mutex<Inner>>,
641 event_tx: broadcast::Sender<ChangeEvent>,
642 _ownership: Arc<File>,
643}
644
645struct OpenDatabase {
646 inner: Weak<Mutex<Inner>>,
647 event_tx: broadcast::Sender<ChangeEvent>,
648 ownership: Weak<File>,
649}
650
651static OPEN_DATABASES: OnceLock<Mutex<HashMap<PathBuf, OpenDatabase>>> = OnceLock::new();
652
653#[derive(Default)]
654#[allow(dead_code)] struct Inner {
656 path: PathBuf,
657 rows: HashMap<String, BTreeMap<String, StoredRow>>,
658 key_counters: HashMap<String, u64>,
659 query_count_by_type: HashMap<String, usize>,
660 adaptive_indexes: HashSet<String>,
661 instance_id: String,
662 sequence: u64,
663 authority_revision: u64,
666 sync_state: SyncState,
667 change_log: ChangeLog,
668 execution_queue: ExecutionQueue,
669 trigger_registry: TriggerRegistry,
670 cron_scheduler: CronScheduler,
671 capability_registry: CapabilityRegistry,
672 peer_registry: PeerRegistry,
673 content_store: ContentAddressableStore,
674 capability_locations: CapabilityLocationRegistry,
675 bootstrapped: bool,
676 applied_transactions: HashSet<String>,
677 transaction_payload_hashes: HashMap<String, String>,
678 transaction_revisions: HashMap<String, (u64, u64)>,
679 transaction_attributions: VecDeque<TransactionAttribution>,
692 transaction_history: Vec<TransactionAttribution>,
696 collection_cardinality: HashMap<String, u64>,
697 equality_index: EqualityIndex,
702 log_recovery: LogRecovery,
707 durable_format: DurableFormat,
710 durability_mode: DurabilityMode,
712 writes_since_barrier: u32,
714 write_fault: Option<String>,
719 log_bytes_after_rewrite: u64,
724 compaction_policy: CompactionPolicy,
726 pruned_since_rewrite: usize,
728 revision_heads: HashMap<String, (state_model::StateId, u64)>,
735 collection_versions: HashMap<String, u64>,
744 collection_clock: u64,
745}
746
747struct StateGuard<'a> {
762 inner: std::sync::MutexGuard<'a, Inner>,
763 held_since: Option<std::time::Instant>,
764}
765
766impl std::ops::Deref for StateGuard<'_> {
767 type Target = Inner;
768 fn deref(&self) -> &Inner {
769 &self.inner
770 }
771}
772
773impl std::ops::DerefMut for StateGuard<'_> {
774 fn deref_mut(&mut self) -> &mut Inner {
775 &mut self.inner
776 }
777}
778
779impl Drop for StateGuard<'_> {
780 fn drop(&mut self) {
781 if let Some(since) = self.held_since {
782 workload_diagnostics::record(
783 workload_diagnostics::Phase::StateLockHold,
784 since.elapsed().as_nanos().min(u64::MAX as u128) as u64,
785 );
786 }
787 }
788}
789
790impl Inner {
806 fn put_row(&mut self, row: StoredRow) -> bool {
811 let capability = row.capability.clone();
812 let key = row.key.clone();
813 self.bump_collection_version(&capability);
814 let indexed = self.equality_index.indexes_collection(&capability);
815 let mutation = workload_diagnostics::span(workload_diagnostics::Phase::RecordMutation);
816 let bucket = self.rows.entry(capability.clone()).or_default();
817 let previous = bucket.insert(key.clone(), row);
818 drop(mutation);
819 if previous.is_none() {
820 *self
821 .collection_cardinality
822 .entry(capability.clone())
823 .or_default() += 1;
824 }
825 if !indexed {
826 return previous.is_none();
827 }
828 let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
832 if let Some(previous) = &previous {
836 self.equality_index
837 .remove_record(&capability, &key, &previous.value);
838 }
839 let Inner {
842 rows,
843 equality_index,
844 ..
845 } = self;
846 if let Some(current) = rows.get(&capability).and_then(|rows| rows.get(&key)) {
847 equality_index.insert_record(&capability, &key, ¤t.value);
848 }
849 previous.is_none()
850 }
851
852 fn remove_row(&mut self, capability: &str, key: &str) -> bool {
854 let mutation = workload_diagnostics::span(workload_diagnostics::Phase::RecordMutation);
855 let Some(removed) = self
856 .rows
857 .get_mut(capability)
858 .and_then(|bucket| bucket.remove(key))
859 else {
860 return false;
861 };
862 drop(mutation);
863 self.bump_collection_version(capability);
864 let count = self
865 .collection_cardinality
866 .entry(capability.to_string())
867 .or_default();
868 *count = count.saturating_sub(1);
869 let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
870 self.equality_index
871 .remove_record(capability, key, &removed.value);
872 true
873 }
874
875 fn bump_collection_version(&mut self, capability: &str) {
877 self.collection_clock += 1;
878 self.collection_versions
879 .insert(capability.to_string(), self.collection_clock);
880 }
881
882 fn clear_rows(&mut self) {
884 let cleared: Vec<String> = self.rows.keys().cloned().collect();
885 for capability in cleared {
886 self.bump_collection_version(&capability);
887 }
888 self.rows.clear();
889 self.collection_cardinality.clear();
890 self.equality_index.clear();
891 }
892}
893
894#[derive(Debug, Clone, Serialize, Deserialize)]
895pub struct AtomicMutation {
896 pub capability: String,
897 pub key: String,
898 pub value: Option<Value>,
899}
900#[derive(Debug, Clone, Serialize, Deserialize)]
901pub struct AtomicPrecondition {
902 pub capability: String,
903 pub key: String,
904 pub expected_version: Option<u64>,
905}
906
907#[derive(Debug, Clone, Default, Serialize, Deserialize)]
925pub struct RecordPrecondition {
926 pub capability: String,
927 pub key: String,
928 #[serde(default)]
930 pub require_absent: bool,
931 #[serde(default)]
933 pub expected_version: Option<u64>,
934 #[serde(default)]
936 pub expected_epoch: Option<u64>,
937 #[serde(default)]
939 pub expected_lease_id: Option<String>,
940}
941
942impl RecordPrecondition {
943 pub fn is_empty(&self) -> bool {
947 !self.require_absent
948 && self.expected_version.is_none()
949 && self.expected_epoch.is_none()
950 && self.expected_lease_id.is_none()
951 }
952}
953
954#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
960pub struct ReadSet {
961 #[serde(default)]
962 pub records: Vec<ReadRecord>,
963 #[serde(default)]
964 pub predicates: Vec<ReadPredicate>,
965}
966
967#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
968pub struct ReadRecord {
969 pub collection: String,
970 pub id: String,
971}
972
973#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
974pub struct ReadPredicate {
975 pub collection: String,
976 #[serde(rename = "where")]
977 pub conditions: Vec<ReadCondition>,
978}
979
980#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
981pub struct ReadCondition {
982 pub field: String,
983 pub eq: Value,
984}
985
986#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
992pub struct ReadBasis {
993 pub reads: ReadSet,
994 pub token: String,
995}
996
997pub(crate) const COLLECTION_FENCE_KEY: &str = "collection-version";
999
1000pub(crate) struct ConsistentRows {
1003 pub(crate) view: HashMap<String, BTreeMap<String, StoredRow>>,
1004 pub(crate) versions: HashMap<String, u64>,
1005}
1006
1007pub const READ_BASIS_PREFIX: &str = "fr1:";
1009pub const READ_SET_LIMIT: usize = 1_000;
1011
1012impl ReadSet {
1013 pub fn normalised(&self) -> Result<ReadSet> {
1016 let valid = |segment: &str| {
1017 !segment.is_empty()
1018 && segment.len() <= 128
1019 && segment
1020 .bytes()
1021 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
1022 };
1023 let mut records: BTreeMap<(String, String), ReadRecord> = BTreeMap::new();
1024 for record in &self.records {
1025 if !valid(&record.collection) || !valid(&record.id) {
1026 return Err(FlowError::CapabilityError(format!(
1027 "INVALID_READ_SET:invalid read {}:{}",
1028 record.collection, record.id
1029 )));
1030 }
1031 records.insert(
1032 (record.collection.clone(), record.id.clone()),
1033 record.clone(),
1034 );
1035 }
1036 let mut predicates: BTreeMap<(String, String), ReadPredicate> = BTreeMap::new();
1037 for predicate in &self.predicates {
1038 if !valid(&predicate.collection) || predicate.conditions.is_empty() {
1039 return Err(FlowError::CapabilityError(format!(
1040 "INVALID_READ_SET:a predicate over {} must name at least one field",
1041 predicate.collection
1042 )));
1043 }
1044 let mut conditions = predicate.conditions.clone();
1045 if conditions
1046 .iter()
1047 .any(|condition| condition.field.is_empty())
1048 {
1049 return Err(FlowError::CapabilityError(
1050 "INVALID_READ_SET:a predicate condition requires a field".into(),
1051 ));
1052 }
1053 conditions.sort_by(|left, right| left.field.cmp(&right.field));
1054 let identity = serde_json::to_string(&conditions).unwrap_or_default();
1055 predicates.insert(
1056 (predicate.collection.clone(), identity),
1057 ReadPredicate {
1058 collection: predicate.collection.clone(),
1059 conditions,
1060 },
1061 );
1062 }
1063 if records.is_empty() && predicates.is_empty() {
1064 return Err(FlowError::CapabilityError(
1065 "INVALID_READ_SET:a read set that names nothing fences nothing".into(),
1066 ));
1067 }
1068 if records.len() + predicates.len() > READ_SET_LIMIT {
1069 return Err(FlowError::CapabilityError(format!(
1070 "INVALID_READ_SET:a read set may name at most {READ_SET_LIMIT} records and predicates"
1071 )));
1072 }
1073 Ok(ReadSet {
1074 records: records.into_values().collect(),
1075 predicates: predicates.into_values().collect(),
1076 })
1077 }
1078}
1079
1080#[derive(Debug, Clone, PartialEq, Eq)]
1085pub enum ReadLayout {
1086 Direct,
1088 Namespace(String),
1090}
1091
1092impl ReadLayout {
1093 fn capability(&self, collection: &str) -> String {
1094 match self {
1095 Self::Direct => collection.to_string(),
1096 Self::Namespace(namespace) => format!("{namespace}:{collection}"),
1097 }
1098 }
1099
1100 fn key(&self, collection: &str, id: &str) -> String {
1101 match self {
1102 Self::Direct => format!("{collection}:{id}"),
1103 Self::Namespace(_) => id.to_string(),
1104 }
1105 }
1106
1107 fn scope_tag(&self) -> String {
1110 match self {
1111 Self::Direct => "d".into(),
1112 Self::Namespace(namespace) => {
1113 let digest = format!(
1114 "{:x}",
1115 Sha256::digest(format!("namespace\0{namespace}").as_bytes())
1116 );
1117 format!("n{}", &digest[..16])
1118 }
1119 }
1120 }
1121
1122 fn token_prefix(&self) -> String {
1123 format!("{READ_BASIS_PREFIX}{}:", self.scope_tag())
1124 }
1125}
1126
1127fn read_basis_token(
1131 rows: &HashMap<String, BTreeMap<String, StoredRow>>,
1132 reads: &ReadSet,
1133 layout: &ReadLayout,
1134) -> String {
1135 let live = |capability: &str, key: &str| {
1136 rows.get(capability)
1137 .and_then(|bucket| bucket.get(key))
1138 .filter(|row| !row.deleted)
1139 .map(|row| row.value.clone())
1140 };
1141 let records: Vec<Value> = reads
1142 .records
1143 .iter()
1144 .map(|record| {
1145 let value = live(
1146 &layout.capability(&record.collection),
1147 &layout.key(&record.collection, &record.id),
1148 );
1149 serde_json::json!([record.collection, record.id, value])
1150 })
1151 .collect();
1152 let predicates: Vec<Value> = reads
1153 .predicates
1154 .iter()
1155 .map(|predicate| {
1156 let matching: Vec<Value> = rows
1157 .get(&layout.capability(&predicate.collection))
1158 .map(|bucket| {
1159 bucket
1160 .iter()
1161 .filter(|(_, row)| !row.deleted)
1162 .filter(|(_, row)| {
1163 predicate.conditions.iter().all(|condition| {
1164 row.value.get(&condition.field) == Some(&condition.eq)
1165 })
1166 })
1167 .map(|(key, row)| serde_json::json!([key, row.value]))
1168 .collect()
1169 })
1170 .unwrap_or_default();
1171 serde_json::json!([predicate.collection, predicate.conditions, matching])
1172 })
1173 .collect();
1174 let observed = serde_json::json!({ "records": records, "predicates": predicates });
1175 format!(
1176 "{}{:x}",
1177 layout.token_prefix(),
1178 Sha256::digest(serde_json::to_vec(&observed).unwrap_or_default())
1179 )
1180}
1181
1182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1188#[serde(rename_all = "camelCase", tag = "predicate")]
1189pub enum PreconditionFailure {
1190 Present { collection: String, key: String },
1192 Missing { collection: String, key: String },
1194 Version {
1195 collection: String,
1196 key: String,
1197 expected: u64,
1198 actual: u64,
1199 },
1200 Epoch {
1201 collection: String,
1202 key: String,
1203 expected: u64,
1204 actual: u64,
1205 },
1206 Lease {
1207 collection: String,
1208 key: String,
1209 expected: String,
1210 actual: Option<String>,
1211 },
1212 Fence {
1217 collection: String,
1218 key: String,
1219 #[serde(skip_serializing, default)]
1220 expected: String,
1221 #[serde(skip_serializing, default)]
1222 actual: String,
1223 },
1224}
1225
1226impl PreconditionFailure {
1227 pub fn collection(&self) -> &str {
1228 match self {
1229 Self::Present { collection, .. }
1230 | Self::Missing { collection, .. }
1231 | Self::Version { collection, .. }
1232 | Self::Epoch { collection, .. }
1233 | Self::Lease { collection, .. }
1234 | Self::Fence { collection, .. } => collection,
1235 }
1236 }
1237 pub fn key(&self) -> &str {
1239 match self {
1240 Self::Present { key, .. }
1241 | Self::Missing { key, .. }
1242 | Self::Version { key, .. }
1243 | Self::Epoch { key, .. }
1244 | Self::Lease { key, .. }
1245 | Self::Fence { key, .. } => key,
1246 }
1247 }
1248}
1249
1250impl PreconditionFailure {
1251 pub fn public_json(&self, layout: &ReadLayout) -> Value {
1261 let mut value = serde_json::to_value(self).unwrap_or(Value::Null);
1262 if matches!(self, Self::Fence { .. }) {
1263 return value;
1264 }
1265 let (collection, id) = match layout {
1266 ReadLayout::Direct => {
1267 let collection = self.collection().to_string();
1268 let id = self
1269 .key()
1270 .strip_prefix(&format!("{collection}:"))
1271 .unwrap_or(self.key())
1272 .to_string();
1273 (collection, id)
1274 }
1275 ReadLayout::Namespace(namespace) => (
1276 self.collection()
1277 .strip_prefix(&format!("{namespace}:"))
1278 .unwrap_or(self.collection())
1279 .to_string(),
1280 self.key().to_string(),
1281 ),
1282 };
1283 if let Some(object) = value.as_object_mut() {
1284 object.insert("key".into(), Value::String(format!("{collection}:{id}")));
1285 object.insert("collection".into(), Value::String(collection));
1286 object.insert("id".into(), Value::String(id));
1287 }
1288 value
1289 }
1290}
1291
1292impl Display for PreconditionFailure {
1293 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1294 match self {
1295 Self::Present { collection, key } => {
1296 write!(f, "{collection}/{key} was required to be absent but exists")
1297 }
1298 Self::Missing { collection, key } => write!(f, "{collection}/{key} does not exist"),
1299 Self::Version {
1300 collection,
1301 key,
1302 expected,
1303 actual,
1304 } => write!(
1305 f,
1306 "{collection}/{key} is at version {actual}, expected {expected}"
1307 ),
1308 Self::Epoch {
1309 collection,
1310 key,
1311 expected,
1312 actual,
1313 } => write!(
1314 f,
1315 "{collection}/{key} is at epoch {actual}, expected {expected}"
1316 ),
1317 Self::Lease {
1318 collection,
1319 key,
1320 expected,
1321 actual,
1322 } => write!(
1323 f,
1324 "{collection}/{key} holds lease {actual:?}, expected {expected}"
1325 ),
1326 Self::Fence { collection, .. } => write!(
1327 f,
1328 "state read by this transaction has changed (read basis over {collection})"
1329 ),
1330 }
1331 }
1332}
1333#[derive(Debug, Clone, Serialize, Deserialize)]
1334pub struct AtomicCommit {
1335 pub transaction_id: String,
1336 pub base_revision: u64,
1337 pub commit_revision: u64,
1338 pub status: TransactionStatus,
1339 pub state_before: u64,
1340 pub state_after: u64,
1341 pub rows: Vec<StoredRow>,
1342 pub duplicate: bool,
1343 #[serde(default, skip_serializing_if = "Option::is_none")]
1347 pub payload_binding: Option<PayloadBinding>,
1348}
1349
1350#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1352#[serde(rename_all = "snake_case")]
1353pub enum PayloadBinding {
1354 Verified,
1357 LegacyUnverified,
1360}
1361
1362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1363#[serde(rename_all = "snake_case")]
1364pub enum TransactionStatus {
1365 Committed,
1366}
1367
1368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1369pub enum JsonCasResult {
1370 Updated {
1371 current_version: u64,
1372 current_epoch: u64,
1373 value: Value,
1374 },
1375 VersionConflict {
1376 current_version: u64,
1377 current_epoch: u64,
1378 },
1379 AuthorityConflict {
1380 current_version: u64,
1381 current_epoch: u64,
1382 },
1383 LeaseConflict {
1384 current_version: u64,
1385 current_epoch: u64,
1386 },
1387 ReplicationConflict {
1388 current_version: u64,
1389 current_epoch: u64,
1390 },
1391 NotFound,
1392}
1393const TRANSACTION_ATTRIBUTION_WINDOW: usize = 512;
1399
1400pub const STANDALONE_OPERATION_WINDOW: usize = 4096;
1404
1405const TRANSACTION_ATTRIBUTION_KEYS: usize = 32;
1411
1412#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1419pub struct TransactionAttribution {
1420 pub transaction_id: String,
1421 #[serde(default)]
1424 pub operation_ids: Vec<String>,
1425 pub subject: Option<String>,
1427 pub tenant_id: Option<String>,
1428 pub application_id: Option<String>,
1429 pub application_revision: Option<String>,
1431 pub base_revision: u64,
1432 pub commit_revision: u64,
1433 pub state_before: u64,
1434 pub state_after: u64,
1435 pub keys: Vec<String>,
1438 pub operations: usize,
1441 #[serde(default)]
1442 pub principal_id: Option<String>,
1443 #[serde(default)]
1444 pub agent_id: Option<String>,
1445 #[serde(default)]
1446 pub session_id: Option<String>,
1447 #[serde(default)]
1448 pub delegator_id: Option<String>,
1449 #[serde(default)]
1450 pub work_id: Option<String>,
1451 pub unix_ms: u128,
1453 #[serde(default)]
1457 pub audit: Option<Value>,
1458}
1459
1460impl TransactionAttribution {
1461 fn from_record(
1463 transaction_id: &str,
1464 audit: Option<&Value>,
1465 base_revision: u64,
1466 commit_revision: u64,
1467 state_before: u64,
1468 state_after: u64,
1469 rows: &[StoredRow],
1470 unix_ms: u128,
1471 ) -> Self {
1472 let text = |field: &str| {
1473 audit
1474 .and_then(|value| value.get(field))
1475 .and_then(Value::as_str)
1476 .map(str::to_string)
1477 };
1478 TransactionAttribution {
1479 transaction_id: transaction_id.to_string(),
1480 operation_ids: rows
1481 .iter()
1482 .filter_map(|row| row.operation.as_ref())
1483 .map(|operation| {
1484 format!("operation:{}:{}", operation.instance_id, operation.sequence)
1485 })
1486 .collect(),
1487 subject: text("subject"),
1488 tenant_id: text("tenant"),
1489 application_id: text("application"),
1490 application_revision: text("revision"),
1491 base_revision,
1492 commit_revision,
1493 state_before,
1494 state_after,
1495 keys: rows
1496 .iter()
1497 .take(TRANSACTION_ATTRIBUTION_KEYS)
1498 .map(|row| format!("{}:{}", row.capability, row.key))
1499 .collect(),
1500 operations: rows.len(),
1501 principal_id: text("principal_id"),
1502 agent_id: text("agent_id"),
1503 session_id: text("session_id"),
1504 delegator_id: text("delegator_id"),
1505 work_id: text("work_id").or_else(|| text("workload_id")),
1506 unix_ms,
1507 audit: audit.cloned(),
1508 }
1509 }
1510
1511 pub fn touched(&self, capability: &str, key: &str) -> bool {
1513 let target = format!("{capability}:{key}");
1514 self.keys.iter().any(|held| *held == target)
1515 }
1516}
1517
1518fn normalize_transaction_attribution(
1522 mut attribution: TransactionAttribution,
1523) -> TransactionAttribution {
1524 if attribution.operation_ids.is_empty() {
1525 if attribution.transaction_id.starts_with("operation:") {
1526 let operation_id = attribution.transaction_id.clone();
1527 attribution.transaction_id = operation_id.replacen("operation:", "transaction:", 1);
1528 attribution.operation_ids.push(operation_id);
1529 } else {
1530 attribution.operation_ids = (1..=attribution.operations)
1531 .map(|index| {
1532 format!(
1533 "operation:legacy-transaction:{}:{index}",
1534 attribution.transaction_id
1535 )
1536 })
1537 .collect();
1538 }
1539 }
1540 attribution
1541}
1542
1543fn remember_attribution(inner: &mut Inner, attribution: TransactionAttribution) {
1545 inner.transaction_history.push(attribution.clone());
1546 if inner.transaction_attributions.len() >= TRANSACTION_ATTRIBUTION_WINDOW {
1547 inner.transaction_attributions.pop_front();
1548 }
1549 inner.transaction_attributions.push_back(attribution);
1550}
1551
1552fn remember_operation_attribution(
1553 inner: &mut Inner,
1554 row: &StoredRow,
1555 base_revision: u64,
1556 commit_revision: u64,
1557) {
1558 let Some(operation) = row.operation.as_ref() else {
1559 return;
1560 };
1561 remember_attribution(
1562 inner,
1563 TransactionAttribution {
1564 transaction_id: format!(
1565 "transaction:{}:{}",
1566 operation.instance_id, operation.sequence
1567 ),
1568 operation_ids: vec![format!(
1569 "operation:{}:{}",
1570 operation.instance_id, operation.sequence
1571 )],
1572 subject: None,
1573 tenant_id: None,
1574 application_id: None,
1575 application_revision: operation
1576 .revision
1577 .as_ref()
1578 .map(|revision| revision.id.clone()),
1579 base_revision,
1580 commit_revision,
1581 state_before: operation.sequence.saturating_sub(1),
1582 state_after: operation.sequence,
1583 keys: vec![format!("{}:{}", row.capability, row.key)],
1584 operations: 1,
1585 principal_id: None,
1586 agent_id: None,
1587 session_id: None,
1588 delegator_id: None,
1589 work_id: None,
1590 unix_ms: row.unix_ms,
1591 audit: None,
1592 },
1593 );
1594}
1595
1596#[derive(Debug, Clone, Serialize, Deserialize)]
1597struct TransactionLogRecord {
1598 record_type: String,
1599 transaction_id: String,
1600 #[serde(default)]
1601 payload_hash: Option<String>,
1602 state_before: u64,
1603 state_after: u64,
1604 #[serde(default)]
1605 base_revision: u64,
1606 #[serde(default)]
1607 commit_revision: u64,
1608 rows: Vec<StoredRow>,
1609 #[serde(default)]
1610 audit: Option<Value>,
1611}
1612
1613#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1617pub struct ChangeEvent {
1618 pub capability: String,
1620 pub key: String,
1622 pub rust_type: String,
1624 pub unix_ms: u128,
1626}
1627
1628#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1629pub struct StoredRow {
1630 pub capability: String,
1631 pub key: String,
1632 pub rust_type: String,
1633 pub value: Value,
1634 pub unix_ms: u128,
1635 #[serde(default)]
1637 pub content_hash: Option<String>,
1638 #[serde(default)]
1640 pub flow_ref: Option<FlowRef>,
1641 #[serde(default)]
1643 pub deleted: bool,
1644 #[serde(default)]
1646 pub operation: Option<Operation>,
1647}
1648
1649#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1659pub struct CompactionPolicy {
1660 pub rewrite_after_pruned: usize,
1663}
1664
1665impl Default for CompactionPolicy {
1666 fn default() -> Self {
1667 CompactionPolicy {
1671 rewrite_after_pruned: 1000,
1672 }
1673 }
1674}
1675
1676#[derive(Debug, Clone, PartialEq, Eq)]
1678pub enum CompactionOutcome {
1679 Idle,
1681 Deferred {
1684 pruned: usize,
1685 pending_since_rewrite: usize,
1686 },
1687 Rewritten { pruned: usize },
1689}
1690
1691impl CompactionOutcome {
1692 pub fn pruned(&self) -> usize {
1694 match self {
1695 CompactionOutcome::Idle => 0,
1696 CompactionOutcome::Deferred { pruned, .. }
1697 | CompactionOutcome::Rewritten { pruned } => *pruned,
1698 }
1699 }
1700
1701 pub fn rewrote_log(&self) -> bool {
1703 matches!(self, CompactionOutcome::Rewritten { .. })
1704 }
1705}
1706
1707#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1714pub enum DurabilityMode {
1715 Flushed,
1720 Synced,
1727 Grouped { every: u32 },
1732}
1733
1734impl Default for DurabilityMode {
1735 fn default() -> Self {
1736 DurabilityMode::Flushed
1737 }
1738}
1739
1740impl DurabilityMode {
1741 pub fn guarantee(&self) -> &'static str {
1744 match self {
1745 DurabilityMode::Flushed => {
1746 "the record has left the process; it survives a process crash. Stable-storage durability is not established."
1747 }
1748 DurabilityMode::Synced => {
1749 "a stable-storage barrier was issued for this record. Whether it reached the device depends on the storage stack, which this database cannot observe."
1750 }
1751 DurabilityMode::Grouped { .. } => {
1752 "the record has left the process. A stable-storage barrier is issued periodically, so an acknowledged write may not yet have been through one."
1753 }
1754 }
1755 }
1756
1757 pub fn unbarriered_window(&self) -> Option<u32> {
1761 match self {
1762 DurabilityMode::Flushed => None,
1763 DurabilityMode::Synced => Some(0),
1764 DurabilityMode::Grouped { every } => Some(every.saturating_sub(1)),
1765 }
1766 }
1767}
1768
1769#[derive(Debug, Clone, PartialEq, Eq)]
1776pub enum StorageHealth {
1777 Clean,
1779 RecoveredIncompleteWrite {
1786 byte_offset: u64,
1787 discarded_bytes: usize,
1788 },
1789}
1790
1791impl StorageHealth {
1792 pub fn is_clean(&self) -> bool {
1794 matches!(self, StorageHealth::Clean)
1795 }
1796
1797 pub fn label(&self) -> &'static str {
1799 match self {
1800 StorageHealth::Clean => "clean",
1801 StorageHealth::RecoveredIncompleteWrite { .. } => "recovered-incomplete-write",
1802 }
1803 }
1804}
1805
1806impl Display for StorageHealth {
1807 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1808 match self {
1809 StorageHealth::Clean => write!(f, "durable log replayed cleanly"),
1810 StorageHealth::RecoveredIncompleteWrite {
1811 byte_offset,
1812 discarded_bytes,
1813 } => write!(
1814 f,
1815 "durable log ended in an incomplete append: {discarded_bytes} bytes at offset \
1816 {byte_offset} were discarded, so the last write did not land"
1817 ),
1818 }
1819 }
1820}
1821
1822#[derive(Debug, Clone, PartialEq, Eq)]
1829pub struct DatabaseHealth {
1830 pub durable_format: DurableFormat,
1835 pub recovery: LogRecovery,
1837 pub storage: StorageHealth,
1839 pub writes: WriteAvailability,
1841}
1842
1843#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1845#[serde(tag = "state", rename_all = "kebab-case")]
1846pub enum WriteAvailability {
1847 Available,
1849 RecoveryRequired { reason: String },
1854}
1855
1856impl WriteAvailability {
1857 pub fn label(&self) -> &'static str {
1858 match self {
1859 WriteAvailability::Available => "available",
1860 WriteAvailability::RecoveryRequired { .. } => "recovery-required",
1861 }
1862 }
1863}
1864
1865impl DatabaseHealth {
1866 pub fn is_nominal(&self) -> bool {
1872 self.storage.is_clean() && self.writes == WriteAvailability::Available
1873 }
1874}
1875
1876pub const BACKUP_VERSION: u32 = 1;
1882
1883const BACKUP_RECORD_TYPE: &str = "feltdb.backup.v1";
1884
1885#[derive(Debug, Clone, Serialize, Deserialize)]
1887struct BackupHeader {
1888 record_type: String,
1889 backup_version: u32,
1890 format_version: u32,
1892 created_unix_ms: u64,
1893 row_count: usize,
1894 observed_versions: HashMap<String, u64>,
1895 content_digest: String,
1897 meaning_digest: String,
1899}
1900
1901#[derive(Debug, Clone, PartialEq, Eq)]
1903pub struct BackupProblem {
1904 pub reason: String,
1906 pub line_number: Option<usize>,
1908 pub action: String,
1910}
1911
1912impl Display for BackupProblem {
1913 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1914 match self.line_number {
1915 Some(line) => write!(f, "record {line}: {}. {}", self.reason, self.action),
1916 None => write!(f, "{}. {}", self.reason, self.action),
1917 }
1918 }
1919}
1920
1921#[derive(Debug, Clone, PartialEq, Eq)]
1938pub struct BackupVerification {
1939 pub backup_version: u32,
1940 pub format_version: u32,
1941 pub created_unix_ms: u64,
1942 pub rows: usize,
1943 pub content_digest: String,
1944 pub meaning_digest: String,
1945}
1946
1947fn backup_problem(reason: &str, line_number: Option<usize>, action: &str) -> FlowError {
1948 FlowError::BackupRejected(Box::new(BackupProblem {
1949 reason: reason.to_string(),
1950 line_number,
1951 action: action.to_string(),
1952 }))
1953}
1954
1955fn backup_content_digest(rows: &[StoredRow]) -> Result<String> {
1957 let mut hasher = Sha256::new();
1958 for row in rows {
1959 hasher.update(serde_json::to_vec(row)?);
1960 hasher.update(b"\n");
1961 }
1962 Ok(format!("{:x}", hasher.finalize()))
1963}
1964
1965fn backup_meaning_digest(rows: &[StoredRow]) -> Result<String> {
1971 let mut meaningful: Vec<(&str, &str, String)> = rows
1972 .iter()
1973 .filter(|row| !row.deleted)
1974 .map(|row| {
1975 (
1976 row.capability.as_str(),
1977 row.key.as_str(),
1978 row.value.to_string(),
1979 )
1980 })
1981 .collect();
1982 meaningful.sort();
1983
1984 let mut hasher = Sha256::new();
1985 hasher.update(b"feltdb.backup.meaning.v1");
1986 for (capability, key, value) in meaningful {
1987 hasher.update(capability.as_bytes());
1988 hasher.update([0u8]);
1989 hasher.update(key.as_bytes());
1990 hasher.update([0u8]);
1991 hasher.update(value.as_bytes());
1992 hasher.update([0u8]);
1993 }
1994 Ok(format!("{:x}", hasher.finalize()))
1995}
1996
1997pub fn verify_backup(path: &Path) -> Result<BackupVerification> {
2003 let text = std::fs::read_to_string(path)?;
2004 let mut lines = text.lines();
2005
2006 let Some(header_line) = lines.next() else {
2007 return Err(backup_problem(
2008 "the artifact is empty",
2009 None,
2010 "Restore from a different backup.",
2011 ));
2012 };
2013 let header: BackupHeader = serde_json::from_str(header_line).map_err(|error| {
2014 backup_problem(
2015 &format!("the header is not a FeltDB backup header: {error}"),
2016 Some(1),
2017 "Check that this file is a FeltDB backup and was not truncated at the start.",
2018 )
2019 })?;
2020 if header.record_type != BACKUP_RECORD_TYPE {
2021 return Err(backup_problem(
2022 &format!("unexpected record type {}", header.record_type),
2023 Some(1),
2024 "Check that this file is a FeltDB backup.",
2025 ));
2026 }
2027 if header.backup_version != BACKUP_VERSION {
2028 return Err(backup_problem(
2029 &format!(
2030 "backup format version {} is not the version this build reads ({BACKUP_VERSION})",
2031 header.backup_version
2032 ),
2033 Some(1),
2034 "Restore with a FeltDB build that reads this backup version.",
2035 ));
2036 }
2037 if header.format_version != DURABLE_FORMAT_VERSION {
2038 return Err(backup_problem(
2039 &format!(
2040 "the rows are in durable format version {} and this build requires version {DURABLE_FORMAT_VERSION}",
2041 header.format_version
2042 ),
2043 Some(1),
2044 "Restore with a FeltDB build matching the format the backup was taken in.",
2045 ));
2046 }
2047
2048 let mut rows = Vec::with_capacity(header.row_count);
2049 for (index, line) in lines.enumerate() {
2050 if line.trim().is_empty() {
2051 continue;
2052 }
2053 let row: StoredRow = serde_json::from_str(line).map_err(|error| {
2054 backup_problem(
2055 &format!("not a durable record: {error}"),
2056 Some(index + 2),
2057 "The backup is damaged. Restore from another copy.",
2058 )
2059 })?;
2060 rows.push(row);
2061 }
2062
2063 if rows.len() != header.row_count {
2064 return Err(backup_problem(
2065 &format!(
2066 "the header declares {} records and the artifact holds {}",
2067 header.row_count,
2068 rows.len()
2069 ),
2070 None,
2071 "The backup is truncated or was appended to. Restore from another copy.",
2072 ));
2073 }
2074
2075 let content_digest = backup_content_digest(&rows)?;
2076 if content_digest != header.content_digest {
2077 return Err(backup_problem(
2078 "the records do not match the digest in the header",
2079 None,
2080 "The backup has been altered or corrupted. Restore from another copy.",
2081 ));
2082 }
2083
2084 let meaning_digest = backup_meaning_digest(&rows)?;
2085 if meaning_digest != header.meaning_digest {
2086 return Err(backup_problem(
2087 "the records do not mean what the header says they mean",
2088 None,
2089 "The backup has been altered. Restore from another copy.",
2090 ));
2091 }
2092
2093 Ok(BackupVerification {
2094 backup_version: header.backup_version,
2095 format_version: header.format_version,
2096 created_unix_ms: header.created_unix_ms,
2097 rows: rows.len(),
2098 content_digest,
2099 meaning_digest,
2100 })
2101}
2102
2103#[derive(Debug, Clone, PartialEq, Eq)]
2108pub struct LogCorruption {
2109 pub line_number: usize,
2111 pub byte_offset: u64,
2113 pub reason: String,
2115 pub excerpt: String,
2117}
2118
2119impl Display for LogCorruption {
2120 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2121 write!(
2122 f,
2123 "durable log record {} at byte {} is corrupt: {}. Excerpt: {}. \
2124 The database was not opened and was not modified.",
2125 self.line_number, self.byte_offset, self.reason, self.excerpt
2126 )
2127 }
2128}
2129
2130#[derive(Debug, Clone, PartialEq, Eq)]
2137pub enum LogRecovery {
2138 Clean,
2140 RecoveredTornTail {
2151 byte_offset: u64,
2153 discarded_bytes: usize,
2155 },
2156}
2157
2158impl Default for LogRecovery {
2159 fn default() -> Self {
2160 LogRecovery::Clean
2161 }
2162}
2163
2164impl Default for DurableFormat {
2165 fn default() -> Self {
2166 DurableFormat::Versioned(DURABLE_FORMAT_VERSION)
2167 }
2168}
2169
2170impl LogRecovery {
2171 pub fn is_clean(&self) -> bool {
2173 matches!(self, LogRecovery::Clean)
2174 }
2175}
2176
2177impl Display for LogRecovery {
2178 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2179 match self {
2180 LogRecovery::Clean => write!(f, "clean"),
2181 LogRecovery::RecoveredTornTail {
2182 byte_offset,
2183 discarded_bytes,
2184 } => write!(
2185 f,
2186 "recovered from an incomplete final append: {discarded_bytes} bytes at offset \
2187 {byte_offset} were not a complete record and were discarded"
2188 ),
2189 }
2190 }
2191}
2192
2193pub const DURABLE_FORMAT_VERSION: u32 = 2;
2203
2204const FORMAT_RECORD_TYPE: &str = "feltdb.format.v1";
2206
2207#[derive(Debug, Serialize, Deserialize)]
2208struct FormatRecord {
2209 record_type: String,
2210 format_version: u32,
2211}
2212
2213#[derive(Debug, Clone, PartialEq, Eq)]
2215pub enum DurableFormat {
2216 Versioned(u32),
2218 Unversioned,
2225}
2226
2227impl Display for DurableFormat {
2228 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2229 match self {
2230 DurableFormat::Versioned(version) => write!(f, "version {version}"),
2231 DurableFormat::Unversioned => write!(f, "unversioned (predates format versioning)"),
2232 }
2233 }
2234}
2235
2236#[derive(Debug, Clone, PartialEq, Eq)]
2238pub struct FormatIncompatibility {
2239 pub found: DurableFormat,
2241 pub required: u32,
2243 pub reason: String,
2245 pub action: String,
2247}
2248
2249impl Display for FormatIncompatibility {
2250 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2251 write!(
2252 f,
2253 "durable format {} is incompatible with this FeltDB, which requires \
2254 format version {}; {}. No data was read or modified. {}",
2255 self.found, self.required, self.reason, self.action
2256 )
2257 }
2258}
2259
2260#[derive(Debug, Clone, PartialEq, Eq)]
2262pub enum FormatCompatibility {
2263 Compatible(DurableFormat),
2265 Incompatible(FormatIncompatibility),
2267}
2268
2269impl FormatCompatibility {
2270 pub fn is_compatible(&self) -> bool {
2272 matches!(self, FormatCompatibility::Compatible(_))
2273 }
2274}
2275
2276#[derive(Debug, Clone, Serialize, Deserialize)]
2277pub struct DatabaseSnapshot {
2278 pub rows: Vec<StoredRow>,
2279 pub versions: HashMap<String, u64>,
2280 pub content_hash: String,
2281 #[serde(default)]
2293 pub format_version: u32,
2294}
2295
2296impl DatabaseSnapshot {
2297 fn compute_hash(rows: &[StoredRow], versions: &HashMap<String, u64>) -> Result<String> {
2298 let mut rows = rows.to_vec();
2299 rows.sort_by(|left, right| left.key.cmp(&right.key));
2300 let versions: std::collections::BTreeMap<_, _> = versions.iter().collect();
2301 let mut hasher = Sha256::new();
2302 hasher.update(serde_json::to_vec(&(rows, versions))?);
2303 Ok(format!("{:x}", hasher.finalize()))
2304 }
2305
2306 pub fn verify(&self) -> Result<()> {
2307 if self.content_hash != Self::compute_hash(&self.rows, &self.versions)? {
2308 return Err(FlowError::CapabilityError(
2309 "snapshot content hash mismatch".to_string(),
2310 ));
2311 }
2312 Ok(())
2313 }
2314}
2315
2316#[derive(Debug, Serialize, Deserialize)]
2317struct LogHeader {
2318 record_type: String,
2319 local_sequence: u64,
2320 observed_versions: HashMap<String, u64>,
2321 #[serde(default)]
2324 format_version: u32,
2325 #[serde(default)]
2328 transaction_history: Vec<TransactionAttribution>,
2329 #[serde(default)]
2333 transaction_payload_hashes: HashMap<String, String>,
2334 #[serde(default)]
2338 authority_revision: u64,
2339}
2340
2341#[derive(Serialize)]
2343struct LogHeaderRef<'a> {
2344 record_type: &'a str,
2345 local_sequence: u64,
2346 observed_versions: &'a HashMap<String, u64>,
2347 format_version: u32,
2348 transaction_history: &'a Vec<TransactionAttribution>,
2349 transaction_payload_hashes: &'a HashMap<String, String>,
2350 authority_revision: u64,
2351}
2352
2353#[derive(Debug, Default, Serialize, Deserialize)]
2354struct SyncMetadata {
2355 local_sequence: u64,
2356 observed_versions: HashMap<String, u64>,
2357 peer_acknowledgements: HashMap<String, HashMap<String, u64>>,
2358}
2359
2360pub fn open<P: AsRef<Path>>(path: P) -> Result<FeltDb> {
2361 FeltDb::open(path)
2362}
2363
2364const UNVERSIONED_CAPABILITIES: &[&str] = &["state", "_retention"];
2370
2371fn is_versioned(capability: &str) -> bool {
2373 !UNVERSIONED_CAPABILITIES.contains(&capability)
2374}
2375
2376fn rebuild_revision_heads(inner: &mut Inner) {
2382 let mut heads: HashMap<String, (state_model::StateId, u64)> = HashMap::new();
2383 if let Some(bucket) = inner.rows.get("state") {
2384 for row in bucket.values() {
2385 if row.deleted || !row.key.starts_with("state:revision:") {
2386 continue;
2387 }
2388 let Ok(revision) =
2389 serde_json::from_value::<state_model::StateRevision>(row.value.clone())
2390 else {
2391 continue;
2392 };
2393 let entry = heads
2394 .entry(revision.resource.clone())
2395 .or_insert_with(|| (revision.id.clone(), revision.sequence));
2396 if revision.sequence >= entry.1 {
2397 *entry = (revision.id.clone(), revision.sequence);
2398 }
2399 }
2400 }
2401 inner.revision_heads = heads;
2402}
2403
2404fn revision_row(revision: &state_model::StateRevision) -> Result<StoredRow> {
2422 Ok(StoredRow {
2423 capability: "state".to_string(),
2424 key: format!("state:revision:{}", revision.id.as_hex()),
2425 rust_type: "feltdb::state_model::StateRevision".to_string(),
2426 value: serde_json::to_value(revision)?,
2427 unix_ms: now_ms(),
2428 content_hash: None,
2429 flow_ref: None,
2430 deleted: false,
2431 operation: None,
2432 })
2433}
2434
2435fn commit_local_write_locked(
2440 inner: &mut Inner,
2441 row: &StoredRow,
2442 planned: Option<&state_model::StateRevision>,
2443 reserved: (u64, Option<HashMap<String, u64>>),
2444) -> Result<()> {
2445 let revision = revision_row_to_append(inner, planned)?;
2446 let mut lines = vec![row];
2447 lines.extend(revision.as_ref());
2448 if let Err(error) = append_rows_locked(inner, &lines) {
2449 inner.sequence = reserved.0;
2450 inner.sync_state.vector_clock = reserved.1;
2451 return Err(error);
2452 }
2453 if let Some(revision) = revision {
2454 inner.put_row(revision);
2455 }
2456 Ok(())
2457}
2458
2459fn revision_row_to_append(
2462 inner: &Inner,
2463 planned: Option<&state_model::StateRevision>,
2464) -> Result<Option<StoredRow>> {
2465 let Some(revision) = planned else {
2466 return Ok(None);
2467 };
2468 let row = revision_row(revision)?;
2469 let stored = inner
2470 .rows
2471 .get("state")
2472 .and_then(|bucket| bucket.get(&row.key))
2473 .is_some_and(|row| !row.deleted);
2474 Ok((!stored).then_some(row))
2475}
2476
2477fn plan_revision(inner: &Inner, resource: &str, content: String) -> state_model::StateRevision {
2485 let parent = inner.revision_heads.get(resource).cloned();
2486 let (parent_id, sequence) = match &parent {
2487 Some((id, sequence)) => (Some(id.clone()), sequence + 1),
2488 None => (None, 0),
2489 };
2490 state_model::StateRevision::at(
2491 resource.to_string(),
2492 content,
2493 parent_id,
2494 sequence,
2495 inner.instance_id.clone(),
2496 )
2497}
2498
2499fn provenance_of(revision: &state_model::StateRevision) -> operation::RevisionProvenance {
2501 operation::RevisionProvenance {
2502 resource: revision.resource.clone(),
2503 id: revision.id.as_hex().to_string(),
2504 parent_id: revision
2505 .parent_id
2506 .as_ref()
2507 .map(|id| id.as_hex().to_string()),
2508 sequence: revision.sequence,
2509 content_id: revision.content_id.as_hex().to_string(),
2510 authority: revision.authority.clone(),
2511 }
2512}
2513
2514fn persist_revision_locked(inner: &mut Inner, revision: state_model::StateRevision) -> Result<()> {
2521 let key = format!("state:revision:{}", revision.id.as_hex());
2522 let already = inner
2523 .rows
2524 .get("state")
2525 .and_then(|bucket| bucket.get(&key))
2526 .is_some_and(|row| !row.deleted);
2527
2528 let resource = revision.resource.clone();
2529 let id = revision.id.clone();
2530 let sequence = revision.sequence;
2531
2532 if !already {
2533 let row = revision_row(&revision)?;
2534 let _ = append_event_locked(inner, &row);
2540 inner.put_row(row);
2541 }
2542
2543 let advance = inner
2546 .revision_heads
2547 .get(&resource)
2548 .is_none_or(|(_, current)| sequence > *current);
2549 if advance {
2550 inner
2551 .revision_heads
2552 .insert(resource.clone(), (id, sequence));
2553 }
2554
2555 let _ = apply_retention_locked(inner, &resource);
2559 Ok(())
2560}
2561
2562fn reconstruct_revision(
2570 provenance: &operation::RevisionProvenance,
2571 value: &Value,
2572) -> Result<state_model::StateRevision> {
2573 let content = value.to_string();
2574 let revision = state_model::StateRevision::at(
2575 provenance.resource.clone(),
2576 content,
2577 provenance
2578 .parent_id
2579 .as_ref()
2580 .map(|id| state_model::StateId::from_hex(id.clone())),
2581 provenance.sequence,
2582 provenance.authority.clone(),
2583 );
2584 if revision.content_id.as_hex() != provenance.content_id {
2585 return Err(FlowError::CapabilityError(format!(
2586 "replicated revision {} does not match the value it arrived with",
2587 provenance.id
2588 )));
2589 }
2590 if revision.id.as_hex() != provenance.id {
2591 return Err(FlowError::CapabilityError(format!(
2592 "replicated revision identity {} does not follow from its own resource, parent and sequence",
2593 provenance.id
2594 )));
2595 }
2596 Ok(revision)
2597}
2598
2599fn apply_retention_locked(inner: &mut Inner, resource: &str) -> Result<()> {
2605 let retention_key = format!("_retention:{resource}");
2606 let Some(state_row) = inner
2607 .rows
2608 .get("_retention")
2609 .and_then(|bucket| bucket.get(&retention_key))
2610 else {
2611 return Ok(());
2612 };
2613 let Ok(mut state) = serde_json::from_value::<RetentionRecord>(state_row.value.clone()) else {
2614 return Ok(());
2615 };
2616 if state.policy.keep_last.is_none() {
2617 return Ok(());
2618 }
2619
2620 let mut history: Vec<(state_model::StateId, u64)> = Vec::new();
2621 if let Some(bucket) = inner.rows.get("state") {
2622 for row in bucket.values() {
2623 if row.deleted || !row.key.starts_with("state:revision:") {
2624 continue;
2625 }
2626 if let Ok(revision) =
2627 serde_json::from_value::<state_model::StateRevision>(row.value.clone())
2628 {
2629 if revision.resource == resource {
2630 history.push((revision.id, revision.sequence));
2631 }
2632 }
2633 }
2634 }
2635 history.sort_by_key(|(_, sequence)| *sequence);
2636
2637 let (expire, horizon) = state_model::revisions_to_expire(&history, state.policy.keep_last);
2638 if expire.is_empty() {
2639 return Ok(());
2640 }
2641 let tombstones: Vec<StoredRow> = expire
2642 .iter()
2643 .map(|id| StoredRow {
2644 capability: "state".to_string(),
2645 key: format!("state:revision:{}", id.as_hex()),
2646 rust_type: "feltdb::state_model::StateRevision".to_string(),
2647 value: Value::Null,
2648 unix_ms: now_ms(),
2649 content_hash: None,
2650 flow_ref: None,
2651 deleted: true,
2652 operation: None,
2653 })
2654 .collect();
2655 if let Some(horizon) = horizon {
2656 state.horizon = horizon;
2657 }
2658 let retention = StoredRow {
2659 capability: "_retention".to_string(),
2660 key: retention_key,
2661 rust_type: "feltdb::state_model::StateRevision".to_string(),
2662 value: serde_json::to_value(&state)?,
2663 unix_ms: now_ms(),
2664 content_hash: None,
2665 flow_ref: None,
2666 deleted: false,
2667 operation: None,
2668 };
2669 let mut lines: Vec<&StoredRow> = tombstones.iter().collect();
2672 lines.push(&retention);
2673 append_rows_locked(inner, &lines)?;
2674 if let Some(bucket) = inner.rows.get_mut("state") {
2675 for tombstone in &tombstones {
2676 bucket.remove(&tombstone.key);
2677 }
2678 }
2679 inner.put_row(retention);
2680 Ok(())
2681}
2682
2683#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2688struct RetentionRecord {
2689 policy: state_model::RetentionPolicy,
2690 horizon: u64,
2691}
2692
2693impl FeltDb {
2694 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
2695 let requested_path = path.as_ref();
2696 let parent = requested_path
2697 .parent()
2698 .filter(|parent| !parent.as_os_str().is_empty())
2699 .unwrap_or_else(|| Path::new("."));
2700 fs::create_dir_all(parent)?;
2701 let path = if requested_path.exists() {
2702 fs::canonicalize(requested_path)?
2703 } else {
2704 let file_name = requested_path.file_name().ok_or_else(|| {
2705 std::io::Error::new(
2706 std::io::ErrorKind::InvalidInput,
2707 "FeltDB database path has no file name",
2708 )
2709 })?;
2710 fs::canonicalize(parent)?.join(file_name)
2711 };
2712
2713 let databases = OPEN_DATABASES.get_or_init(|| Mutex::new(HashMap::new()));
2714 let mut databases = databases.lock().expect("open database registry poisoned");
2715 if let Some(open) = databases.get(&path) {
2716 if let (Some(inner), Some(ownership)) = (open.inner.upgrade(), open.ownership.upgrade())
2717 {
2718 return Ok(Self {
2719 inner,
2720 event_tx: open.event_tx.clone(),
2721 _ownership: ownership,
2722 });
2723 }
2724 }
2725 databases.remove(&path);
2726
2727 let mut ownership_path = path.as_os_str().to_os_string();
2728 ownership_path.push(".lock");
2729 let ownership = OpenOptions::new()
2730 .create(true)
2731 .read(true)
2732 .write(true)
2733 .open(PathBuf::from(ownership_path))?;
2734 ownership.try_lock().map_err(|error| match error {
2735 std::fs::TryLockError::WouldBlock => std::io::Error::new(
2736 std::io::ErrorKind::WouldBlock,
2737 format!(
2738 "FeltDB database {} is already owned by another process",
2739 path.display()
2740 ),
2741 ),
2742 std::fs::TryLockError::Error(error) => error,
2743 })?;
2744
2745 for leftover in ["compact.tmp", "snapshot.tmp"] {
2750 match fs::remove_file(sibling(&path, leftover)) {
2751 Ok(()) => {}
2752 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2753 Err(error) => return Err(error.into()),
2754 }
2755 }
2756
2757 let accepted_format = match inspect_durable_format(&path)? {
2762 FormatCompatibility::Incompatible(incompatibility) => {
2763 return Err(FlowError::IncompatibleFormat(Box::new(incompatibility)));
2764 }
2765 FormatCompatibility::Compatible(format) => format,
2766 };
2767
2768 if !path.exists() {
2769 let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
2770 write_json_line(
2773 &mut file,
2774 &FormatRecord {
2775 record_type: FORMAT_RECORD_TYPE.to_string(),
2776 format_version: DURABLE_FORMAT_VERSION,
2777 },
2778 )?;
2779 file.sync_data()?;
2782 sync_parent_directory(&path)?;
2783 }
2784
2785 let instance_id = format!("feltdb-{:x}", {
2787 let mut hasher = DefaultHasher::new();
2788 path.to_string_lossy().hash(&mut hasher);
2789 hasher.finish()
2790 });
2791
2792 let mut inner = Inner {
2793 path: path.clone(),
2794 instance_id: instance_id.clone(),
2795 durable_format: accepted_format,
2796 ..Inner::default()
2797 };
2798
2799 inner.sync_state = SyncState::new(instance_id);
2801 replay_log(&mut inner)?;
2802 load_sync_metadata(&mut inner)?;
2803 rebuild_revision_heads(&mut inner);
2804
2805 let (event_tx, _) = broadcast::channel(1024);
2806
2807 let database = Self {
2808 inner: Arc::new(Mutex::new(inner)),
2809 event_tx,
2810 _ownership: Arc::new(ownership),
2811 };
2812 databases.insert(
2813 path,
2814 OpenDatabase {
2815 inner: Arc::downgrade(&database.inner),
2816 event_tx: database.event_tx.clone(),
2817 ownership: Arc::downgrade(&database._ownership),
2818 },
2819 );
2820 Ok(database)
2821 }
2822
2823 pub fn write_backup(&self, path: &Path) -> Result<BackupVerification> {
2830 let (rows, observed_versions) = {
2831 let inner = self.state();
2832 let mut rows: Vec<StoredRow> = inner
2833 .rows
2834 .values()
2835 .flat_map(|bucket| bucket.values().cloned())
2836 .filter(|row| !row.deleted)
2837 .collect();
2838 rows.sort_by(|left, right| {
2839 left.capability
2840 .cmp(&right.capability)
2841 .then_with(|| left.key.cmp(&right.key))
2842 });
2843 (rows, inner.change_log.versions())
2844 };
2845
2846 let content_digest = backup_content_digest(&rows)?;
2847 let meaning_digest = backup_meaning_digest(&rows)?;
2848 let header = BackupHeader {
2849 record_type: BACKUP_RECORD_TYPE.to_string(),
2850 backup_version: BACKUP_VERSION,
2851 format_version: DURABLE_FORMAT_VERSION,
2852 created_unix_ms: now_ms() as u64,
2853 row_count: rows.len(),
2854 observed_versions,
2855 content_digest: content_digest.clone(),
2856 meaning_digest: meaning_digest.clone(),
2857 };
2858
2859 if let Some(parent) = path.parent() {
2860 fs::create_dir_all(parent)?;
2861 }
2862 let partial = sibling(path, "partial");
2866 let mut file = OpenOptions::new()
2867 .create(true)
2868 .write(true)
2869 .truncate(true)
2870 .open(&partial)?;
2871 write_json_line(&mut file, &header)?;
2872 for row in &rows {
2873 write_json_line(&mut file, row)?;
2874 }
2875 file.flush()?;
2876 file.sync_all()?;
2877 drop(file);
2878 fs::rename(&partial, path)?;
2879 sync_parent_directory(path)?;
2880
2881 Ok(BackupVerification {
2882 backup_version: BACKUP_VERSION,
2883 format_version: DURABLE_FORMAT_VERSION,
2884 created_unix_ms: header.created_unix_ms,
2885 rows: rows.len(),
2886 content_digest,
2887 meaning_digest,
2888 })
2889 }
2890
2891 pub fn restore_backup(backup: &Path, target: &Path) -> Result<(Self, BackupVerification)> {
2900 let verification = verify_backup(backup)?;
2901 if target.exists() {
2902 return Err(backup_problem(
2903 "the restore target already exists",
2904 None,
2905 "Restore into a new path. FeltDB will not write over an existing database.",
2906 ));
2907 }
2908
2909 let text = std::fs::read_to_string(backup)?;
2910 let mut lines = text.lines();
2911 let header: BackupHeader = serde_json::from_str(lines.next().unwrap_or_default())?;
2912
2913 if let Some(parent) = target.parent() {
2914 fs::create_dir_all(parent)?;
2915 }
2916 let staging = sibling(target, "restoring");
2921 remove_restore_staging(&staging)?;
2922 {
2923 let mut file = OpenOptions::new()
2924 .create_new(true)
2925 .write(true)
2926 .open(&staging)?;
2927 write_json_line(
2928 &mut file,
2929 &FormatRecord {
2930 record_type: FORMAT_RECORD_TYPE.to_string(),
2931 format_version: header.format_version,
2932 },
2933 )?;
2934 for line in lines {
2935 if line.trim().is_empty() {
2936 continue;
2937 }
2938 writeln!(file, "{line}")?;
2939 }
2940 file.flush()?;
2941 file.sync_all()?;
2942 }
2943
2944 let restored_meaning = {
2948 let staged = FeltDb::open(&staging)?;
2949 let inner = staged.state();
2950 let mut rows: Vec<StoredRow> = inner
2951 .rows
2952 .values()
2953 .flat_map(|bucket| bucket.values().cloned())
2954 .filter(|row| !row.deleted)
2955 .collect();
2956 rows.sort_by(|left, right| {
2957 left.capability
2958 .cmp(&right.capability)
2959 .then_with(|| left.key.cmp(&right.key))
2960 });
2961 backup_meaning_digest(&rows)?
2962 };
2963 if restored_meaning != verification.meaning_digest {
2964 remove_restore_staging(&staging)?;
2965 return Err(backup_problem(
2966 "the restored database does not mean what the backup says it means",
2967 None,
2968 "The backup or the restore path is damaged. Do not use this database.",
2969 ));
2970 }
2971 if target.exists() {
2972 remove_restore_staging(&staging)?;
2973 return Err(backup_problem(
2974 "the restore target appeared while the restore was running",
2975 None,
2976 "Restore into a new path. FeltDB will not write over an existing database.",
2977 ));
2978 }
2979 fs::rename(&staging, target)?;
2980 sync_parent_directory(target)?;
2981 remove_restore_staging(&staging)?;
2982
2983 let restored = FeltDb::open(target)?;
2984 Ok((restored, verification))
2985 }
2986
2987 pub fn state_digest(&self) -> Result<String> {
2997 let inner = self.state();
2998 let mut entries: Vec<(String, String, String)> = inner
2999 .rows
3000 .iter()
3001 .filter(|(capability, _)| is_versioned(capability))
3002 .flat_map(|(capability, bucket)| {
3003 bucket
3004 .values()
3005 .filter(|row| !row.deleted)
3006 .map(move |row| (capability.clone(), row.key.clone(), row.value.to_string()))
3007 })
3008 .collect();
3009 entries.sort();
3010
3011 let mut hasher = Sha256::new();
3012 hasher.update(b"feltdb.state.v1");
3013 for (capability, key, value) in entries {
3014 hasher.update(capability.as_bytes());
3015 hasher.update([0u8]);
3016 hasher.update(key.as_bytes());
3017 hasher.update([0u8]);
3018 hasher.update(value.as_bytes());
3019 hasher.update([0u8]);
3020 }
3021 Ok(format!("{:x}", hasher.finalize()))
3022 }
3023
3024 pub fn durability_mode(&self) -> DurabilityMode {
3026 self.state().durability_mode
3027 }
3028
3029 pub fn set_durability_mode(&self, mode: DurabilityMode) {
3034 let mut inner = self.state();
3035 inner.durability_mode = mode;
3036 inner.writes_since_barrier = 0;
3037 }
3038
3039 pub fn health(&self) -> DatabaseHealth {
3050 let recovery = self.log_recovery();
3051 let storage = match &recovery {
3052 LogRecovery::Clean => StorageHealth::Clean,
3053 LogRecovery::RecoveredTornTail {
3054 byte_offset,
3055 discarded_bytes,
3056 } => StorageHealth::RecoveredIncompleteWrite {
3057 byte_offset: *byte_offset,
3058 discarded_bytes: *discarded_bytes,
3059 },
3060 };
3061 let inner = self.state();
3062 DatabaseHealth {
3063 durable_format: inner.durable_format.clone(),
3064 recovery,
3065 storage,
3066 writes: match &inner.write_fault {
3067 None => WriteAvailability::Available,
3068 Some(reason) => WriteAvailability::RecoveryRequired {
3069 reason: reason.clone(),
3070 },
3071 },
3072 }
3073 }
3074
3075 pub fn log_recovery(&self) -> LogRecovery {
3085 self.state().log_recovery.clone()
3086 }
3087
3088 pub fn insert<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
3089 let capability = key
3090 .split_once(':')
3091 .map(|(cap, _)| cap)
3092 .unwrap_or("default")
3093 .to_string();
3094 self.insert_internal(capability, key.to_string(), value, false)?;
3095 Ok(())
3096 }
3097
3098 pub fn insert_if_absent<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
3101 let capability = key
3102 .split_once(':')
3103 .map(|(cap, _)| cap)
3104 .unwrap_or("default")
3105 .to_string();
3106 self.insert_internal(capability, key.to_string(), value, true)
3107 }
3108
3109 pub fn update<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
3111 let capability = key
3112 .split_once(':')
3113 .map(|(cap, _)| cap)
3114 .unwrap_or("default")
3115 .to_string();
3116 self.update_internal(capability, key.to_string(), value)?;
3117 Ok(())
3118 }
3119
3120 pub fn compare_and_set_json(
3124 &self,
3125 key: &str,
3126 expected_version: u64,
3127 expected_epoch: Option<u64>,
3128 expected_lease_id: Option<&str>,
3129 reject_if_diverged: bool,
3130 mut value: Value,
3131 ) -> Result<JsonCasResult> {
3132 let capability = key
3133 .split_once(':')
3134 .map(|(cap, _)| cap)
3135 .unwrap_or("default")
3136 .to_string();
3137 let rust_type = type_name::<Value>().to_string();
3138 let (result, event) = {
3139 let mut inner = self.state();
3140 let Some(current) = inner.rows.get(&capability).and_then(|rows| rows.get(key)) else {
3141 return Ok(JsonCasResult::NotFound);
3142 };
3143 let current_version = current
3144 .value
3145 .get("__version")
3146 .and_then(Value::as_u64)
3147 .unwrap_or(1);
3148 let current_epoch = current
3149 .value
3150 .pointer("/authority/epoch")
3151 .and_then(Value::as_u64)
3152 .unwrap_or(0);
3153 if current_version != expected_version {
3154 return Ok(JsonCasResult::VersionConflict {
3155 current_version,
3156 current_epoch,
3157 });
3158 }
3159 if expected_epoch.is_some_and(|expected| expected != current_epoch) {
3160 return Ok(JsonCasResult::AuthorityConflict {
3161 current_version,
3162 current_epoch,
3163 });
3164 }
3165 let persisted_lease = current.value.get("lease").filter(|lease| !lease.is_null());
3166 let lease_matches = match (persisted_lease, expected_lease_id) {
3167 (None, None) => true,
3168 (None, Some(_)) => false,
3169 (Some(lease), Some(expected_id)) => {
3170 lease.get("leaseId").and_then(Value::as_str) == Some(expected_id)
3171 && lease
3172 .get("expiresAt")
3173 .and_then(Value::as_u64)
3174 .is_some_and(|expires_at| expires_at > now_ms() as u64)
3175 }
3176 (Some(lease), None) => lease
3177 .get("expiresAt")
3178 .and_then(Value::as_u64)
3179 .is_some_and(|expires_at| expires_at <= now_ms() as u64),
3180 };
3181 if !lease_matches {
3182 return Ok(JsonCasResult::LeaseConflict {
3183 current_version,
3184 current_epoch,
3185 });
3186 }
3187 if reject_if_diverged && capability == "_cells" {
3188 let replica_key = key
3189 .strip_prefix("_cells:")
3190 .map(|suffix| format!("_replicas:{suffix}"));
3191 let divergent = replica_key.as_ref().and_then(|replica_key| {
3192 inner
3193 .rows
3194 .get("_replicas")
3195 .and_then(|rows| rows.get(replica_key))
3196 .and_then(|row| row.value.get("divergent"))
3197 .and_then(Value::as_bool)
3198 }) == Some(true);
3199 if divergent {
3200 return Ok(JsonCasResult::ReplicationConflict {
3201 current_version,
3202 current_epoch,
3203 });
3204 }
3205 }
3206
3207 let object = value.as_object_mut().ok_or_else(|| {
3208 FlowError::CapabilityError("CAS value must be a JSON object".to_string())
3209 })?;
3210 object.insert("__version".to_string(), Value::from(expected_version + 1));
3211
3212 let reserved = (inner.sequence, inner.sync_state.vector_clock.clone());
3213 inner.sequence += 1;
3214 inner.sync_state.increment_vector_clock();
3215 let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
3216 let planned =
3217 is_versioned(&capability).then(|| plan_revision(&inner, key, value.to_string()));
3218 let mut operation = Operation::update(
3219 inner.sequence,
3220 inner.instance_id.clone(),
3221 inner.sequence,
3222 key.to_string(),
3223 value.clone(),
3224 rust_type.clone(),
3225 capability.clone(),
3226 )
3227 .with_vector_clock(vector_clock);
3228 operation.revision = planned.as_ref().map(provenance_of);
3229 let row = StoredRow {
3230 capability: capability.clone(),
3231 key: key.to_string(),
3232 rust_type: rust_type.clone(),
3233 value: value.clone(),
3234 unix_ms: now_ms(),
3235 content_hash: Some(operation.content_hash.clone()),
3236 flow_ref: None,
3237 deleted: false,
3238 operation: Some(operation.clone()),
3239 };
3240 commit_local_write_locked(&mut inner, &row, planned.as_ref(), reserved)?;
3241 inner.authority_revision += 1;
3242 inner.change_log.add_operation(operation);
3243 inner.put_row(row.clone());
3244 if let Some(revision) = planned {
3245 persist_revision_locked(&mut inner, revision)?;
3246 }
3247 let event = ChangeEvent {
3248 capability,
3249 key: key.to_string(),
3250 rust_type,
3251 unix_ms: row.unix_ms,
3252 };
3253 (
3254 JsonCasResult::Updated {
3255 current_version: expected_version + 1,
3256 current_epoch: value
3257 .pointer("/authority/epoch")
3258 .and_then(Value::as_u64)
3259 .unwrap_or(current_epoch),
3260 value,
3261 },
3262 event,
3263 )
3264 };
3265 let _ = self.event_tx.send(event);
3266 Ok(result)
3267 }
3268
3269 pub fn delete(&self, key: &str) -> Result<()> {
3271 let capability = key
3272 .split_once(':')
3273 .map(|(cap, _)| cap)
3274 .unwrap_or("default")
3275 .to_string();
3276 self.delete_internal(capability, key.to_string())?;
3277 Ok(())
3278 }
3279
3280 fn evaluate_authorization(&self, context: &AuthorizationContext) -> Result<()> {
3282 if !context.capabilities.contains(&"state:write".to_string()) {
3284 return Err(FlowError::CapabilityError(
3285 "AUTHORIZATION_DENIED:missing_state_write_capability".to_string(),
3286 ));
3287 }
3288
3289 Ok(())
3293 }
3294
3295 pub fn insert_with_authorization<T: Serialize>(
3297 &self,
3298 key: &str,
3299 value: T,
3300 context: &AuthorizationContext,
3301 ) -> Result<()> {
3302 self.evaluate_authorization(context)?;
3304
3305 let capability = key
3306 .split_once(':')
3307 .map(|(cap, _)| cap)
3308 .unwrap_or("default")
3309 .to_string();
3310
3311 if !context.writable_collections.is_empty()
3313 && !context.writable_collections.contains(&capability)
3314 {
3315 return Err(FlowError::CapabilityError(format!(
3316 "AUTHORIZATION_DENIED:not_writable_collection:{}",
3317 capability
3318 )));
3319 }
3320
3321 self.insert_internal(capability, key.to_string(), value, false)?;
3323 Ok(())
3324 }
3325
3326 pub fn update_with_authorization<T: Serialize>(
3328 &self,
3329 key: &str,
3330 value: T,
3331 context: &AuthorizationContext,
3332 ) -> Result<()> {
3333 self.evaluate_authorization(context)?;
3335
3336 let capability = key
3337 .split_once(':')
3338 .map(|(cap, _)| cap)
3339 .unwrap_or("default")
3340 .to_string();
3341
3342 if !context.writable_collections.is_empty()
3344 && !context.writable_collections.contains(&capability)
3345 {
3346 return Err(FlowError::CapabilityError(format!(
3347 "AUTHORIZATION_DENIED:not_writable_collection:{}",
3348 capability
3349 )));
3350 }
3351
3352 self.update_internal(capability, key.to_string(), value)?;
3355 Ok(())
3356 }
3357
3358 pub fn delete_with_authorization(
3360 &self,
3361 key: &str,
3362 context: &AuthorizationContext,
3363 ) -> Result<()> {
3364 self.evaluate_authorization(context)?;
3366
3367 let capability = key
3368 .split_once(':')
3369 .map(|(cap, _)| cap)
3370 .unwrap_or("default")
3371 .to_string();
3372
3373 if !context.writable_collections.is_empty()
3375 && !context.writable_collections.contains(&capability)
3376 {
3377 return Err(FlowError::CapabilityError(format!(
3378 "AUTHORIZATION_DENIED:not_writable_collection:{}",
3379 capability
3380 )));
3381 }
3382
3383 self.delete_internal(capability, key.to_string())?;
3386 Ok(())
3387 }
3388
3389 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
3390 let capability = key
3391 .split_once(':')
3392 .map(|(cap, _)| cap)
3393 .unwrap_or("default")
3394 .to_string();
3395 let inner = self.state();
3396 let value = inner
3397 .rows
3398 .get(&capability)
3399 .and_then(|bucket| bucket.get(key))
3400 .map(|row| serde_json::from_value::<T>(row.value.clone()))
3401 .transpose()?;
3402 Ok(value)
3403 }
3404
3405 pub fn query<T, F>(&self, predicate: F) -> Result<Vec<T>>
3406 where
3407 T: DeserializeOwned,
3408 F: Fn(&T) -> bool,
3409 {
3410 let rust_type = type_name::<T>().to_string();
3411 let mut inner = self.state();
3412 *inner
3413 .query_count_by_type
3414 .entry(rust_type.clone())
3415 .or_default() += 1;
3416 if inner.query_count_by_type[&rust_type] >= 3 {
3417 inner.adaptive_indexes.insert(rust_type.clone());
3418 }
3419
3420 let mut out = Vec::new();
3421 for bucket in inner.rows.values() {
3422 for row in bucket.values() {
3423 if row.rust_type != rust_type {
3424 continue;
3425 }
3426 let parsed = serde_json::from_value::<T>(row.value.clone())?;
3427 if predicate(&parsed) {
3428 out.push(parsed);
3429 }
3430 }
3431 }
3432 Ok(out)
3433 }
3434
3435 pub fn capability(&self, name: &str) -> Capability {
3436 Capability {
3437 db: self.clone(),
3438 name: name.to_string(),
3439 }
3440 }
3441
3442 pub fn is_auto_indexed<T>(&self) -> bool {
3443 let rust_type = type_name::<T>().to_string();
3444 let inner = self.state();
3445 inner.adaptive_indexes.contains(&rust_type)
3446 }
3447
3448 pub fn instance_id(&self) -> Result<String> {
3450 let inner = self.state();
3451 Ok(inner.instance_id.clone())
3452 }
3453
3454 pub fn sequence(&self) -> Result<u64> {
3456 let inner = self.state();
3457 Ok(inner.sequence)
3458 }
3459
3460 pub fn current_revision(&self) -> Result<u64> {
3464 Ok(self.state().authority_revision)
3465 }
3466
3467 pub(crate) fn authority_state(&self) -> (u64, Vec<StoredRow>) {
3470 let inner = self.state();
3471 let rows = inner
3472 .rows
3473 .values()
3474 .flat_map(|bucket| bucket.values().cloned())
3475 .collect();
3476 (inner.authority_revision, rows)
3477 }
3478
3479 pub fn collection_cardinality(&self, capability: &str) -> Result<u64> {
3482 let inner = self.state();
3483 Ok(inner
3484 .collection_cardinality
3485 .get(capability)
3486 .copied()
3487 .unwrap_or(0))
3488 }
3489
3490 pub fn list_cardinalities(&self) -> Result<Vec<(String, u64)>> {
3493 let inner = self.state();
3494 let mut result: Vec<_> = inner
3495 .collection_cardinality
3496 .iter()
3497 .map(|(k, v)| (k.clone(), *v))
3498 .collect();
3499 result.sort_by(|a, b| b.1.cmp(&a.1)); Ok(result)
3501 }
3502
3503 pub fn diagnostic_row_count(&self, capability_pattern: &str) -> Result<usize> {
3506 let inner = self.state();
3507 Ok(inner
3508 .rows
3509 .iter()
3510 .filter(|(cap, _)| cap.contains(capability_pattern))
3511 .map(|(_, bucket)| bucket.values().filter(|row| !row.deleted).count())
3512 .sum())
3513 }
3514
3515 pub fn diagnostic_capability_keys(&self, capability_pattern: &str) -> Result<Vec<String>> {
3518 let inner = self.state();
3519 let mut keys: Vec<_> = inner
3520 .rows
3521 .keys()
3522 .filter(|k| k.contains(capability_pattern))
3523 .cloned()
3524 .collect();
3525 keys.sort();
3526 Ok(keys)
3527 }
3528
3529 pub(crate) fn revision_head_ids(&self) -> Vec<state_model::StateId> {
3533 self.state()
3534 .revision_heads
3535 .values()
3536 .map(|(id, _)| id.clone())
3537 .collect()
3538 }
3539
3540 pub fn committed_transaction(
3546 &self,
3547 transaction_id: &str,
3548 payload_hash: Option<&str>,
3549 ) -> Result<Option<AtomicCommit>> {
3550 let inner = self.state();
3551 committed_transaction_locked(&inner, transaction_id, payload_hash).transpose()
3552 }
3553
3554 pub fn has_applied_transaction(&self, transaction_id: &str) -> Result<bool> {
3555 Ok(self
3556 .inner
3557 .lock()
3558 .expect("lock poisoned")
3559 .applied_transactions
3560 .contains(transaction_id))
3561 }
3562
3563 pub fn applied_transaction_payload_hash(&self, transaction_id: &str) -> Result<Option<String>> {
3564 Ok(self
3565 .inner
3566 .lock()
3567 .expect("lock poisoned")
3568 .transaction_payload_hashes
3569 .get(transaction_id)
3570 .cloned())
3571 }
3572
3573 pub fn apply_atomic_transaction(
3577 &self,
3578 transaction_id: &str,
3579 expected_parent: Option<u64>,
3580 preconditions: &[AtomicPrecondition],
3581 mutations: &[AtomicMutation],
3582 audit: Option<Value>,
3583 ) -> Result<AtomicCommit> {
3584 self.apply_atomic_transaction_content_addressed(
3585 transaction_id,
3586 None,
3587 expected_parent,
3588 preconditions,
3589 mutations,
3590 audit,
3591 )
3592 }
3593
3594 pub fn apply_atomic_transaction_content_addressed(
3597 &self,
3598 transaction_id: &str,
3599 payload_hash: Option<&str>,
3600 expected_parent: Option<u64>,
3601 preconditions: &[AtomicPrecondition],
3602 mutations: &[AtomicMutation],
3603 audit: Option<Value>,
3604 ) -> Result<AtomicCommit> {
3605 self.apply_atomic_transaction_guarded(
3606 transaction_id,
3607 payload_hash,
3608 expected_parent,
3609 preconditions,
3610 &[],
3611 mutations,
3612 audit,
3613 )
3614 }
3615
3616 #[allow(clippy::too_many_arguments)]
3630 pub fn apply_atomic_transaction_guarded(
3631 &self,
3632 transaction_id: &str,
3633 payload_hash: Option<&str>,
3634 expected_parent: Option<u64>,
3635 preconditions: &[AtomicPrecondition],
3636 record_preconditions: &[RecordPrecondition],
3637 mutations: &[AtomicMutation],
3638 audit: Option<Value>,
3639 ) -> Result<AtomicCommit> {
3640 self.apply_atomic_transaction_fenced(
3641 transaction_id,
3642 payload_hash,
3643 expected_parent,
3644 preconditions,
3645 record_preconditions,
3646 &[],
3647 mutations,
3648 audit,
3649 )
3650 }
3651
3652 pub fn read_basis(&self, reads: &ReadSet) -> Result<ReadBasis> {
3654 self.read_basis_in(reads, &ReadLayout::Direct)
3655 }
3656
3657 pub fn read_basis_in(&self, reads: &ReadSet, layout: &ReadLayout) -> Result<ReadBasis> {
3659 let reads = reads.normalised()?;
3660 let inner = self.state();
3661 let token = read_basis_token(&inner.rows, &reads, layout);
3662 Ok(ReadBasis { reads, token })
3663 }
3664
3665 #[allow(clippy::too_many_arguments)]
3673 pub fn apply_atomic_transaction_fenced(
3674 &self,
3675 transaction_id: &str,
3676 payload_hash: Option<&str>,
3677 expected_parent: Option<u64>,
3678 preconditions: &[AtomicPrecondition],
3679 record_preconditions: &[RecordPrecondition],
3680 fences: &[ReadBasis],
3681 mutations: &[AtomicMutation],
3682 audit: Option<Value>,
3683 ) -> Result<AtomicCommit> {
3684 self.apply_atomic_transaction_fenced_in(
3685 transaction_id,
3686 payload_hash,
3687 expected_parent,
3688 preconditions,
3689 record_preconditions,
3690 &ReadLayout::Direct,
3691 fences,
3692 mutations,
3693 audit,
3694 )
3695 }
3696
3697 #[allow(clippy::too_many_arguments)]
3700 pub fn apply_atomic_transaction_fenced_in(
3701 &self,
3702 transaction_id: &str,
3703 payload_hash: Option<&str>,
3704 expected_parent: Option<u64>,
3705 preconditions: &[AtomicPrecondition],
3706 record_preconditions: &[RecordPrecondition],
3707 layout: &ReadLayout,
3708 fences: &[ReadBasis],
3709 mutations: &[AtomicMutation],
3710 audit: Option<Value>,
3711 ) -> Result<AtomicCommit> {
3712 self.apply_atomic_transaction_collection_fenced_in(
3713 transaction_id,
3714 payload_hash,
3715 expected_parent,
3716 preconditions,
3717 record_preconditions,
3718 layout,
3719 fences,
3720 &[],
3721 mutations,
3722 audit,
3723 )
3724 }
3725
3726 pub(crate) fn apply_atomic_transaction_collection_fenced_in(
3733 &self,
3734 transaction_id: &str,
3735 payload_hash: Option<&str>,
3736 expected_parent: Option<u64>,
3737 preconditions: &[AtomicPrecondition],
3738 record_preconditions: &[RecordPrecondition],
3739 layout: &ReadLayout,
3740 fences: &[ReadBasis],
3741 collection_fences: &[(String, u64)],
3742 mutations: &[AtomicMutation],
3743 audit: Option<Value>,
3744 ) -> Result<AtomicCommit> {
3745 let (commit, events) = {
3746 let mut inner = self.state();
3747 if let Some(duplicate) =
3748 committed_transaction_locked(&inner, transaction_id, payload_hash)
3749 {
3750 return duplicate;
3751 }
3752 for condition in record_preconditions {
3757 if condition.is_empty() {
3758 return Err(FlowError::CapabilityError(format!(
3759 "EMPTY_PRECONDITION:{}:{}",
3760 condition.capability, condition.key
3761 )));
3762 }
3763 let collection = condition.capability.clone();
3764 let key = condition.key.clone();
3765 let current = inner
3766 .rows
3767 .get(&condition.capability)
3768 .and_then(|bucket| bucket.get(&condition.key));
3769
3770 if condition.require_absent {
3771 if current.is_some() {
3772 return Err(FlowError::PreconditionFailed(Box::new(
3773 PreconditionFailure::Present { collection, key },
3774 )));
3775 }
3776 continue;
3777 }
3778
3779 let Some(current) = current else {
3780 return Err(FlowError::PreconditionFailed(Box::new(
3781 PreconditionFailure::Missing { collection, key },
3782 )));
3783 };
3784
3785 if let Some(expected) = condition.expected_version {
3787 let actual = current
3788 .value
3789 .get("__version")
3790 .and_then(Value::as_u64)
3791 .unwrap_or(1);
3792 if actual != expected {
3793 return Err(FlowError::PreconditionFailed(Box::new(
3794 PreconditionFailure::Version {
3795 collection,
3796 key,
3797 expected,
3798 actual,
3799 },
3800 )));
3801 }
3802 }
3803 if let Some(expected) = condition.expected_epoch {
3804 let actual = current
3805 .value
3806 .pointer("/authority/epoch")
3807 .and_then(Value::as_u64)
3808 .unwrap_or(0);
3809 if actual != expected {
3810 return Err(FlowError::PreconditionFailed(Box::new(
3811 PreconditionFailure::Epoch {
3812 collection,
3813 key,
3814 expected,
3815 actual,
3816 },
3817 )));
3818 }
3819 }
3820 if let Some(expected) = &condition.expected_lease_id {
3821 let lease = current.value.get("lease").filter(|lease| !lease.is_null());
3822 let held = lease.and_then(|lease| {
3823 lease
3824 .get("leaseId")
3825 .and_then(Value::as_str)
3826 .map(str::to_string)
3827 });
3828 let unexpired = lease
3831 .and_then(|lease| lease.get("expiresAt").and_then(Value::as_u64))
3832 .is_some_and(|expires_at| expires_at > now_ms() as u64);
3833 if held.as_deref() != Some(expected.as_str()) || !unexpired {
3834 return Err(FlowError::PreconditionFailed(Box::new(
3835 PreconditionFailure::Lease {
3836 collection,
3837 key,
3838 expected: expected.clone(),
3839 actual: held,
3840 },
3841 )));
3842 }
3843 }
3844 }
3845
3846 for fence in fences {
3847 if !fence.token.starts_with(&layout.token_prefix()) {
3848 return Err(FlowError::CapabilityError(
3849 "INVALID_READ_SET:a read basis was not issued by this authority scope"
3850 .into(),
3851 ));
3852 }
3853 let reads = fence.reads.normalised()?;
3854 let actual = read_basis_token(&inner.rows, &reads, layout);
3855 if actual != fence.token {
3856 let collection = reads
3857 .records
3858 .first()
3859 .map(|record| record.collection.clone())
3860 .or_else(|| {
3861 reads
3862 .predicates
3863 .first()
3864 .map(|predicate| predicate.collection.clone())
3865 })
3866 .unwrap_or_default();
3867 return Err(FlowError::PreconditionFailed(Box::new(
3868 PreconditionFailure::Fence {
3869 collection,
3870 key: "read-basis".into(),
3871 expected: fence.token.clone(),
3872 actual,
3873 },
3874 )));
3875 }
3876 }
3877 for (capability, expected) in collection_fences {
3878 let actual = inner.collection_versions.get(capability).copied().unwrap_or(0);
3879 if actual != *expected {
3880 return Err(FlowError::PreconditionFailed(Box::new(
3881 PreconditionFailure::Fence {
3882 collection: capability.clone(),
3883 key: COLLECTION_FENCE_KEY.into(),
3884 expected: format!("collection-version:{expected}"),
3885 actual: format!("collection-version:{actual}"),
3886 },
3887 )));
3888 }
3889 }
3890
3891 let advanced: Vec<AtomicMutation>;
3901 let creates: HashSet<(&str, &str)> = preconditions
3902 .iter()
3903 .filter(|condition| condition.expected_version.is_none())
3904 .map(|condition| (condition.capability.as_str(), condition.key.as_str()))
3905 .chain(
3906 record_preconditions
3907 .iter()
3908 .filter(|condition| condition.require_absent)
3909 .map(|condition| (condition.capability.as_str(), condition.key.as_str())),
3910 )
3911 .collect();
3912 let fenced: HashMap<(&str, &str), u64> = record_preconditions
3913 .iter()
3914 .filter_map(|condition| {
3915 condition.expected_version.map(|version| {
3916 (
3917 (condition.capability.as_str(), condition.key.as_str()),
3918 version,
3919 )
3920 })
3921 })
3922 .collect();
3923 let mutations: &[AtomicMutation] = if fenced.is_empty() && creates.is_empty() {
3924 mutations
3925 } else {
3926 advanced = mutations
3927 .iter()
3928 .map(|mutation| {
3929 let record = (mutation.capability.as_str(), mutation.key.as_str());
3930 let version = if creates.contains(&record) {
3931 Some(1)
3932 } else {
3933 fenced.get(&record).map(|expected| expected + 1)
3934 };
3935 let Some(version) = version else {
3936 return mutation.clone();
3937 };
3938 let Some(Value::Object(fields)) = mutation.value.clone() else {
3941 return mutation.clone();
3942 };
3943 let mut fields = fields;
3944 fields.insert("__version".to_string(), Value::from(version));
3947 AtomicMutation {
3948 capability: mutation.capability.clone(),
3949 key: mutation.key.clone(),
3950 value: Some(Value::Object(fields)),
3951 }
3952 })
3953 .collect();
3954 &advanced
3955 };
3956
3957 let mut bookkeeping_nanos = 0u64;
3964 let prepare_began = workload_diagnostics::enabled().then(std::time::Instant::now);
3965 let base_revision = inner.authority_revision;
3966 if let Some(expected) = expected_parent {
3967 if expected != base_revision {
3968 return Err(FlowError::RevisionConflict {
3969 expected,
3970 actual: base_revision,
3971 });
3972 }
3973 }
3974 for condition in preconditions {
3975 let actual = inner
3976 .rows
3977 .get(&condition.capability)
3978 .and_then(|bucket| bucket.get(&condition.key))
3979 .and_then(|row| row.operation.as_ref())
3980 .map(|op| op.sequence);
3981 if actual != condition.expected_version {
3982 return Err(FlowError::CapabilityError(format!(
3983 "PRECONDITION_FAILED:{}:{}:{:?}:{actual:?}",
3984 condition.capability, condition.key, condition.expected_version
3985 )));
3986 }
3987 }
3988 let state_before = inner.sequence;
3989 let mut next_sequence = state_before;
3990 let mut rows = Vec::with_capacity(mutations.len());
3991 let mut vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
3992 for mutation in mutations {
3993 next_sequence += 1;
3994 *vector_clock.entry(inner.instance_id.clone()).or_default() += 1;
3995 let rust_type = "serde_json::Value".to_string();
3996 let operation = match &mutation.value {
3997 Some(value)
3998 if inner
3999 .rows
4000 .get(&mutation.capability)
4001 .is_some_and(|bucket| bucket.contains_key(&mutation.key)) =>
4002 {
4003 Operation::update(
4004 next_sequence,
4005 inner.instance_id.clone(),
4006 next_sequence,
4007 mutation.key.clone(),
4008 value.clone(),
4009 rust_type.clone(),
4010 mutation.capability.clone(),
4011 )
4012 }
4013 Some(value) => Operation::insert(
4014 next_sequence,
4015 inner.instance_id.clone(),
4016 next_sequence,
4017 mutation.key.clone(),
4018 value.clone(),
4019 rust_type.clone(),
4020 mutation.capability.clone(),
4021 ),
4022 None => Operation::delete(
4023 next_sequence,
4024 inner.instance_id.clone(),
4025 next_sequence,
4026 mutation.key.clone(),
4027 rust_type.clone(),
4028 mutation.capability.clone(),
4029 ),
4030 }
4031 .with_vector_clock(vector_clock.clone());
4032 rows.push(StoredRow {
4033 capability: mutation.capability.clone(),
4034 key: mutation.key.clone(),
4035 rust_type,
4036 value: mutation.value.clone().unwrap_or(Value::Null),
4037 unix_ms: now_ms(),
4038 content_hash: Some(operation.content_hash.clone()),
4039 flow_ref: None,
4040 deleted: mutation.value.is_none(),
4041 operation: Some(operation),
4042 });
4043 }
4044 let mut audit = audit;
4045 if let Some(object) = audit.as_mut().and_then(Value::as_object_mut) {
4046 object.insert("state_before".into(), Value::from(state_before));
4047 object.insert("state_after".into(), Value::from(next_sequence));
4048 }
4049 let record = TransactionLogRecord {
4050 record_type: "feltdb.transaction.v1".into(),
4051 transaction_id: transaction_id.into(),
4052 payload_hash: payload_hash.map(str::to_owned),
4053 state_before,
4054 state_after: next_sequence,
4055 base_revision,
4056 commit_revision: base_revision + 1,
4057 rows: rows.clone(),
4058 audit,
4059 };
4060 if let Some(began) = prepare_began {
4061 bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
4062 }
4063 append_transaction_locked(&mut inner, &record)?;
4064 remember_attribution(
4068 &mut inner,
4069 TransactionAttribution::from_record(
4070 transaction_id,
4071 record.audit.as_ref(),
4072 base_revision,
4073 base_revision + 1,
4074 state_before,
4075 next_sequence,
4076 &record.rows,
4077 now_ms(),
4078 ),
4079 );
4080 inner.authority_revision = base_revision + 1;
4081 inner.sequence = next_sequence;
4082 for row in &rows {
4083 if let Some(operation) = row.operation.clone() {
4084 inner.change_log.add_operation(operation);
4085 }
4086 if row.deleted {
4087 inner.remove_row(&row.capability, &row.key);
4088 } else {
4089 inner.put_row(row.clone());
4093 }
4094 }
4095 let tail_began = workload_diagnostics::enabled().then(std::time::Instant::now);
4096 inner.sync_state.merge_vector_clock(&vector_clock);
4097 inner.applied_transactions.insert(transaction_id.into());
4098 let commit_revision = inner.authority_revision;
4099 inner
4100 .transaction_revisions
4101 .insert(transaction_id.into(), (base_revision, commit_revision));
4102 if let Some(hash) = payload_hash {
4103 inner
4104 .transaction_payload_hashes
4105 .insert(transaction_id.into(), hash.into());
4106 }
4107 if let Some(began) = tail_began {
4108 bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
4109 }
4110 workload_diagnostics::record(
4111 workload_diagnostics::Phase::TransactionCommit,
4112 bookkeeping_nanos,
4113 );
4114 let events = rows
4115 .iter()
4116 .map(|row| ChangeEvent {
4117 capability: row.capability.clone(),
4118 key: row.key.clone(),
4119 rust_type: row.rust_type.clone(),
4120 unix_ms: row.unix_ms,
4121 })
4122 .collect::<Vec<_>>();
4123 (
4124 AtomicCommit {
4125 transaction_id: transaction_id.into(),
4126 base_revision,
4127 commit_revision: inner.authority_revision,
4128 status: TransactionStatus::Committed,
4129 state_before,
4130 state_after: inner.sequence,
4131 rows,
4132 duplicate: false,
4133 payload_binding: None,
4134 },
4135 events,
4136 )
4137 };
4138 for event in events {
4139 let _ = self.event_tx.send(event);
4140 }
4141 Ok(commit)
4142 }
4143
4144 pub fn apply_atomic_transaction_with_authorization(
4146 &self,
4147 transaction_id: &str,
4148 expected_parent: Option<u64>,
4149 preconditions: &[AtomicPrecondition],
4150 mutations: &[AtomicMutation],
4151 audit: Option<Value>,
4152 context: &AuthorizationContext,
4153 ) -> Result<AtomicCommit> {
4154 self.evaluate_authorization(context)?;
4156
4157 for mutation in mutations {
4159 if !context.writable_collections.is_empty()
4160 && !context.writable_collections.contains(&mutation.capability)
4161 {
4162 return Err(FlowError::CapabilityError(format!(
4163 "AUTHORIZATION_DENIED:not_writable_collection:{}",
4164 mutation.capability
4165 )));
4166 }
4167 }
4168
4169 self.apply_atomic_transaction(
4171 transaction_id,
4172 expected_parent,
4173 preconditions,
4174 mutations,
4175 audit,
4176 )
4177 }
4178
4179 fn insert_internal<T: Serialize>(
4180 &self,
4181 capability: String,
4182 key: String,
4183 value: T,
4184 require_absent: bool,
4185 ) -> Result<()> {
4186 let rust_type = type_name::<T>().to_string();
4187 let value_json = serde_json::to_value(value)?;
4188 let canonical = value_json.to_string();
4191 let mut row = StoredRow {
4192 capability: capability.clone(),
4193 key: key.clone(),
4194 rust_type: rust_type.clone(),
4195 value: value_json.clone(),
4196 unix_ms: now_ms(),
4197 content_hash: None,
4198 flow_ref: None,
4199 deleted: false,
4200 operation: None,
4201 };
4202
4203 {
4204 let mut inner = self.state();
4205
4206 if require_absent
4207 && inner
4208 .rows
4209 .get(&capability)
4210 .and_then(|bucket| bucket.get(&key))
4211 .is_some()
4212 {
4213 return Err(FlowError::PreconditionFailed(Box::new(
4214 PreconditionFailure::Present {
4215 collection: capability,
4216 key,
4217 },
4218 )));
4219 }
4220
4221 let reserved = (inner.sequence, inner.sync_state.vector_clock.clone());
4222 inner.sequence += 1;
4223 inner.sync_state.increment_vector_clock();
4224
4225 let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
4227
4228 let planned =
4231 is_versioned(&capability).then(|| plan_revision(&inner, &key, canonical.clone()));
4232
4233 let mut op = Operation::insert(
4235 inner.sequence,
4236 inner.instance_id.clone(),
4237 inner.sequence,
4238 key.clone(),
4239 value_json,
4240 rust_type.clone(),
4241 capability.clone(),
4242 )
4243 .with_vector_clock(vector_clock);
4244 op.revision = planned.as_ref().map(provenance_of);
4245
4246 row.operation = Some(op.clone());
4249 commit_local_write_locked(&mut inner, &row, planned.as_ref(), reserved)?;
4250 inner.change_log.add_operation(op);
4251 let base_revision = inner.authority_revision;
4252 inner.authority_revision += 1;
4253 remember_operation_attribution(&mut inner, &row, base_revision, base_revision + 1);
4254 inner.put_row(row);
4255 if let Some(revision) = planned {
4256 persist_revision_locked(&mut inner, revision)?;
4257 }
4258 let suffix = key.rsplit_once(':').map(|(_, right)| right);
4259 if let Some(id) = suffix.and_then(|s| s.parse::<u64>().ok()) {
4260 let entry = inner.key_counters.entry(capability.clone()).or_default();
4261 *entry = (*entry).max(id + 1);
4262 }
4263 }
4264
4265 let event = ChangeEvent {
4266 capability,
4267 key,
4268 rust_type,
4269 unix_ms: now_ms(),
4270 };
4271 let _ = self.event_tx.send(event);
4272
4273 Ok(())
4274 }
4275
4276 fn update_internal<T: Serialize>(
4277 &self,
4278 capability: String,
4279 key: String,
4280 value: T,
4281 ) -> Result<()> {
4282 let rust_type = type_name::<T>().to_string();
4283 let value_json = serde_json::to_value(value)?;
4284 let canonical = value_json.to_string();
4287 let mut row = StoredRow {
4288 capability: capability.clone(),
4289 key: key.clone(),
4290 rust_type: rust_type.clone(),
4291 value: value_json.clone(),
4292 unix_ms: now_ms(),
4293 content_hash: None,
4294 flow_ref: None,
4295 deleted: false,
4296 operation: None,
4297 };
4298
4299 {
4300 let mut inner = self.state();
4301
4302 let reserved = (inner.sequence, inner.sync_state.vector_clock.clone());
4303 inner.sequence += 1;
4304 inner.sync_state.increment_vector_clock();
4305
4306 let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
4308
4309 let planned =
4310 is_versioned(&capability).then(|| plan_revision(&inner, &key, canonical.clone()));
4311
4312 let mut op = Operation::update(
4314 inner.sequence,
4315 inner.instance_id.clone(),
4316 inner.sequence,
4317 key.clone(),
4318 value_json,
4319 rust_type.clone(),
4320 capability.clone(),
4321 )
4322 .with_vector_clock(vector_clock);
4323 op.revision = planned.as_ref().map(provenance_of);
4324
4325 row.operation = Some(op.clone());
4328 commit_local_write_locked(&mut inner, &row, planned.as_ref(), reserved)?;
4329 inner.change_log.add_operation(op);
4330 let base_revision = inner.authority_revision;
4331 inner.authority_revision += 1;
4332 remember_operation_attribution(&mut inner, &row, base_revision, base_revision + 1);
4333 inner.put_row(row);
4334 if let Some(revision) = planned {
4335 persist_revision_locked(&mut inner, revision)?;
4336 }
4337 }
4338
4339 let event = ChangeEvent {
4340 capability,
4341 key,
4342 rust_type,
4343 unix_ms: now_ms(),
4344 };
4345 let _ = self.event_tx.send(event);
4346
4347 Ok(())
4348 }
4349
4350 fn delete_internal(&self, capability: String, key: String) -> Result<()> {
4351 {
4352 let mut inner = self.state();
4353
4354 let rust_type = inner
4356 .rows
4357 .get(&capability)
4358 .and_then(|bucket| bucket.get(&key))
4359 .map(|row| row.rust_type.clone())
4360 .unwrap_or_else(|| "Unknown".to_string());
4361
4362 let reserved = (inner.sequence, inner.sync_state.vector_clock.clone());
4363 inner.sequence += 1;
4364 inner.sync_state.increment_vector_clock();
4365
4366 let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
4368
4369 let op = Operation::delete(
4371 inner.sequence,
4372 inner.instance_id.clone(),
4373 inner.sequence,
4374 key.clone(),
4375 rust_type.clone(),
4376 capability.clone(),
4377 )
4378 .with_vector_clock(vector_clock);
4379
4380 let tombstone = StoredRow {
4384 capability: capability.clone(),
4385 key: key.clone(),
4386 rust_type,
4387 value: serde_json::Value::Null,
4388 unix_ms: now_ms(),
4389 content_hash: None,
4390 flow_ref: None,
4391 deleted: true,
4392 operation: Some(op.clone()),
4393 };
4394 commit_local_write_locked(&mut inner, &tombstone, None, reserved)?;
4395 inner.change_log.add_operation(op);
4397 let base_revision = inner.authority_revision;
4398 inner.authority_revision += 1;
4399 remember_operation_attribution(
4400 &mut inner,
4401 &tombstone,
4402 base_revision,
4403 base_revision + 1,
4404 );
4405
4406 inner.remove_row(&capability, &key);
4408 }
4409
4410 let event = ChangeEvent {
4411 capability,
4412 key,
4413 rust_type: "Deleted".to_string(),
4414 unix_ms: now_ms(),
4415 };
4416 let _ = self.event_tx.send(event);
4417
4418 Ok(())
4419 }
4420 pub async fn live_query<T, F>(&self, predicate: F) -> Result<LiveQuerySubscription<T>>
4440 where
4441 T: DeserializeOwned + Send + Sync + 'static,
4442 F: Fn(&T) -> bool + Send + Sync + 'static,
4443 {
4444 let rust_type = type_name::<T>().to_string();
4445
4446 let initial_results = {
4447 let mut inner = self.state();
4448 *inner
4449 .query_count_by_type
4450 .entry(rust_type.clone())
4451 .or_default() += 1;
4452 if inner.query_count_by_type[&rust_type] >= 3 {
4453 inner.adaptive_indexes.insert(rust_type.clone());
4454 }
4455
4456 let mut out = Vec::new();
4457 for bucket in inner.rows.values() {
4458 for row in bucket.values() {
4459 if row.rust_type != rust_type {
4460 continue;
4461 }
4462 let parsed = serde_json::from_value::<T>(row.value.clone())?;
4463 if predicate(&parsed) {
4464 out.push(parsed);
4465 }
4466 }
4467 }
4468 out
4469 };
4470
4471 let rx = self.event_tx.subscribe();
4472 let db = self.clone();
4473 let subscription = LiveQuerySubscription {
4474 initial_results,
4475 rx,
4476 db,
4477 rust_type,
4478 predicate: Arc::new(Mutex::new(None)),
4479 };
4480
4481 Ok(subscription)
4482 }
4483
4484 pub fn sync_info(&self) -> Result<SyncState> {
4486 let inner = self.state();
4487 Ok(inner.sync_state.clone())
4488 }
4489
4490 pub fn authority_time_ms(&self) -> u64 {
4496 now_ms() as u64
4497 }
4498
4499 pub fn get_value(&self, key: &str) -> Result<Option<Value>> {
4501 self.get::<Value>(key)
4502 }
4503
4504 pub(crate) fn live_row(&self, capability: &str, key: &str) -> Option<StoredRow> {
4510 let inner = self.state();
4511 inner
4512 .rows
4513 .get(capability)
4514 .and_then(|rows| rows.get(key))
4515 .filter(|row| !row.deleted)
4516 .cloned()
4517 }
4518
4519 pub(crate) fn consistent_rows(
4531 &self,
4532 capabilities: &BTreeSet<String>,
4533 records: &BTreeSet<(String, String)>,
4534 ) -> ConsistentRows {
4535 let inner = self.state();
4536 let versions = capabilities
4537 .iter()
4538 .map(|capability| {
4539 (
4540 capability.clone(),
4541 inner.collection_versions.get(capability).copied().unwrap_or(0),
4542 )
4543 })
4544 .collect();
4545 let mut view: HashMap<String, BTreeMap<String, StoredRow>> = HashMap::new();
4546 for capability in capabilities {
4547 if let Some(bucket) = inner.rows.get(capability) {
4548 query_execution_diagnostics::record_full_materialization(bucket.len());
4549 view.insert(capability.clone(), bucket.clone());
4550 }
4551 }
4552 for (capability, key) in records {
4553 if capabilities.contains(capability) {
4554 continue;
4555 }
4556 if let Some(row) = inner
4557 .rows
4558 .get(capability)
4559 .and_then(|bucket| bucket.get(key))
4560 .filter(|row| !row.deleted)
4561 {
4562 view.entry(capability.clone())
4563 .or_default()
4564 .insert(key.clone(), row.clone());
4565 }
4566 }
4567 ConsistentRows { view, versions }
4568 }
4569
4570 pub fn list_collection(&self, capability: &str) -> Result<Vec<StoredRow>> {
4571 let inner = self.state();
4572 let rows: Vec<StoredRow> = inner
4573 .rows
4574 .get(capability)
4575 .map(|rows| rows.values().cloned().collect())
4576 .unwrap_or_default();
4577 query_execution_diagnostics::record_full_materialization(rows.len());
4578 Ok(rows)
4579 }
4580
4581 pub fn query_collection<F>(
4598 &self,
4599 capability: &str,
4600 limit: Option<usize>,
4601 mut predicate: F,
4602 ) -> Result<Vec<StoredRow>>
4603 where
4604 F: FnMut(&StoredRow) -> bool,
4605 {
4606 let mut matched = Vec::new();
4607 let mut visited = 0usize;
4608 {
4609 let inner = self.state();
4610 let _query = workload_diagnostics::span(workload_diagnostics::Phase::ScanQuery);
4611 if let Some(rows) = inner.rows.get(capability) {
4612 for row in rows.values() {
4613 if limit.is_some_and(|bound| matched.len() >= bound) {
4614 break;
4615 }
4616 visited += 1;
4617 let _predicate = workload_diagnostics::span(
4621 workload_diagnostics::Phase::PredicateEvaluation,
4622 );
4623 if predicate(row) {
4624 drop(_predicate);
4625 matched.push(row.clone());
4626 }
4627 }
4628 }
4629 }
4630 query_execution_diagnostics::record_scan(visited, matched.len());
4631 Ok(matched)
4632 }
4633
4634 fn state(&self) -> StateGuard<'_> {
4650 if !workload_diagnostics::enabled() {
4651 return StateGuard {
4652 inner: self.inner.lock().expect("lock poisoned"),
4653 held_since: None,
4654 };
4655 }
4656 let requested = std::time::Instant::now();
4657 let inner = self.inner.lock().expect("lock poisoned");
4658 let acquired = std::time::Instant::now();
4659 workload_diagnostics::record(
4660 workload_diagnostics::Phase::StateLockWait,
4661 acquired
4662 .duration_since(requested)
4663 .as_nanos()
4664 .min(u64::MAX as u128) as u64,
4665 );
4666 StateGuard {
4667 inner,
4668 held_since: Some(acquired),
4669 }
4670 }
4671
4672 pub fn create_equality_index(&self, collection: &str, field: &str) -> Result<bool> {
4684 let mut inner = self.state();
4685 let created = inner
4686 .equality_index
4687 .declare(collection, field)
4688 .map_err(FlowError::CapabilityError)?;
4689 if created {
4690 let Inner {
4691 rows,
4692 equality_index,
4693 ..
4694 } = &mut *inner;
4695 equality_index.rebuild_field(collection, field, rows);
4696 }
4697 Ok(created)
4698 }
4699
4700 pub fn drop_equality_index(&self, collection: &str, field: &str) -> Result<bool> {
4702 let mut inner = self.state();
4703 Ok(inner.equality_index.undeclare(collection, field))
4704 }
4705
4706 pub fn equality_indexes(&self) -> Result<Vec<(String, String)>> {
4708 let inner = self.state();
4709 Ok(inner.equality_index.declarations())
4710 }
4711
4712 pub fn rebuild_equality_indexes(&self) -> Result<EqualityIndexStats> {
4718 let mut inner = self.state();
4719 let Inner {
4720 rows,
4721 equality_index,
4722 ..
4723 } = &mut *inner;
4724 equality_index.rebuild(rows);
4725 Ok(equality_index.stats())
4726 }
4727
4728 pub fn equality_index_stats(&self) -> Result<EqualityIndexStats> {
4730 let inner = self.state();
4731 Ok(inner.equality_index.stats())
4732 }
4733
4734 #[doc(hidden)]
4740 pub fn equality_index_snapshot(
4741 &self,
4742 ) -> Result<BTreeMap<String, BTreeMap<String, BTreeMap<String, Vec<String>>>>> {
4743 let inner = self.state();
4744 Ok(inner.equality_index.snapshot())
4745 }
4746
4747 pub fn verify_equality_index(&self) -> Result<std::result::Result<(), String>> {
4755 let inner = self.state();
4756 let live = inner.equality_index.snapshot();
4757 let mut expected = inner.equality_index.clone();
4758 expected.clear();
4759 expected.rebuild(&inner.rows);
4760 let expected = expected.snapshot();
4761 if live == expected {
4762 return Ok(Ok(()));
4763 }
4764 for (collection, fields) in expected.iter() {
4765 for (field, buckets) in fields {
4766 let observed = live.get(collection).and_then(|fields| fields.get(field));
4767 if observed != Some(buckets) {
4768 return Ok(Err(format!(
4769 "index for {collection}.{field} diverges from authoritative state: \
4770 {} value buckets held, {} derived",
4771 observed.map(BTreeMap::len).unwrap_or(0),
4772 buckets.len(),
4773 )));
4774 }
4775 }
4776 }
4777 Ok(Err(
4778 "the live index holds entries no authoritative record derives".to_string(),
4779 ))
4780 }
4781
4782 pub fn equality_index_candidates(
4791 &self,
4792 collection: &str,
4793 field: &str,
4794 value: &Value,
4795 ) -> Result<Option<Vec<String>>> {
4796 let inner = self.state();
4797 Ok(inner
4798 .equality_index
4799 .candidates(collection, field, value)
4800 .map(|keys| keys.iter().cloned().collect()))
4801 }
4802
4803 pub fn query_collection_by_equality<F>(
4824 &self,
4825 capability: &str,
4826 equalities: &[(&str, &Value)],
4827 mut predicate: F,
4828 ) -> Result<Option<Vec<StoredRow>>>
4829 where
4830 F: FnMut(&StoredRow) -> bool,
4831 {
4832 let mut matched = Vec::new();
4833 let mut examined = 0usize;
4834 let mut evaluated = 0usize;
4835 {
4836 let inner = self.state();
4837 let _query = workload_diagnostics::span(workload_diagnostics::Phase::IndexedQuery);
4838 let lookup = workload_diagnostics::span(workload_diagnostics::Phase::IndexLookup);
4843 let mut applicable: Vec<&BTreeSet<String>> = Vec::new();
4844 for (field, value) in equalities {
4845 if let Some(keys) = inner.equality_index.candidates(capability, field, value) {
4846 applicable.push(keys);
4847 }
4848 }
4849 if applicable.is_empty() {
4850 drop(lookup);
4851 drop(_query);
4852 drop(inner);
4853 query_execution_diagnostics::record_index_miss();
4854 return Ok(None);
4855 }
4856 applicable.sort_by_key(|keys| keys.len());
4860 drop(lookup);
4861 let (smallest, rest) = applicable.split_first().expect("non-empty");
4862 let bucket = inner.rows.get(capability);
4863 for key in smallest.iter() {
4864 if !rest.iter().all(|keys| keys.contains(key)) {
4865 continue;
4866 }
4867 examined += 1;
4868 let retrieval =
4869 workload_diagnostics::span(workload_diagnostics::Phase::CandidateRetrieval);
4870 let row = bucket.and_then(|rows| rows.get(key));
4871 drop(retrieval);
4872 let Some(row) = row else {
4873 continue;
4877 };
4878 evaluated += 1;
4879 let _predicate =
4880 workload_diagnostics::span(workload_diagnostics::Phase::PredicateEvaluation);
4881 if predicate(row) {
4882 drop(_predicate);
4883 matched.push(row.clone());
4884 }
4885 }
4886 }
4887 query_execution_diagnostics::record_indexed_query(examined, evaluated, matched.len());
4888 Ok(Some(matched))
4889 }
4890
4891 pub fn list_collection_page(
4893 &self,
4894 capability: &str,
4895 after: Option<&str>,
4896 limit: usize,
4897 ) -> Result<Vec<StoredRow>> {
4898 let inner = self.state();
4899 let Some(rows) = inner.rows.get(capability) else {
4900 return Ok(vec![]);
4901 };
4902 let values: Box<dyn Iterator<Item = &StoredRow>> = match after {
4903 Some(key) => Box::new(
4904 rows.range::<str, _>((Excluded(key), Unbounded))
4905 .map(|(_, row)| row),
4906 ),
4907 None => Box::new(rows.values()),
4908 };
4909 Ok(values
4910 .filter(|row| !row.deleted)
4911 .take(limit)
4912 .cloned()
4913 .collect())
4914 }
4915
4916 pub fn get_collection_record(&self, capability: &str, key: &str) -> Result<Option<StoredRow>> {
4918 let inner = self.state();
4919 Ok(inner
4920 .rows
4921 .get(capability)
4922 .and_then(|rows| rows.get(key))
4923 .filter(|row| !row.deleted)
4924 .cloned())
4925 }
4926
4927 pub fn get_collection_records<'a>(
4929 &self,
4930 capability: &str,
4931 keys: impl IntoIterator<Item = &'a str>,
4932 ) -> Result<Vec<StoredRow>> {
4933 let inner = self.state();
4934 let Some(rows) = inner.rows.get(capability) else {
4935 return Ok(Vec::new());
4936 };
4937 Ok(keys
4938 .into_iter()
4939 .filter_map(|key| rows.get(key))
4940 .filter(|row| !row.deleted)
4941 .cloned()
4942 .collect())
4943 }
4944
4945 pub fn subscribe_changes(&self) -> broadcast::Receiver<ChangeEvent> {
4947 self.event_tx.subscribe()
4948 }
4949
4950 pub fn add_sync_peer(&self, peer_id: String) -> Result<()> {
4952 let mut inner = self.state();
4953 inner.sync_state.add_peer(peer_id);
4954 Ok(())
4955 }
4956
4957 pub fn remove_sync_peer(&self, peer_id: &str) -> Result<()> {
4959 let mut inner = self.state();
4960 inner.sync_state.remove_peer(peer_id);
4961 Ok(())
4962 }
4963
4964 pub fn get_pending_for_peer(
4966 &self,
4967 _peer_id: &str,
4968 since_sequence: u64,
4969 ) -> Result<Vec<Operation>> {
4970 let inner = self.state();
4971 let pending: Vec<Operation> = inner
4972 .change_log
4973 .pending_operations()
4974 .into_iter()
4975 .filter(|op| op.sequence > since_sequence)
4976 .collect();
4977 Ok(pending)
4978 }
4979
4980 pub fn operations_since(
4982 &self,
4983 versions: &std::collections::HashMap<String, u64>,
4984 ) -> Result<Vec<Operation>> {
4985 let inner = self.state();
4986 Ok(inner.change_log.operations_since(versions))
4987 }
4988
4989 pub fn operation_versions(&self) -> Result<std::collections::HashMap<String, u64>> {
4991 let inner = self.state();
4992 Ok(inner.change_log.versions())
4993 }
4994
4995 pub fn export_snapshot(&self) -> Result<DatabaseSnapshot> {
4996 let inner = self.state();
4997 let mut rows: Vec<StoredRow> = inner
4998 .rows
4999 .values()
5000 .flat_map(|bucket| bucket.values().cloned())
5001 .collect();
5002 for row in &mut rows {
5003 row.operation = None;
5004 }
5005 rows.sort_by(|left, right| left.key.cmp(&right.key));
5006 let versions = inner.change_log.versions();
5007 let content_hash = DatabaseSnapshot::compute_hash(&rows, &versions)?;
5008 Ok(DatabaseSnapshot {
5009 rows,
5010 versions,
5011 content_hash,
5012 format_version: DURABLE_FORMAT_VERSION,
5013 })
5014 }
5015 pub fn state_rows(&self) -> Result<Vec<StoredRow>> {
5019 let inner = self.state();
5020 Ok(inner
5021 .rows
5022 .values()
5023 .flat_map(|bucket| bucket.values().cloned())
5024 .collect())
5025 }
5026
5027 pub fn state_rows_for_namespace(&self, state_namespace: &str) -> Result<Vec<StoredRow>> {
5033 let _span = workload_diagnostics::span(workload_diagnostics::Phase::NamespaceSnapshot);
5034 let inner = self.state();
5035 let prefix = format!("{state_namespace}:");
5036 Ok(inner
5037 .rows
5038 .iter()
5039 .filter(|(capability, _)| capability.starts_with(&prefix))
5040 .flat_map(|(_, bucket)| bucket.values().cloned())
5041 .collect())
5042 }
5043
5044 pub fn can_install_snapshot(&self) -> Result<bool> {
5045 let inner = self.state();
5046 Ok(!inner.bootstrapped && inner.rows.is_empty() && inner.change_log.pending.is_empty())
5047 }
5048
5049 pub fn install_snapshot(&self, snapshot: DatabaseSnapshot) -> Result<()> {
5052 if snapshot.format_version != DURABLE_FORMAT_VERSION {
5056 return Err(FlowError::IncompatibleFormat(Box::new(
5057 FormatIncompatibility {
5058 found: if snapshot.format_version == 0 {
5059 DurableFormat::Unversioned
5060 } else {
5061 DurableFormat::Versioned(snapshot.format_version)
5062 },
5063 required: DURABLE_FORMAT_VERSION,
5064 reason: "the snapshot was produced in a different durable format".to_string(),
5065 action: "Take a fresh snapshot from a FeltDB build matching this one."
5066 .to_string(),
5067 },
5068 )));
5069 }
5070 snapshot.verify()?;
5071 let mut inner = self.state();
5072 refuse_if_faulted(&inner)?;
5073 if !inner.rows.is_empty() || !inner.change_log.pending.is_empty() {
5074 return Err(FlowError::CapabilityError(
5075 "snapshot bootstrap requires a pristine node".to_string(),
5076 ));
5077 }
5078 for row in &snapshot.rows {
5079 if row.deleted
5080 || row.operation.is_some()
5081 || !row.key.starts_with(&format!("{}:", row.capability))
5082 {
5083 return Err(FlowError::CapabilityError(
5084 "snapshot contains an invalid state row".to_string(),
5085 ));
5086 }
5087 }
5088
5089 let temporary = sibling(&inner.path, "snapshot.tmp");
5090 let mut file = OpenOptions::new()
5091 .create(true)
5092 .write(true)
5093 .truncate(true)
5094 .open(&temporary)?;
5095 write_json_line(
5096 &mut file,
5097 &LogHeader {
5098 record_type: "feltdb.snapshot.v1".to_string(),
5099 format_version: DURABLE_FORMAT_VERSION,
5100 local_sequence: inner.sequence,
5101 observed_versions: snapshot.versions.clone(),
5102 transaction_history: Vec::new(),
5103 transaction_payload_hashes: HashMap::new(),
5104 authority_revision: 0,
5106 },
5107 )?;
5108 for row in &snapshot.rows {
5109 write_json_line(&mut file, row)?;
5110 }
5111 file.flush()?;
5112 file.sync_all()?;
5113 fs::rename(temporary, &inner.path)?;
5114 sync_parent_directory(&inner.path)?;
5115
5116 inner.clear_rows();
5117 inner.key_counters.clear();
5118 for row in snapshot.rows {
5119 if let Some(id) = row
5120 .key
5121 .rsplit_once(':')
5122 .and_then(|(_, right)| right.parse::<u64>().ok())
5123 {
5124 let counter = inner
5125 .key_counters
5126 .entry(row.capability.clone())
5127 .or_default();
5128 *counter = (*counter).max(id + 1);
5129 }
5130 inner.put_row(row);
5131 }
5132 inner.change_log.observed_versions = snapshot.versions;
5133 inner.bootstrapped = true;
5134 persist_sync_metadata(&inner)
5135 }
5136
5137 pub fn acknowledge_peer_operations(&self, peer_id: String, sequence: u64) -> Result<()> {
5139 let mut inner = self.state();
5140 inner.change_log.acknowledge_peer(peer_id, sequence);
5141 persist_sync_metadata(&inner)?;
5142 Ok(())
5143 }
5144
5145 pub fn acknowledge_peer_versions(
5147 &self,
5148 peer_id: String,
5149 versions: HashMap<String, u64>,
5150 ) -> Result<()> {
5151 let mut inner = self.state();
5152 inner.change_log.acknowledge_versions(peer_id, versions);
5153 persist_sync_metadata(&inner)
5154 }
5155
5156 pub fn acknowledged_peer_versions(&self, peer_id: &str) -> Result<HashMap<String, u64>> {
5157 let inner = self.state();
5158 Ok(inner.change_log.acknowledged_versions(peer_id))
5159 }
5160
5161 pub fn recent_transactions(&self, limit: usize) -> Result<Vec<TransactionAttribution>> {
5169 let inner = self.state();
5170 Ok(inner
5171 .transaction_attributions
5172 .iter()
5173 .rev()
5174 .take(limit)
5175 .cloned()
5176 .collect())
5177 }
5178
5179 pub fn transaction_attribution(
5182 &self,
5183 transaction_id: &str,
5184 ) -> Result<Option<TransactionAttribution>> {
5185 let inner = self.state();
5186 Ok(inner
5187 .transaction_history
5188 .iter()
5189 .rev()
5190 .find(|attribution| attribution.transaction_id == transaction_id)
5191 .cloned())
5192 }
5193
5194 pub fn committed_transactions_after(
5197 &self,
5198 commit_revision: u64,
5199 limit: usize,
5200 ) -> Result<Vec<TransactionAttribution>> {
5201 let inner = self.state();
5202 Ok(inner
5203 .transaction_history
5204 .iter()
5205 .filter(|transaction| transaction.commit_revision > commit_revision)
5206 .take(limit)
5207 .cloned()
5208 .collect())
5209 }
5210
5211 pub fn transactions_touching(
5217 &self,
5218 capability: &str,
5219 key: &str,
5220 limit: usize,
5221 ) -> Result<Vec<TransactionAttribution>> {
5222 let inner = self.state();
5223 Ok(inner
5224 .transaction_history
5225 .iter()
5226 .rev()
5227 .filter(|attribution| attribution.touched(capability, key))
5228 .take(limit)
5229 .cloned()
5230 .collect())
5231 }
5232
5233 pub fn transaction_attribution_window(&self) -> Result<(usize, usize)> {
5238 let inner = self.state();
5239 Ok((
5240 inner.transaction_attributions.len(),
5241 TRANSACTION_ATTRIBUTION_WINDOW,
5242 ))
5243 }
5244
5245 pub fn retained_operation_floors(&self) -> Result<HashMap<String, u64>> {
5246 let inner = self.state();
5247 Ok(inner.change_log.retained_from())
5248 }
5249
5250 pub fn acknowledged_operation_count(&self, active_peers: &[String]) -> Result<usize> {
5251 let inner = self.state();
5252 Ok(inner.change_log.acknowledged_operation_count(active_peers))
5253 }
5254
5255 pub fn compact_operation_log(&self, active_peers: &[String]) -> Result<usize> {
5258 self.compact_internal(active_peers, true)
5259 .map(|outcome| outcome.pruned())
5260 }
5261
5262 pub fn maybe_compact_operation_log(
5275 &self,
5276 active_peers: &[String],
5277 ) -> Result<CompactionOutcome> {
5278 self.compact_internal(active_peers, false)
5279 }
5280
5281 pub fn compaction_policy(&self) -> CompactionPolicy {
5283 self.state().compaction_policy
5284 }
5285
5286 pub fn set_compaction_policy(&self, policy: CompactionPolicy) {
5288 self.state().compaction_policy = policy;
5289 }
5290
5291 fn compact_internal(&self, active_peers: &[String], force: bool) -> Result<CompactionOutcome> {
5292 let mut inner = self.state();
5293 refuse_if_faulted(&inner)?;
5296 let standalone = active_peers.is_empty() && !inner.change_log.has_peer_history();
5300 let removed = if standalone {
5301 inner.change_log.prune_standalone(STANDALONE_OPERATION_WINDOW)
5302 } else {
5303 inner.change_log.prune_acknowledged(active_peers)
5304 };
5305 if removed == 0 {
5306 return Ok(CompactionOutcome::Idle);
5307 }
5308 persist_sync_metadata(&inner)?;
5309 inner.pruned_since_rewrite += removed;
5310
5311 let log_bytes = fs::metadata(&inner.path).map(|m| m.len()).unwrap_or(0);
5317 let doubled = log_bytes >= inner.log_bytes_after_rewrite.saturating_mul(2);
5318 if !force
5319 && (inner.pruned_since_rewrite < inner.compaction_policy.rewrite_after_pruned
5320 || (standalone && !doubled))
5321 {
5322 return Ok(CompactionOutcome::Deferred {
5323 pruned: removed,
5324 pending_since_rewrite: inner.pruned_since_rewrite,
5325 });
5326 }
5327
5328 rewrite_log_locked(&mut inner)?;
5329 inner.bootstrapped = true;
5330 inner.pruned_since_rewrite = 0;
5331 inner.log_bytes_after_rewrite = fs::metadata(&inner.path).map(|m| m.len()).unwrap_or(0);
5332 Ok(CompactionOutcome::Rewritten { pruned: removed })
5333 }
5334
5335 pub fn register_peer(&self, advertisement: PeerAdvertisement) -> Result<()> {
5337 let inner = self.state();
5338 inner
5339 .peer_registry
5340 .register(advertisement)
5341 .map_err(|e| FlowError::CapabilityError(e))
5342 }
5343
5344 pub fn get_peer(&self, peer_id: &PeerId) -> Result<Option<PeerAdvertisement>> {
5346 let inner = self.state();
5347 inner
5348 .peer_registry
5349 .get(peer_id)
5350 .map_err(|e| FlowError::CapabilityError(e))
5351 }
5352
5353 pub fn peers_for_namespace(&self, namespace: &str) -> Result<Vec<PeerAdvertisement>> {
5355 let inner = self.state();
5356 inner
5357 .peer_registry
5358 .peers_for_namespace(namespace)
5359 .map_err(|e| FlowError::CapabilityError(e))
5360 }
5361
5362 pub fn peers_for_capability(&self, capability: &str) -> Result<Vec<PeerAdvertisement>> {
5364 let inner = self.state();
5365 inner
5366 .peer_registry
5367 .peers_for_capability(capability)
5368 .map_err(|e| FlowError::CapabilityError(e))
5369 }
5370
5371 pub fn all_peers(&self) -> Result<Vec<PeerAdvertisement>> {
5373 let inner = self.state();
5374 inner
5375 .peer_registry
5376 .all_peers()
5377 .map_err(|e| FlowError::CapabilityError(e))
5378 }
5379
5380 pub fn resolve_ref<T: DeserializeOwned>(&self, flow_ref: &FlowRef) -> Result<Option<T>> {
5382 let key = format!("{}:{}", flow_ref.namespace, flow_ref.object_id);
5384 self.get::<T>(&key)
5385 }
5386
5387 pub fn sync_state(&self) -> Result<SyncState> {
5389 let inner = self.state();
5390 Ok(inner.sync_state.clone())
5391 }
5392
5393 pub fn apply_remote_operation(&self, op: Operation) -> Result<()> {
5396 let mut inner = self.state();
5397
5398 if op.content_hash != op.compute_content_hash() {
5399 return Err(FlowError::CapabilityError(
5400 "remote operation content hash mismatch".to_string(),
5401 ));
5402 }
5403 if inner.change_log.has_operation(&op.instance_id, op.sequence) {
5404 return Ok(());
5405 }
5406 let expected = inner
5407 .change_log
5408 .versions()
5409 .get(&op.instance_id)
5410 .copied()
5411 .unwrap_or(0)
5412 + 1;
5413 if op.sequence != expected {
5414 return Err(FlowError::CapabilityError(format!(
5415 "remote operation sequence gap for {}: expected {}, received {}",
5416 op.instance_id, expected, op.sequence
5417 )));
5418 }
5419
5420 if let Some(remote_clock) = &op.vector_clock {
5422 inner.sync_state.merge_vector_clock(remote_clock);
5423 }
5424
5425 let local_op_for_key = inner
5427 .change_log
5428 .pending_operations()
5429 .into_iter()
5430 .find(|local_op| local_op.key == op.key && local_op.capability == op.capability);
5431
5432 let op_to_apply = if let Some(local_op) = local_op_for_key {
5434 let local_vc = local_op.vector_clock.clone().unwrap_or_default();
5437 let remote_vc = op.vector_clock.clone().unwrap_or_default();
5438
5439 let local_history = CausalHistory::new(
5440 StateVersion::from_clocks(local_vc, local_op.timestamp_ms),
5441 local_op.instance_id.clone(),
5442 local_op.sequence,
5443 );
5444
5445 let remote_history = CausalHistory::new(
5446 StateVersion::from_clocks(remote_vc, op.timestamp_ms),
5447 op.instance_id.clone(),
5448 op.sequence,
5449 );
5450
5451 let resolution = ConflictResolution::resolve_lww(&[local_history, remote_history]);
5453
5454 if resolution.winner_index == Some(1) {
5456 op.clone()
5457 } else {
5458 if let Some(mut history_row) = inner
5461 .rows
5462 .get(&op.capability)
5463 .and_then(|rows| rows.get(&op.key))
5464 .cloned()
5465 {
5466 history_row.operation = Some(op.clone());
5467 append_event_locked(&mut inner, &history_row)?;
5468 inner.authority_revision += 1;
5469 }
5470 inner.change_log.add_operation(op);
5471 inner.sync_state.increment_received(1);
5472 return Ok(());
5473 }
5474 } else {
5475 op.clone()
5476 };
5477
5478 let row = StoredRow {
5480 capability: op_to_apply.capability.clone(),
5481 key: op_to_apply.key.clone(),
5482 rust_type: op_to_apply.rust_type.clone(),
5483 value: op_to_apply.value.clone().unwrap_or(serde_json::Value::Null),
5484 unix_ms: op_to_apply.timestamp_ms,
5485 content_hash: Some(op_to_apply.content_hash.clone()),
5486 flow_ref: None,
5487 deleted: op_to_apply.op_type == OperationType::Delete,
5488 operation: Some(op.clone()),
5489 };
5490
5491 match op_to_apply.op_type {
5492 OperationType::Insert | OperationType::Update => {
5493 append_event_locked(&mut inner, &row)?;
5494 inner.authority_revision += 1;
5495 inner.put_row(row);
5496 }
5497 OperationType::Delete => {
5498 append_event_locked(&mut inner, &row)?;
5499 inner.authority_revision += 1;
5500 inner.remove_row(&op_to_apply.capability, &op_to_apply.key);
5501 }
5502 }
5503 if let Some(id) = op_to_apply
5504 .key
5505 .rsplit_once(':')
5506 .and_then(|(_, right)| right.parse::<u64>().ok())
5507 {
5508 let counter = inner
5509 .key_counters
5510 .entry(op_to_apply.capability.clone())
5511 .or_default();
5512 *counter = (*counter).max(id + 1);
5513 }
5514
5515 if let Some(provenance) = op.revision.clone() {
5525 if let Some(value) = op.value.clone() {
5526 let revision = reconstruct_revision(&provenance, &value)?;
5527 persist_revision_locked(&mut inner, revision)?;
5528 }
5529 }
5530
5531 inner.change_log.add_operation(op);
5532 inner.sync_state.increment_received(1);
5533 let event = ChangeEvent {
5534 capability: op_to_apply.capability,
5535 key: op_to_apply.key,
5536 rust_type: op_to_apply.rust_type,
5537 unix_ms: op_to_apply.timestamp_ms,
5538 };
5539 drop(inner);
5540 let _ = self.event_tx.send(event);
5541
5542 Ok(())
5543 }
5544
5545 pub fn get_vector_clock(&self) -> Result<Option<std::collections::HashMap<String, u64>>> {
5547 let inner = self.state();
5548 Ok(inner.sync_state.vector_clock.clone())
5549 }
5550}
5551
5552#[derive(Clone)]
5553pub struct Capability {
5554 db: FeltDb,
5555 name: String,
5556}
5557
5558impl Capability {
5559 pub fn insert<T: Serialize>(&self, value: T) -> Result<String> {
5560 let key = {
5561 let mut inner = self.db.state();
5562 let next = inner.key_counters.entry(self.name.clone()).or_default();
5563 let key = format!("{}:{}", self.name, *next);
5564 *next += 1;
5565 key
5566 };
5567 self.db
5568 .insert_internal(self.name.clone(), key.clone(), value, false)?;
5569 Ok(key)
5570 }
5571
5572 pub fn insert_with_key<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
5573 let full_key = if key.contains(':') {
5574 key.to_string()
5575 } else {
5576 format!("{}:{key}", self.name)
5577 };
5578 self.db
5579 .insert_internal(self.name.clone(), full_key, value, false)
5580 }
5581
5582 pub fn update<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
5583 let full_key = if key.contains(':') {
5584 key.to_string()
5585 } else {
5586 format!("{}:{key}", self.name)
5587 };
5588 self.db.update_internal(self.name.clone(), full_key, value)
5589 }
5590
5591 pub fn delete(&self, key: &str) -> Result<()> {
5592 let full_key = if key.contains(':') {
5593 key.to_string()
5594 } else {
5595 format!("{}:{key}", self.name)
5596 };
5597 self.db.delete_internal(self.name.clone(), full_key)
5598 }
5599
5600 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
5601 let full_key = if key.contains(':') {
5602 key.to_string()
5603 } else {
5604 format!("{}:{key}", self.name)
5605 };
5606 self.db.get(&full_key)
5607 }
5608
5609 pub fn find<T, F>(&self, predicate: F) -> Result<Vec<T>>
5610 where
5611 T: DeserializeOwned,
5612 F: Fn(&T) -> bool,
5613 {
5614 let rust_type = type_name::<T>().to_string();
5615 let mut inner = self.db.state();
5616 *inner
5617 .query_count_by_type
5618 .entry(rust_type.clone())
5619 .or_default() += 1;
5620 if inner.query_count_by_type[&rust_type] >= 3 {
5621 inner.adaptive_indexes.insert(rust_type.clone());
5622 }
5623
5624 let mut out = Vec::new();
5625 if let Some(bucket) = inner.rows.get(&self.name) {
5626 for row in bucket.values() {
5627 if row.rust_type != rust_type {
5628 continue;
5629 }
5630 let parsed = serde_json::from_value::<T>(row.value.clone())?;
5631 if predicate(&parsed) {
5632 out.push(parsed);
5633 }
5634 }
5635 }
5636 Ok(out)
5637 }
5638
5639 pub async fn live_find<T, F>(&self, predicate: F) -> Result<LiveCapabilitySubscription<T>>
5661 where
5662 T: DeserializeOwned + Send + Sync + 'static,
5663 F: Fn(&T) -> bool + Send + Sync + 'static,
5664 {
5665 let rust_type = type_name::<T>().to_string();
5666 let capability_name = self.name.clone();
5667
5668 let initial_results = {
5669 let mut inner = self.db.state();
5670 *inner
5671 .query_count_by_type
5672 .entry(rust_type.clone())
5673 .or_default() += 1;
5674 if inner.query_count_by_type[&rust_type] >= 3 {
5675 inner.adaptive_indexes.insert(rust_type.clone());
5676 }
5677
5678 let mut out = Vec::new();
5679 if let Some(bucket) = inner.rows.get(&capability_name) {
5680 for row in bucket.values() {
5681 if row.rust_type != rust_type {
5682 continue;
5683 }
5684 let parsed = serde_json::from_value::<T>(row.value.clone())?;
5685 if predicate(&parsed) {
5686 out.push(parsed);
5687 }
5688 }
5689 }
5690 out
5691 };
5692
5693 let rx = self.db.event_tx.subscribe();
5694 let subscription = LiveCapabilitySubscription {
5695 initial_results,
5696 rx,
5697 db: self.db.clone(),
5698 capability_name,
5699 rust_type,
5700 predicate: Arc::new(Mutex::new(None)),
5701 };
5702
5703 Ok(subscription)
5704 }
5705}
5706
5707type PredicateBox<T> = Arc<Mutex<Option<Box<dyn Fn(&T) -> bool + Send + Sync>>>>;
5709
5710pub struct LiveQuerySubscription<T> {
5715 pub initial_results: Vec<T>,
5717 rx: broadcast::Receiver<ChangeEvent>,
5718 db: FeltDb,
5719 rust_type: String,
5720 predicate: PredicateBox<T>,
5721}
5722
5723impl<T> LiveQuerySubscription<T>
5724where
5725 T: DeserializeOwned + Send + Sync + 'static,
5726{
5727 pub fn set_predicate<F>(&mut self, predicate: F)
5728 where
5729 F: Fn(&T) -> bool + Send + Sync + 'static,
5730 {
5731 *self.predicate.lock().expect("lock poisoned") = Some(Box::new(predicate));
5732 }
5733
5734 pub async fn next(&mut self) -> Option<Vec<T>> {
5742 loop {
5743 match self.rx.recv().await {
5744 Ok(event) => {
5745 if event.rust_type != self.rust_type {
5746 continue;
5747 }
5748
5749 let results = {
5750 let inner = self.db.state();
5751 let mut out = Vec::new();
5752 for bucket in inner.rows.values() {
5753 for row in bucket.values() {
5754 if row.rust_type != self.rust_type {
5755 continue;
5756 }
5757 if let Ok(parsed) = serde_json::from_value::<T>(row.value.clone()) {
5758 out.push(parsed);
5759 }
5760 }
5761 }
5762 out
5763 };
5764
5765 return Some(results);
5766 }
5767 Err(_) => {
5768 return None;
5769 }
5770 }
5771 }
5772 }
5773
5774 pub fn initial_results(&self) -> &[T] {
5779 &self.initial_results
5780 }
5781}
5782
5783pub struct LiveCapabilitySubscription<T> {
5788 pub initial_results: Vec<T>,
5789 rx: broadcast::Receiver<ChangeEvent>,
5790 db: FeltDb,
5791 capability_name: String,
5792 rust_type: String,
5793 predicate: PredicateBox<T>,
5794}
5795
5796impl<T> LiveCapabilitySubscription<T>
5797where
5798 T: DeserializeOwned + Send + Sync + 'static,
5799{
5800 pub fn set_predicate<F>(&mut self, predicate: F)
5801 where
5802 F: Fn(&T) -> bool + Send + Sync + 'static,
5803 {
5804 *self.predicate.lock().expect("lock poisoned") = Some(Box::new(predicate));
5805 }
5806
5807 pub async fn next(&mut self) -> Option<Vec<T>> {
5812 loop {
5813 match self.rx.recv().await {
5814 Ok(event) => {
5815 if event.rust_type != self.rust_type || event.capability != self.capability_name
5816 {
5817 continue;
5818 }
5819
5820 let results = {
5821 let inner = self.db.state();
5822 let mut out = Vec::new();
5823 if let Some(bucket) = inner.rows.get(&self.capability_name) {
5824 for row in bucket.values() {
5825 if row.rust_type != self.rust_type {
5826 continue;
5827 }
5828 if let Ok(parsed) = serde_json::from_value::<T>(row.value.clone()) {
5829 out.push(parsed);
5830 }
5831 }
5832 }
5833 out
5834 };
5835
5836 return Some(results);
5837 }
5838 Err(_) => {
5839 return None;
5840 }
5841 }
5842 }
5843 }
5844
5845 pub fn initial_results(&self) -> &[T] {
5846 &self.initial_results
5847 }
5848}
5849
5850pub fn inspect_durable_format(path: &Path) -> Result<FormatCompatibility> {
5869 if !path.exists() {
5870 return Ok(FormatCompatibility::Compatible(DurableFormat::Versioned(
5873 DURABLE_FORMAT_VERSION,
5874 )));
5875 }
5876
5877 #[derive(Deserialize)]
5881 struct FormatProbe {
5882 #[serde(default)]
5883 record_type: Option<Value>,
5884 #[serde(default)]
5885 format_version: Option<Value>,
5886 #[serde(default)]
5887 key: Option<Value>,
5888 }
5889 let mut declared: Option<u32> = None;
5890 let mut legacy_revision = false;
5891 visit_log(path, |line| {
5892 if line.text.trim().is_empty() {
5893 return Ok(());
5894 }
5895 let Ok(probe) = serde_json::from_str::<FormatProbe>(&line.text) else {
5896 return Ok(());
5897 };
5898 match probe.record_type.as_ref().and_then(Value::as_str) {
5899 Some(FORMAT_RECORD_TYPE) => {
5900 if let Some(version) = probe.format_version.as_ref().and_then(Value::as_u64) {
5901 declared = Some(version as u32);
5902 }
5903 }
5904 Some("feltdb.snapshot.v1" | "flowdb.snapshot.v1") => {
5905 match probe.format_version.as_ref().and_then(Value::as_u64) {
5906 Some(version) if version > 0 => declared = Some(version as u32),
5907 _ => {}
5908 }
5909 }
5910 _ => {
5911 let is_revision = probe
5916 .key
5917 .as_ref()
5918 .and_then(Value::as_str)
5919 .is_some_and(|key| key.starts_with("state:revision:"));
5920 if is_revision
5921 && serde_json::from_str::<Value>(&line.text).is_ok_and(|value| {
5922 value
5923 .get("value")
5924 .and_then(Value::as_object)
5925 .is_some_and(|revision| !revision.contains_key("resource"))
5926 })
5927 {
5928 legacy_revision = true;
5929 }
5930 }
5931 }
5932 Ok(())
5933 })?;
5934
5935 if let Some(version) = declared {
5936 return Ok(match version.cmp(&DURABLE_FORMAT_VERSION) {
5937 std::cmp::Ordering::Equal => {
5938 FormatCompatibility::Compatible(DurableFormat::Versioned(version))
5939 }
5940 std::cmp::Ordering::Greater => {
5941 FormatCompatibility::Incompatible(FormatIncompatibility {
5942 found: DurableFormat::Versioned(version),
5943 required: DURABLE_FORMAT_VERSION,
5944 reason: "the database was written by a newer FeltDB".to_string(),
5945 action: "Run a FeltDB build that understands this format, or restore \
5946 a backup taken in an older format."
5947 .to_string(),
5948 })
5949 }
5950 std::cmp::Ordering::Less => FormatCompatibility::Incompatible(FormatIncompatibility {
5951 found: DurableFormat::Versioned(version),
5952 required: DURABLE_FORMAT_VERSION,
5953 reason: "the database was written in an older durable format".to_string(),
5954 action: "Migrate the database with a FeltDB build that supports both \
5955 formats, or restore a backup."
5956 .to_string(),
5957 }),
5958 });
5959 }
5960
5961 if legacy_revision {
5962 return Ok(FormatCompatibility::Incompatible(FormatIncompatibility {
5963 found: DurableFormat::Unversioned,
5964 required: DURABLE_FORMAT_VERSION,
5965 reason: "it holds revision records written before revisions belonged to a \
5966 resource, and those records cannot be migrated without inventing \
5967 information they never contained: a revision recorded no resource, \
5968 so which history each one belongs to is not recoverable from the data"
5969 .to_string(),
5970 action: "Export the application state you need from a FeltDB build that \
5971 understands the old format, then load it into a new database. The \
5972 existing file has not been modified."
5973 .to_string(),
5974 }));
5975 }
5976
5977 Ok(FormatCompatibility::Compatible(DurableFormat::Unversioned))
5980}
5981
5982struct ScannedLine {
5984 text: String,
5985 line_number: usize,
5986 byte_offset: u64,
5987}
5988
5989#[derive(Clone)]
5991struct TornTail {
5992 byte_offset: u64,
5993 bytes: usize,
5994}
5995
5996fn visit_log(
6011 path: &Path,
6012 mut visit: impl FnMut(&ScannedLine) -> Result<()>,
6013) -> Result<Option<TornTail>> {
6014 let mut file = BufReader::with_capacity(1 << 20, OpenOptions::new().read(true).open(path)?);
6015 let mut buffer = Vec::new();
6016 let mut offset = 0u64;
6017 let mut line_number = 0usize;
6018
6019 loop {
6020 buffer.clear();
6021 let read = file.read_until(b'\n', &mut buffer)?;
6022 if read == 0 {
6023 return Ok(None);
6024 }
6025 line_number += 1;
6026 if !buffer.ends_with(b"\n") {
6027 let blank = buffer.iter().all(u8::is_ascii_whitespace);
6030 return Ok((!blank).then_some(TornTail {
6031 byte_offset: offset,
6032 bytes: read,
6033 }));
6034 }
6035 buffer.pop();
6036 let line = ScannedLine {
6037 text: match String::from_utf8(std::mem::take(&mut buffer)) {
6038 Ok(text) => text,
6039 Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
6040 },
6041 line_number,
6042 byte_offset: offset,
6043 };
6044 visit(&line)?;
6045 buffer = line.text.into_bytes();
6046 offset += read as u64;
6047 }
6048}
6049
6050#[derive(Deserialize)]
6053struct RecordProbe {
6054 #[serde(default)]
6055 record_type: Option<Value>,
6056}
6057
6058fn unreadable(line: &ScannedLine) -> FlowError {
6061 if serde_json::from_str::<serde::de::IgnoredAny>(&line.text).is_ok() {
6062 corruption(line, "not a durable record this build understands")
6063 } else {
6064 corruption(line, "not valid JSON")
6065 }
6066}
6067
6068fn truncate_torn_tail(path: &Path, tail: &TornTail) -> Result<()> {
6074 let file = OpenOptions::new().write(true).open(path)?;
6075 file.set_len(tail.byte_offset)?;
6076 file.sync_all()?;
6077 Ok(())
6078}
6079
6080fn corruption(line: &ScannedLine, reason: &str) -> FlowError {
6081 let excerpt: String = line.text.chars().take(120).collect();
6082 FlowError::CorruptLogLine(Box::new(LogCorruption {
6083 line_number: line.line_number,
6084 byte_offset: line.byte_offset,
6085 reason: reason.to_string(),
6086 excerpt,
6087 }))
6088}
6089
6090fn replay_log(inner: &mut Inner) -> Result<()> {
6091 let path = inner.path.clone();
6092 let mut snapshot_log = false;
6093 let mut snapshot_authority = false;
6094 let mut snapshot_versions: HashMap<String, u64> = HashMap::new();
6095 let mut recoverable_revisions: HashMap<String, (operation::RevisionProvenance, Value)> =
6096 HashMap::new();
6097 let torn_tail = visit_log(&path, |line| {
6098 if line.text.trim().is_empty() {
6099 return Ok(());
6100 }
6101 let probe: RecordProbe =
6102 serde_json::from_str(&line.text).map_err(|_| unreadable(line))?;
6103 let record_type = probe.record_type.as_ref().and_then(Value::as_str);
6104 if matches!(record_type, Some("feltdb.snapshot.v1" | "flowdb.snapshot.v1")) {
6105 let header: LogHeader = serde_json::from_str(&line.text)
6106 .map_err(|error| corruption(line, &format!("malformed log header: {error}")))?;
6107 inner.sequence = inner.sequence.max(header.local_sequence);
6108 if header.authority_revision > 0 {
6112 inner.authority_revision = inner.authority_revision.max(header.authority_revision);
6113 snapshot_authority = true;
6114 }
6115 snapshot_versions = header.observed_versions;
6116 for attribution in header.transaction_history {
6117 let attribution = normalize_transaction_attribution(attribution);
6118 inner.authority_revision =
6119 inner.authority_revision.max(attribution.commit_revision);
6120 inner.transaction_revisions.insert(
6121 attribution.transaction_id.clone(),
6122 (attribution.base_revision, attribution.commit_revision),
6123 );
6124 inner
6125 .applied_transactions
6126 .insert(attribution.transaction_id.clone());
6127 remember_attribution(inner, attribution);
6128 }
6129 inner
6130 .transaction_payload_hashes
6131 .extend(header.transaction_payload_hashes);
6132 inner.bootstrapped = true;
6133 snapshot_log = true;
6134 return Ok(());
6135 }
6136 if record_type == Some(FORMAT_RECORD_TYPE) {
6137 return Ok(());
6139 }
6140 if record_type == Some("feltdb.transaction.v1") {
6141 let transaction: TransactionLogRecord =
6142 serde_json::from_str(&line.text).map_err(|error| {
6143 corruption(line, &format!("malformed transaction record: {error}"))
6144 })?;
6145 inner.authority_revision =
6148 inner
6149 .authority_revision
6150 .max(if transaction.commit_revision == 0 {
6151 inner.authority_revision + 1
6152 } else {
6153 transaction.commit_revision
6154 });
6155 let base_revision = if transaction.commit_revision == 0 {
6156 inner.authority_revision.saturating_sub(1)
6157 } else {
6158 transaction.base_revision
6159 };
6160 inner.transaction_revisions.insert(
6161 transaction.transaction_id.clone(),
6162 (base_revision, inner.authority_revision),
6163 );
6164 if let Some(hash) = transaction.payload_hash.clone() {
6165 inner
6166 .transaction_payload_hashes
6167 .insert(transaction.transaction_id.clone(), hash);
6168 }
6169 let attribution = TransactionAttribution::from_record(
6173 &transaction.transaction_id,
6174 transaction.audit.as_ref(),
6175 base_revision,
6176 inner.authority_revision,
6177 transaction.state_before,
6178 transaction.state_after,
6179 &transaction.rows,
6180 transaction
6181 .rows
6182 .first()
6183 .map(|row| row.unix_ms)
6184 .unwrap_or_default(),
6185 );
6186 remember_attribution(inner, attribution);
6187 if inner
6188 .applied_transactions
6189 .insert(transaction.transaction_id)
6190 {
6191 for row in transaction.rows {
6192 if let Some(operation) = row.operation.clone() {
6193 inner.sequence = inner.sequence.max(operation.sequence);
6194 if let Some(clock) = &operation.vector_clock {
6195 inner.sync_state.merge_vector_clock(clock);
6196 }
6197 inner.change_log.add_operation(operation);
6198 }
6199 if row.deleted {
6200 inner.remove_row(&row.capability, &row.key);
6201 } else {
6202 inner.put_row(row);
6203 }
6204 }
6205 }
6206 inner.sequence = inner.sequence.max(transaction.state_after);
6207 return Ok(());
6208 }
6209 let row: StoredRow = serde_json::from_str(&line.text)
6210 .map_err(|_| corruption(line, "not a durable record this build understands"))?;
6211 let derived_revision = row.capability == "state" && row.operation.is_none();
6212 if derived_revision {
6213 if let Some(id) = row.key.strip_prefix("state:revision:") {
6214 recoverable_revisions.remove(id);
6215 }
6216 }
6217 let counted_by_header = snapshot_authority
6222 && row.operation.as_ref().is_none_or(|operation| {
6223 snapshot_versions.get(&operation.instance_id).copied().unwrap_or(0)
6224 >= operation.sequence
6225 });
6226 if let Some(operation) = row.operation.clone() {
6227 if let (Some(provenance), Some(value)) = (&operation.revision, &operation.value) {
6234 recoverable_revisions.insert(provenance.id.clone(), (provenance.clone(), value.clone()));
6235 }
6236 inner.sequence = inner.sequence.max(
6237 (operation.instance_id == inner.instance_id)
6238 .then_some(operation.sequence)
6239 .unwrap_or(0),
6240 );
6241 if let Some(clock) = &operation.vector_clock {
6242 inner.sync_state.merge_vector_clock(clock);
6243 }
6244 inner.change_log.add_operation(operation);
6245 } else if !snapshot_log && !derived_revision {
6246 inner.sequence += 1;
6249 }
6250 if !derived_revision && !counted_by_header {
6251 let base_revision = inner.authority_revision;
6252 inner.authority_revision += 1;
6253 if !snapshot_log || snapshot_authority {
6257 remember_operation_attribution(inner, &row, base_revision, base_revision + 1);
6258 }
6259 }
6260 if let Some(id) = row
6261 .key
6262 .rsplit_once(':')
6263 .and_then(|(_, right)| right.parse::<u64>().ok())
6264 {
6265 let counter = inner
6266 .key_counters
6267 .entry(row.capability.clone())
6268 .or_default();
6269 *counter = (*counter).max(id + 1);
6270 }
6271 if row.deleted {
6272 inner.remove_row(&row.capability, &row.key);
6273 } else {
6274 inner.put_row(row);
6275 }
6276 Ok(())
6277 })?;
6278 for (origin, sequence) in snapshot_versions {
6281 inner
6282 .change_log
6283 .observed_versions
6284 .entry(origin)
6285 .and_modify(|current| *current = (*current).max(sequence))
6286 .or_insert(sequence);
6287 }
6288
6289 for (provenance, value) in recoverable_revisions.into_values() {
6302 let key = format!("state:revision:{}", provenance.id);
6303 let present = inner
6304 .rows
6305 .get("state")
6306 .and_then(|bucket| bucket.get(&key))
6307 .is_some_and(|row| !row.deleted);
6308 if present {
6309 continue;
6310 }
6311
6312 let horizon = inner
6317 .rows
6318 .get("_retention")
6319 .and_then(|bucket| bucket.get(&format!("_retention:{}", provenance.resource)))
6320 .and_then(|row| serde_json::from_value::<RetentionRecord>(row.value.clone()).ok())
6321 .map(|state| state.horizon);
6322 if horizon.is_some_and(|horizon| provenance.sequence < horizon) {
6323 continue;
6324 }
6325 let Ok(revision) = reconstruct_revision(&provenance, &value) else {
6326 continue;
6327 };
6328 let Ok(encoded) = serde_json::to_value(&revision) else {
6329 continue;
6330 };
6331 inner.put_row(StoredRow {
6332 capability: "state".to_string(),
6333 key,
6334 rust_type: "feltdb::state_model::StateRevision".to_string(),
6335 value: encoded,
6336 unix_ms: now_ms(),
6337 content_hash: None,
6338 flow_ref: None,
6339 deleted: false,
6340 operation: None,
6341 });
6342 }
6343
6344 for (capability, bucket) in &inner.rows {
6346 let count = bucket.values().filter(|row| !row.deleted).count() as u64;
6347 inner
6348 .collection_cardinality
6349 .insert(capability.clone(), count);
6350 }
6351
6352 if let Some(tail) = torn_tail {
6356 truncate_torn_tail(&inner.path, &tail)?;
6357 inner.log_recovery = LogRecovery::RecoveredTornTail {
6358 byte_offset: tail.byte_offset,
6359 discarded_bytes: tail.bytes,
6360 };
6361 }
6362
6363 Ok(())
6364}
6365
6366fn write_json_line<T: Serialize>(file: &mut std::fs::File, value: &T) -> Result<()> {
6367 serde_json::to_writer(&mut *file, value)?;
6368 file.write_all(b"\n")?;
6369 Ok(())
6370}
6371
6372fn stored_row_for_operation(operation: Operation) -> StoredRow {
6373 StoredRow {
6374 capability: operation.capability.clone(),
6375 key: operation.key.clone(),
6376 rust_type: operation.rust_type.clone(),
6377 value: operation.value.clone().unwrap_or(Value::Null),
6378 unix_ms: operation.timestamp_ms,
6379 content_hash: Some(operation.content_hash.clone()),
6380 flow_ref: None,
6381 deleted: operation.op_type == OperationType::Delete,
6382 operation: Some(operation),
6383 }
6384}
6385
6386fn sync_metadata_path(path: &Path) -> PathBuf {
6387 path.with_extension("sync.json")
6388}
6389
6390fn load_sync_metadata(inner: &mut Inner) -> Result<()> {
6391 let path = sync_metadata_path(&inner.path);
6392 if !path.exists() {
6393 return Ok(());
6394 }
6395 let metadata: SyncMetadata = serde_json::from_slice(&fs::read(path)?)?;
6396 inner.sequence = inner.sequence.max(metadata.local_sequence);
6397 for (origin, sequence) in metadata.observed_versions {
6398 inner
6399 .change_log
6400 .observed_versions
6401 .entry(origin)
6402 .and_modify(|current| *current = (*current).max(sequence))
6403 .or_insert(sequence);
6404 }
6405 inner.change_log.peer_acknowledgements = metadata.peer_acknowledgements;
6406 Ok(())
6407}
6408
6409fn persist_sync_metadata(inner: &Inner) -> Result<()> {
6410 let path = sync_metadata_path(&inner.path);
6411 let temporary = path.with_extension("sync.tmp");
6412 let metadata = SyncMetadata {
6413 local_sequence: inner.sequence,
6414 observed_versions: inner.change_log.versions(),
6415 peer_acknowledgements: inner.change_log.peer_acknowledgements.clone(),
6416 };
6417 let mut file = OpenOptions::new()
6418 .create(true)
6419 .write(true)
6420 .truncate(true)
6421 .open(&temporary)?;
6422 file.write_all(&serde_json::to_vec_pretty(&metadata)?)?;
6423 file.flush()?;
6424 file.sync_all()?;
6425 fs::rename(temporary, &path)?;
6426 sync_parent_directory(&path)?;
6427 Ok(())
6428}
6429
6430fn append_event_locked(inner: &mut Inner, event: &StoredRow) -> Result<()> {
6432 append_rows_locked(inner, &[event])
6433}
6434
6435fn append_rows_locked(inner: &mut Inner, rows: &[&StoredRow]) -> Result<()> {
6441 let mut bytes = Vec::new();
6442 for row in rows {
6443 serde_json::to_writer(&mut bytes, row)?;
6444 bytes.push(b'\n');
6445 }
6446 let barrier = match inner.durability_mode {
6447 DurabilityMode::Flushed => Barrier::None,
6448 DurabilityMode::Synced => Barrier::Now,
6449 DurabilityMode::Grouped { every } => {
6450 inner.writes_since_barrier += 1;
6451 if inner.writes_since_barrier >= every.max(1) {
6452 Barrier::Group
6453 } else {
6454 Barrier::None
6455 }
6456 }
6457 };
6458 let outcome = append_bytes_locked(inner, &bytes, barrier);
6459 if barrier == Barrier::Group && outcome.is_ok() {
6460 inner.writes_since_barrier = 0;
6461 }
6462 outcome
6463}
6464
6465#[derive(Clone, Copy, PartialEq, Eq)]
6467enum Barrier {
6468 None,
6470 Now,
6472 Group,
6474}
6475
6476fn append_bytes_locked(inner: &mut Inner, bytes: &[u8], barrier: Barrier) -> Result<()> {
6492 let _span = workload_diagnostics::span(workload_diagnostics::Phase::Persistence);
6493 refuse_if_faulted(inner)?;
6494 let failed = |kind, error: &std::io::Error| {
6495 durability_failure(kind, Some(false), error.to_string())
6496 };
6497 let mut file = OpenOptions::new()
6498 .append(true)
6499 .open(&inner.path)
6500 .map_err(|error| failed(classify_write_error(&error), &error))?;
6501 let before = file
6502 .metadata()
6503 .map_err(|error| failed(DurabilityFailureKind::WriteFailed, &error))?
6504 .len();
6505
6506 let written = fault::write_all(&mut file, bytes).and_then(|()| file.flush());
6507 let outcome = match written {
6508 Err(error) => Err((classify_write_error(&error), error)),
6509 Ok(()) if barrier == Barrier::None => Ok(()),
6510 Ok(()) => fault::sync_data(&file).map_err(|error| {
6511 let kind = if barrier == Barrier::Group {
6515 DurabilityFailureKind::Indeterminate
6516 } else {
6517 DurabilityFailureKind::SyncFailed
6518 };
6519 (kind, error)
6520 }),
6521 };
6522 let Err((kind, error)) = outcome else {
6523 return Ok(());
6524 };
6525
6526 let rollback = fault::rollback(&file, before);
6527 let reason = match (rollback, kind) {
6528 (Err(rollback_error), _) => format!(
6529 "{error}; cutting the log back to byte {before} also failed: {rollback_error}"
6530 ),
6531 (Ok(()), DurabilityFailureKind::Indeterminate) => format!(
6532 "{error}; writes acknowledged earlier in this durability group may not be durable"
6533 ),
6534 (Ok(()), kind) => return Err(failed(kind, &error)),
6535 };
6536 inner.write_fault = Some(reason.clone());
6537 Err(durability_failure(
6538 DurabilityFailureKind::Indeterminate,
6539 None,
6540 reason,
6541 ))
6542}
6543
6544fn sibling(path: &Path, suffix: &str) -> PathBuf {
6546 let mut name = path.as_os_str().to_os_string();
6547 name.push(".");
6548 name.push(suffix);
6549 PathBuf::from(name)
6550}
6551
6552fn remove_restore_staging(staging: &Path) -> std::io::Result<()> {
6555 for path in [staging.to_path_buf(), sibling(staging, "lock")] {
6556 match fs::remove_file(&path) {
6557 Ok(()) => {}
6558 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
6559 Err(error) => return Err(error),
6560 }
6561 }
6562 Ok(())
6563}
6564
6565fn refuse_if_faulted(inner: &Inner) -> Result<()> {
6568 match &inner.write_fault {
6569 Some(reason) => Err(durability_failure(
6570 DurabilityFailureKind::RecoveryRequired,
6571 Some(false),
6572 format!("an earlier write left the durable log indeterminate ({reason}); reopen the database"),
6573 )),
6574 None => Ok(()),
6575 }
6576}
6577
6578fn rewrite_log_locked(inner: &mut Inner) -> Result<()> {
6600 let temporary = sibling(&inner.path, "compact.tmp");
6601 let written = (|| -> Result<()> {
6602 fault::stage(fault::Stage::CompactionWrite)?;
6603 let file = OpenOptions::new()
6604 .create(true)
6605 .write(true)
6606 .truncate(true)
6607 .open(&temporary)?;
6608 let mut out = std::io::BufWriter::with_capacity(1 << 20, file);
6609 write_json_line_to(
6613 &mut out,
6614 &LogHeaderRef {
6615 record_type: "feltdb.snapshot.v1",
6616 format_version: DURABLE_FORMAT_VERSION,
6617 local_sequence: inner.sequence,
6618 observed_versions: &inner.change_log.observed_versions,
6619 transaction_history: &inner.transaction_history,
6620 transaction_payload_hashes: &inner.transaction_payload_hashes,
6621 authority_revision: inner.authority_revision,
6622 },
6623 )?;
6624 fault::stage(fault::Stage::CompactionMidWrite)?;
6625 for operation in inner.change_log.pending.iter().cloned() {
6626 write_json_line_to(&mut out, &stored_row_for_operation(operation))?;
6627 }
6628 for bucket in inner.rows.values() {
6629 for row in bucket.values() {
6630 let mut snapshot_row = row.clone();
6631 snapshot_row.operation = None;
6632 write_json_line_to(&mut out, &snapshot_row)?;
6633 }
6634 }
6635 let file = out.into_inner().map_err(|error| error.into_error())?;
6636 fault::stage(fault::Stage::CompactionSync)?;
6637 file.sync_all()?;
6638 fault::stage(fault::Stage::CompactionRename)?;
6639 fs::rename(&temporary, &inner.path)?;
6640 Ok(())
6641 })();
6642 if let Err(error) = written {
6643 let _ = fs::remove_file(&temporary);
6645 return Err(error);
6646 }
6647 if let Err(error) = fault::stage(fault::Stage::CompactionDirectorySync)
6648 .and_then(|()| sync_parent_directory(&inner.path))
6649 {
6650 inner.write_fault = Some(format!(
6651 "the compacted log replaced the old one, but its directory entry could not be made durable: {error}"
6652 ));
6653 return Err(FlowError::Io(error));
6654 }
6655 Ok(())
6656}
6657
6658fn write_json_line_to<W: Write, T: Serialize>(out: &mut W, value: &T) -> Result<()> {
6659 serde_json::to_writer(&mut *out, value)?;
6660 out.write_all(b"\n")?;
6661 Ok(())
6662}
6663
6664fn durability_failure(
6665 kind: DurabilityFailureKind,
6666 committed: Option<bool>,
6667 detail: String,
6668) -> FlowError {
6669 FlowError::Durability(Box::new(DurabilityFailure {
6670 kind,
6671 committed,
6672 detail,
6673 }))
6674}
6675
6676fn classify_write_error(error: &std::io::Error) -> DurabilityFailureKind {
6679 use std::io::ErrorKind;
6680 match error.kind() {
6681 ErrorKind::StorageFull | ErrorKind::FileTooLarge | ErrorKind::QuotaExceeded => {
6682 DurabilityFailureKind::StorageFull
6683 }
6684 _ => DurabilityFailureKind::WriteFailed,
6685 }
6686}
6687
6688fn sync_parent_directory(path: &Path) -> std::io::Result<()> {
6693 #[cfg(unix)]
6694 {
6695 let parent = path
6696 .parent()
6697 .filter(|parent| !parent.as_os_str().is_empty())
6698 .unwrap_or(Path::new("."));
6699 File::open(parent)?.sync_all()
6700 }
6701 #[cfg(not(unix))]
6702 {
6703 let _ = path;
6704 Ok(())
6705 }
6706}
6707
6708mod fault {
6712 use std::fs::File;
6713 use std::io::Write;
6714
6715 #[cfg(test)]
6716 pub(crate) use injection::*;
6717
6718 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
6721 pub(crate) enum Stage {
6722 CompactionWrite,
6723 CompactionMidWrite,
6724 CompactionSync,
6725 CompactionRename,
6726 CompactionDirectorySync,
6727 }
6728
6729 pub(crate) fn stage(stage: Stage) -> std::io::Result<()> {
6730 #[cfg(test)]
6731 if let Some(error) = injection::take(injection::Fault::At(stage)) {
6732 return Err(error);
6733 }
6734 let _ = stage;
6735 Ok(())
6736 }
6737
6738 pub(crate) fn write_all(file: &mut File, bytes: &[u8]) -> std::io::Result<()> {
6739 #[cfg(test)]
6740 if let Some(result) = injection::on_write(file, bytes) {
6741 return result;
6742 }
6743 file.write_all(bytes)
6744 }
6745
6746 pub(crate) fn sync_data(file: &File) -> std::io::Result<()> {
6747 #[cfg(test)]
6748 if let Some(error) = injection::take(injection::Fault::Sync) {
6749 return Err(error);
6750 }
6751 file.sync_data()
6752 }
6753
6754 pub(crate) fn rollback(file: &File, length: u64) -> std::io::Result<()> {
6756 #[cfg(test)]
6757 if let Some(error) = injection::take(injection::Fault::Rollback) {
6758 return Err(error);
6759 }
6760 file.set_len(length)?;
6761 file.sync_all()
6762 }
6763
6764 #[cfg(test)]
6765 mod injection {
6766 use std::cell::RefCell;
6767 use std::fs::File;
6768 use std::io::Write;
6769
6770 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
6772 pub(crate) enum Fault {
6773 ShortWrite { bytes: usize, errno: i32 },
6775 Sync,
6777 Rollback,
6779 At(super::Stage),
6781 }
6782
6783 thread_local! {
6784 static ARMED: RefCell<Vec<Fault>> = const { RefCell::new(Vec::new()) };
6785 }
6786
6787 pub(crate) fn arm(fault: Fault) {
6788 ARMED.with(|armed| armed.borrow_mut().push(fault));
6789 }
6790
6791 pub(crate) fn disarm() {
6792 ARMED.with(|armed| armed.borrow_mut().clear());
6793 }
6794
6795 pub(super) fn take(kind: Fault) -> Option<std::io::Error> {
6796 ARMED.with(|armed| {
6797 let mut armed = armed.borrow_mut();
6798 let index = armed.iter().position(|fault| *fault == kind)?;
6799 armed.remove(index);
6800 Some(std::io::Error::from_raw_os_error(5))
6801 })
6802 }
6803
6804 pub(super) fn on_write(file: &mut File, bytes: &[u8]) -> Option<std::io::Result<()>> {
6805 let fault = ARMED.with(|armed| {
6806 let mut armed = armed.borrow_mut();
6807 let index = armed
6808 .iter()
6809 .position(|fault| matches!(fault, Fault::ShortWrite { .. }))?;
6810 Some(armed.remove(index))
6811 })?;
6812 let Fault::ShortWrite { bytes: partial, errno } = fault else {
6813 unreachable!()
6814 };
6815 let partial = partial.min(bytes.len());
6816 Some(
6817 file.write_all(&bytes[..partial])
6818 .and_then(|()| file.flush())
6819 .and(Err(std::io::Error::from_raw_os_error(errno))),
6820 )
6821 }
6822 }
6823}
6824
6825fn committed_transaction_locked(
6833 inner: &Inner,
6834 transaction_id: &str,
6835 payload_hash: Option<&str>,
6836) -> Option<Result<AtomicCommit>> {
6837 if !inner.applied_transactions.contains(transaction_id) {
6838 return None;
6839 }
6840 if let Some(expected_hash) = payload_hash {
6841 if inner
6842 .transaction_payload_hashes
6843 .get(transaction_id)
6844 .map(String::as_str)
6845 != Some(expected_hash)
6846 {
6847 return Some(Err(FlowError::TransactionIdPayloadMismatch {
6848 transaction_id: transaction_id.to_string(),
6849 }));
6850 }
6851 }
6852 let (base_revision, commit_revision) = inner
6853 .transaction_revisions
6854 .get(transaction_id)
6855 .copied()
6856 .unwrap_or((inner.authority_revision, inner.authority_revision));
6857 let (state_before, state_after) = inner
6858 .transaction_history
6859 .iter()
6860 .find(|attribution| attribution.transaction_id == transaction_id)
6861 .map(|attribution| (attribution.state_before, attribution.state_after))
6862 .unwrap_or((inner.sequence, inner.sequence));
6863 Some(Ok(AtomicCommit {
6864 transaction_id: transaction_id.into(),
6865 base_revision,
6866 commit_revision,
6867 status: TransactionStatus::Committed,
6868 state_before,
6869 state_after,
6870 rows: vec![],
6871 duplicate: true,
6872 payload_binding: payload_hash.map(|_| PayloadBinding::Verified),
6873 }))
6874}
6875
6876fn append_transaction_locked(inner: &mut Inner, transaction: &TransactionLogRecord) -> Result<()> {
6879 let mut bytes = serde_json::to_vec(transaction)?;
6880 bytes.push(b'\n');
6881 append_bytes_locked(inner, &bytes, Barrier::Now)
6882}
6883
6884fn now_ms() -> u128 {
6885 SystemTime::now()
6886 .duration_since(UNIX_EPOCH)
6887 .expect("time moved backwards")
6888 .as_millis()
6889}
6890
6891#[cfg(test)]
6892mod tests {
6893 use super::*;
6894 use std::sync::atomic::{AtomicU64, Ordering};
6895 use std::time::{SystemTime, UNIX_EPOCH};
6896
6897 static TEMP_FILE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
6898
6899 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
6900 struct User {
6901 email: String,
6902 age: u8,
6903 active: bool,
6904 }
6905
6906 fn temp_file() -> PathBuf {
6907 let nanos = SystemTime::now()
6908 .duration_since(UNIX_EPOCH)
6909 .expect("time")
6910 .as_nanos();
6911 let sequence = TEMP_FILE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
6912 std::env::temp_dir().join(format!(
6913 "feltdb-test-{}-{nanos}-{sequence}.data",
6914 std::process::id()
6915 ))
6916 }
6917
6918 #[test]
6921 fn a_committed_transaction_records_who_committed_it() {
6922 let path = temp_file();
6923 let db = open(&path).expect("open");
6924 db.apply_atomic_transaction(
6925 "tx_agent_1",
6926 None,
6927 &[],
6928 &[AtomicMutation {
6929 capability: "billing:Customer".into(),
6930 key: "cus_1".into(),
6931 value: Some(serde_json::json!({"name": "Ada", "status": "active"})),
6932 }],
6933 Some(serde_json::json!({
6934 "subject": "agent_data_cleanup",
6935 "tenant": "acme",
6936 "application": "app_billing",
6937 "revision": "rev_7",
6938 })),
6939 )
6940 .expect("commit");
6941
6942 let recent = db.recent_transactions(10).expect("attributions");
6943 assert_eq!(recent.len(), 1);
6944 assert_eq!(recent[0].transaction_id, "tx_agent_1");
6945 assert_eq!(recent[0].subject.as_deref(), Some("agent_data_cleanup"));
6946 assert_eq!(recent[0].application_id.as_deref(), Some("app_billing"));
6947 assert_eq!(recent[0].operations, 1);
6948 assert_eq!(recent[0].keys, vec!["billing:Customer:cus_1".to_string()]);
6949 assert!(!serde_json::to_string(&recent[0]).unwrap().contains("Ada"));
6951
6952 let touching = db
6953 .transactions_touching("billing:Customer", "cus_1", 10)
6954 .expect("touching");
6955 assert_eq!(touching.len(), 1);
6956 assert_eq!(touching[0].transaction_id, "tx_agent_1");
6957 assert!(db
6958 .transactions_touching("billing:Customer", "cus_2", 10)
6959 .expect("touching")
6960 .is_empty());
6961
6962 assert_eq!(
6963 db.transaction_attribution("tx_agent_1")
6964 .expect("lookup")
6965 .map(|value| value.subject)
6966 .flatten()
6967 .as_deref(),
6968 Some("agent_data_cleanup")
6969 );
6970
6971 drop(db);
6974 let reopened = open(&path).expect("reopen");
6975 let after = reopened.recent_transactions(10).expect("attributions");
6976 assert_eq!(after.len(), 1);
6977 assert_eq!(after[0].subject.as_deref(), Some("agent_data_cleanup"));
6978 assert_eq!(after[0].keys, vec!["billing:Customer:cus_1".to_string()]);
6979 drop(reopened);
6980 let _ = fs::remove_file(path);
6981 }
6982
6983 #[test]
6985 fn transaction_attribution_is_bounded_and_newest_first() {
6986 let path = temp_file();
6987 let db = open(&path).expect("open");
6988 let commits = TRANSACTION_ATTRIBUTION_WINDOW + 5;
6989 for index in 0..commits {
6990 db.apply_atomic_transaction(
6991 &format!("tx_{index}"),
6992 None,
6993 &[],
6994 &[AtomicMutation {
6995 capability: "app:Note".into(),
6996 key: format!("note_{index}"),
6997 value: Some(serde_json::json!({"index": index})),
6998 }],
6999 Some(serde_json::json!({"subject": format!("actor_{index}")})),
7000 )
7001 .expect("commit");
7002 }
7003 let (held, capacity) = db.transaction_attribution_window().expect("window");
7004 assert_eq!(capacity, TRANSACTION_ATTRIBUTION_WINDOW);
7005 assert_eq!(held, TRANSACTION_ATTRIBUTION_WINDOW);
7006 let recent = db.recent_transactions(3).expect("attributions");
7007 assert_eq!(
7008 recent
7009 .iter()
7010 .map(|value| value.transaction_id.clone())
7011 .collect::<Vec<_>>(),
7012 vec![
7013 format!("tx_{}", commits - 1),
7014 format!("tx_{}", commits - 2),
7015 format!("tx_{}", commits - 3),
7016 ]
7017 );
7018 assert!(db
7021 .transaction_attribution("tx_0")
7022 .expect("lookup")
7023 .is_some());
7024 drop(db);
7025 let _ = fs::remove_file(path);
7026 }
7027
7028 #[test]
7029 fn canonical_transaction_history_survives_window_restart_and_compaction() {
7030 let path = temp_file();
7031 let db = open(&path).expect("open");
7032 let commits = TRANSACTION_ATTRIBUTION_WINDOW + 1;
7033 for index in 0..commits {
7034 db.apply_atomic_transaction(
7035 &format!("history_tx_{index}"),
7036 None,
7037 &[],
7038 &[AtomicMutation {
7039 capability: "history:Note".into(),
7040 key: format!("note_{index}"),
7041 value: Some(serde_json::json!({"index": index})),
7042 }],
7043 Some(serde_json::json!({"subject": "agent", "work_id": "work-1"})),
7044 )
7045 .expect("commit");
7046 }
7047 let complete = db.committed_transactions_after(0, usize::MAX).unwrap();
7048 assert_eq!(complete.len(), commits);
7049 assert_eq!(complete[0].transaction_id, "history_tx_0");
7050 assert_eq!(complete[0].operation_ids.len(), 1);
7051 assert_ne!(complete[0].operation_ids[0], complete[0].transaction_id);
7052 assert_eq!(complete[0].work_id.as_deref(), Some("work-1"));
7053 assert_eq!(
7054 db.transactions_touching("history:Note", "note_0", 1)
7055 .unwrap()[0]
7056 .transaction_id,
7057 "history_tx_0"
7058 );
7059 assert_eq!(
7060 db.transaction_attribution("history_tx_0")
7061 .unwrap()
7062 .unwrap()
7063 .audit
7064 .unwrap()["work_id"],
7065 "work-1"
7066 );
7067 let versions = db.operation_versions().unwrap();
7068 db.acknowledge_peer_versions("history-peer".into(), versions)
7069 .unwrap();
7070 db.compact_operation_log(&["history-peer".into()]).unwrap();
7071 drop(db);
7072
7073 let reopened = open(&path).expect("reopen");
7074 assert_eq!(
7075 reopened
7076 .committed_transactions_after(0, usize::MAX)
7077 .unwrap()
7078 .len(),
7079 commits
7080 );
7081 assert!(reopened
7082 .transaction_attribution("history_tx_0")
7083 .unwrap()
7084 .is_some());
7085 assert_eq!(
7086 reopened
7087 .transactions_touching("history:Note", "note_0", 1)
7088 .unwrap()[0]
7089 .transaction_id,
7090 "history_tx_0"
7091 );
7092 assert_eq!(
7093 reopened
7094 .transaction_attribution("history_tx_0")
7095 .unwrap()
7096 .unwrap()
7097 .operation_ids,
7098 complete[0].operation_ids
7099 );
7100 let _ = fs::remove_file(sync_metadata_path(&path));
7101 let _ = fs::remove_file(path);
7102 }
7103
7104 #[test]
7105 fn transaction_payload_binding_survives_restart_and_compaction() {
7106 let path = temp_file();
7107 let db = open(&path).expect("open");
7108 let mutation = AtomicMutation {
7109 capability: "managed:State".into(),
7110 key: "record-1".into(),
7111 value: Some(serde_json::json!({"value": 1})),
7112 };
7113 let first = db
7114 .apply_atomic_transaction_content_addressed(
7115 "managed-idempotency-1",
7116 Some("sha256:request-a"),
7117 None,
7118 &[],
7119 &[mutation.clone()],
7120 None,
7121 )
7122 .expect("initial commit");
7123 let duplicate = db
7124 .apply_atomic_transaction_content_addressed(
7125 "managed-idempotency-1",
7126 Some("sha256:request-a"),
7127 None,
7128 &[],
7129 &[mutation.clone()],
7130 None,
7131 )
7132 .expect("same semantic request");
7133 assert!(duplicate.duplicate);
7134 assert_eq!(duplicate.base_revision, first.base_revision);
7135 assert_eq!(duplicate.commit_revision, first.commit_revision);
7136 assert_eq!(duplicate.state_before, first.state_before);
7137 assert_eq!(duplicate.state_after, first.state_after);
7138
7139 let versions = db.operation_versions().unwrap();
7140 db.acknowledge_peer_versions("managed-peer".into(), versions)
7141 .unwrap();
7142 db.compact_operation_log(&["managed-peer".into()]).unwrap();
7143 drop(db);
7144
7145 let reopened = open(&path).expect("restart");
7146 let replay = reopened
7147 .apply_atomic_transaction_content_addressed(
7148 "managed-idempotency-1",
7149 Some("sha256:request-a"),
7150 None,
7151 &[],
7152 &[mutation.clone()],
7153 None,
7154 )
7155 .expect("retry after restart");
7156 assert!(replay.duplicate);
7157 assert_eq!(replay.commit_revision, first.commit_revision);
7158 let conflict = reopened
7159 .apply_atomic_transaction_content_addressed(
7160 "managed-idempotency-1",
7161 Some("sha256:request-b"),
7162 None,
7163 &[],
7164 &[mutation],
7165 None,
7166 )
7167 .unwrap_err();
7168 assert!(
7169 matches!(
7170 &conflict,
7171 FlowError::TransactionIdPayloadMismatch { transaction_id } if transaction_id == "managed-idempotency-1"
7172 ),
7173 "expected the typed idempotency conflict, got {conflict:?}"
7174 );
7175 let _ = fs::remove_file(sync_metadata_path(&path));
7176 let _ = fs::remove_file(path);
7177 }
7178
7179 #[test]
7180 fn single_record_commits_are_canonical_history_positions() {
7181 let path = temp_file();
7182 let db = open(&path).unwrap();
7183 db.insert("orders:one", serde_json::json!({"status":"paid"}))
7184 .unwrap();
7185 let history = db.committed_transactions_after(0, 10).unwrap();
7186 assert_eq!(history.len(), 1);
7187 assert!(history[0].transaction_id.starts_with("transaction:"));
7188 assert!(history[0].operation_ids[0].starts_with("operation:"));
7189 assert_ne!(history[0].transaction_id, history[0].operation_ids[0]);
7190 assert_eq!(history[0].keys, vec!["orders:orders:one"]);
7191 drop(db);
7192 let reopened = open(&path).unwrap();
7193 assert_eq!(
7194 reopened.committed_transactions_after(0, 10).unwrap()[0].transaction_id,
7195 history[0].transaction_id
7196 );
7197 assert_eq!(
7198 reopened.committed_transactions_after(0, 10).unwrap()[0].operation_ids,
7199 history[0].operation_ids
7200 );
7201 let _ = fs::remove_file(path);
7202 }
7203
7204 #[test]
7205 fn compacted_legacy_attribution_gets_stable_distinct_operation_identity() {
7206 let legacy = TransactionAttribution {
7207 transaction_id: "legacy-transaction".into(),
7208 operation_ids: Vec::new(),
7209 subject: Some("agent-1".into()),
7210 tenant_id: Some("tenant-1".into()),
7211 application_id: Some("app-1".into()),
7212 application_revision: None,
7213 base_revision: 4,
7214 commit_revision: 5,
7215 state_before: 4,
7216 state_after: 5,
7217 keys: vec!["orders:one".into()],
7218 operations: 2,
7219 principal_id: None,
7220 agent_id: Some("agent-1".into()),
7221 session_id: None,
7222 delegator_id: None,
7223 work_id: None,
7224 unix_ms: 1,
7225 audit: None,
7226 };
7227 let first = normalize_transaction_attribution(legacy.clone());
7228 let second = normalize_transaction_attribution(legacy);
7229 assert_eq!(first, second);
7230 assert_eq!(first.transaction_id, "legacy-transaction");
7231 assert_eq!(first.operation_ids.len(), 2);
7232 assert!(first
7233 .operation_ids
7234 .iter()
7235 .all(|operation| operation.starts_with("operation:legacy-transaction:")));
7236
7237 let single = TransactionAttribution {
7238 transaction_id: "operation:authority:9".into(),
7239 operation_ids: Vec::new(),
7240 operations: 1,
7241 ..first
7242 };
7243 let upgraded = normalize_transaction_attribution(single);
7244 assert_eq!(upgraded.transaction_id, "transaction:authority:9");
7245 assert_eq!(upgraded.operation_ids, vec!["operation:authority:9"]);
7246 }
7247
7248 #[test]
7250 fn a_bulk_transaction_truncates_its_key_list_and_keeps_the_count() {
7251 let path = temp_file();
7252 let db = open(&path).expect("open");
7253 let mutations = (0..TRANSACTION_ATTRIBUTION_KEYS + 20)
7254 .map(|index| AtomicMutation {
7255 capability: "app:Customer".into(),
7256 key: format!("cus_{index}"),
7257 value: Some(serde_json::json!({"index": index})),
7258 })
7259 .collect::<Vec<_>>();
7260 db.apply_atomic_transaction(
7261 "tx_bulk",
7262 None,
7263 &[],
7264 &mutations,
7265 Some(serde_json::json!({"subject": "migration_agent"})),
7266 )
7267 .expect("commit");
7268 let attribution = db
7269 .transaction_attribution("tx_bulk")
7270 .expect("lookup")
7271 .expect("held");
7272 assert_eq!(attribution.operations, mutations.len());
7273 assert_eq!(attribution.keys.len(), TRANSACTION_ATTRIBUTION_KEYS);
7274 drop(db);
7275 let _ = fs::remove_file(path);
7276 }
7277
7278 #[test]
7279 fn open_is_zero_init_and_creates_store() {
7280 let path = temp_file();
7281 let db = open(&path).expect("open");
7282 assert!(path.exists());
7283 drop(db);
7284 let _ = fs::remove_file(path);
7285 }
7286
7287 #[test]
7288 fn insert_get_and_reopen_are_durable() {
7289 let path = temp_file();
7290 let db = open(&path).expect("open");
7291 let user = User {
7292 email: "a@feltdb.dev".to_string(),
7293 age: 33,
7294 active: true,
7295 };
7296 db.insert("user:1", &user).expect("insert");
7297 let loaded: User = db.get("user:1").expect("get").expect("some");
7298 assert_eq!(loaded, user);
7299 drop(db);
7300
7301 let reopened = open(&path).expect("reopen");
7302 let loaded_after_restart: User = reopened.get("user:1").expect("get").expect("some");
7303 assert_eq!(loaded_after_restart, user);
7304 let _ = fs::remove_file(path);
7305 }
7306
7307 #[test]
7308 fn delete_and_reopen_preserves_tombstone() {
7309 let path = temp_file();
7310 let db = open(&path).expect("open");
7311 db.insert("users:deleted", serde_json::json!({"name": "gone"}))
7312 .expect("insert");
7313 db.delete("users:deleted").expect("delete");
7314 drop(db);
7315
7316 let reopened = open(&path).expect("reopen");
7317 assert_eq!(reopened.get_value("users:deleted").expect("get"), None);
7318 let _ = fs::remove_file(path);
7319 }
7320
7321 #[test]
7322 fn closure_query_and_capability_find_work() {
7323 let path = temp_file();
7324 let db = open(&path).expect("open");
7325 let users = db.capability("users");
7326 users
7327 .insert(User {
7328 email: "one@feltdb.dev".to_string(),
7329 age: 28,
7330 active: true,
7331 })
7332 .expect("insert");
7333 users
7334 .insert(User {
7335 email: "two@feltdb.dev".to_string(),
7336 age: 41,
7337 active: false,
7338 })
7339 .expect("insert");
7340
7341 let global = db.query::<User, _>(|u| u.age > 30).expect("query");
7342 assert_eq!(global.len(), 1);
7343
7344 let active = users.find::<User, _>(|u| u.active).expect("find");
7345 assert_eq!(active.len(), 1);
7346 assert_eq!(active[0].email, "one@feltdb.dev");
7347 let _ = fs::remove_file(path);
7348 }
7349
7350 #[test]
7351 fn adaptive_index_marks_frequent_query_types() {
7352 let path = temp_file();
7353 let db = open(&path).expect("open");
7354 db.insert(
7355 "user:1",
7356 User {
7357 email: "index@feltdb.dev".to_string(),
7358 age: 21,
7359 active: true,
7360 },
7361 )
7362 .expect("insert");
7363
7364 for _ in 0..3 {
7365 let _ = db.query::<User, _>(|u| u.active).expect("query");
7366 }
7367 assert!(db.is_auto_indexed::<User>());
7368 let _ = fs::remove_file(path);
7369 }
7370
7371 #[tokio::test]
7372 async fn live_query_returns_initial_results() {
7373 let path = temp_file();
7374 let db = open(&path).expect("open");
7375 let users = db.capability("users");
7376 users
7377 .insert(User {
7378 email: "live@feltdb.dev".to_string(),
7379 age: 28,
7380 active: true,
7381 })
7382 .expect("insert");
7383 users
7384 .insert(User {
7385 email: "inactive@feltdb.dev".to_string(),
7386 age: 35,
7387 active: false,
7388 })
7389 .expect("insert");
7390
7391 let subscription = db
7392 .live_query::<User, _>(|u| u.active)
7393 .await
7394 .expect("live_query");
7395 let initial = subscription.initial_results();
7396 assert_eq!(initial.len(), 1);
7397 assert_eq!(initial[0].email, "live@feltdb.dev");
7398 let _ = fs::remove_file(path);
7399 }
7400
7401 #[tokio::test]
7402 async fn live_query_streams_new_changes() {
7403 let path = temp_file();
7404 let db = open(&path).expect("open");
7405 let users = db.capability("users");
7406 users
7407 .insert(User {
7408 email: "initial@feltdb.dev".to_string(),
7409 age: 28,
7410 active: true,
7411 })
7412 .expect("insert");
7413
7414 let mut subscription = db
7415 .live_query::<User, _>(|u| u.active)
7416 .await
7417 .expect("live_query");
7418 let initial = subscription.initial_results();
7419 assert_eq!(initial.len(), 1);
7420
7421 let db_for_insert = db.clone();
7422 tokio::spawn(async move {
7423 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
7424 let users_inner = db_for_insert.capability("users");
7425 users_inner
7426 .insert(User {
7427 email: "new@feltdb.dev".to_string(),
7428 age: 25,
7429 active: true,
7430 })
7431 .expect("insert");
7432 });
7433
7434 let updated = subscription.next().await;
7435 assert!(updated.is_some());
7436 let results = updated.unwrap();
7437 assert_eq!(results.len(), 2);
7438 let _ = fs::remove_file(path);
7439 }
7440
7441 #[tokio::test]
7442 async fn live_query_filters_by_predicate() {
7443 let path = temp_file();
7444 let db = open(&path).expect("open");
7445 let users = db.capability("users");
7446 users
7447 .insert(User {
7448 email: "active@feltdb.dev".to_string(),
7449 age: 28,
7450 active: true,
7451 })
7452 .expect("insert");
7453 users
7454 .insert(User {
7455 email: "inactive@feltdb.dev".to_string(),
7456 age: 35,
7457 active: false,
7458 })
7459 .expect("insert");
7460
7461 let subscription = db
7462 .live_query::<User, _>(|u| u.active)
7463 .await
7464 .expect("live_query");
7465 let initial = subscription.initial_results();
7466 assert_eq!(initial.len(), 1);
7467 assert!(initial[0].active);
7468 let _ = fs::remove_file(path);
7469 }
7470
7471 #[tokio::test]
7472 async fn change_events_are_emitted_on_insert() {
7473 let path = temp_file();
7474 let db = open(&path).expect("open");
7475
7476 let mut subscription = db
7477 .live_query::<User, _>(|_| true)
7478 .await
7479 .expect("live_query");
7480 let initial = subscription.initial_results();
7481 assert_eq!(initial.len(), 0);
7482
7483 let db_for_insert = db.clone();
7484 tokio::spawn(async move {
7485 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
7486 let users_inner = db_for_insert.capability("users");
7487 users_inner
7488 .insert(User {
7489 email: "event@feltdb.dev".to_string(),
7490 age: 30,
7491 active: true,
7492 })
7493 .expect("insert");
7494 });
7495
7496 let update = subscription.next().await;
7497 assert!(update.is_some());
7498 let users_result = update.unwrap();
7499 assert_eq!(users_result.len(), 1);
7500 let _ = fs::remove_file(path);
7501 }
7502
7503 #[tokio::test]
7504 async fn capability_live_find_returns_initial_results() {
7505 let path = temp_file();
7506 let db = open(&path).expect("open");
7507 let users = db.capability("users");
7508 users
7509 .insert(User {
7510 email: "cap_live@feltdb.dev".to_string(),
7511 age: 28,
7512 active: true,
7513 })
7514 .expect("insert");
7515 users
7516 .insert(User {
7517 email: "cap_inactive@feltdb.dev".to_string(),
7518 age: 35,
7519 active: false,
7520 })
7521 .expect("insert");
7522
7523 let subscription = users
7524 .live_find::<User, _>(|u| u.active)
7525 .await
7526 .expect("live_find");
7527 let initial = subscription.initial_results();
7528 assert_eq!(initial.len(), 1);
7529 assert_eq!(initial[0].email, "cap_live@feltdb.dev");
7530 let _ = fs::remove_file(path);
7531 }
7532
7533 #[tokio::test]
7534 async fn capability_live_find_streams_scoped_changes() {
7535 let path = temp_file();
7536 let db = open(&path).expect("open");
7537 let users = db.capability("users");
7538 let _orders = db.capability("orders");
7539 users
7540 .insert(User {
7541 email: "user_scoped@feltdb.dev".to_string(),
7542 age: 28,
7543 active: true,
7544 })
7545 .expect("insert");
7546
7547 let mut subscription = users
7548 .live_find::<User, _>(|u| u.active)
7549 .await
7550 .expect("live_find");
7551 let initial = subscription.initial_results();
7552 assert_eq!(initial.len(), 1);
7553
7554 let db_for_insert = db.clone();
7555 tokio::spawn(async move {
7556 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
7557 let users_inner = db_for_insert.capability("users");
7558 users_inner
7559 .insert(User {
7560 email: "new_user_scoped@feltdb.dev".to_string(),
7561 age: 25,
7562 active: true,
7563 })
7564 .expect("insert");
7565 });
7566
7567 let updated = subscription.next().await;
7568 assert!(updated.is_some());
7569 let results = updated.unwrap();
7570 assert_eq!(results.len(), 2);
7571 let _ = fs::remove_file(path);
7572 }
7573
7574 #[test]
7575 fn vector_clocks_increment_on_mutations() {
7576 let path = temp_file();
7577 let db = open(&path).expect("open");
7578
7579 let initial_clock = db.get_vector_clock().expect("get_vector_clock");
7581 assert!(initial_clock.is_none() || initial_clock.as_ref().unwrap().is_empty());
7582
7583 db.insert(
7585 "users:1",
7586 User {
7587 email: "user1@feltdb.dev".to_string(),
7588 age: 25,
7589 active: true,
7590 },
7591 )
7592 .expect("insert");
7593
7594 let pending = db
7596 .get_pending_for_peer("peer-1", 0)
7597 .expect("get_pending_for_peer");
7598 assert_eq!(pending.len(), 1);
7599 assert!(pending[0].vector_clock.is_some());
7600
7601 db.insert(
7603 "users:2",
7604 User {
7605 email: "user2@feltdb.dev".to_string(),
7606 age: 30,
7607 active: false,
7608 },
7609 )
7610 .expect("insert");
7611
7612 let pending = db
7614 .get_pending_for_peer("peer-1", 0)
7615 .expect("get_pending_for_peer");
7616 assert_eq!(pending.len(), 2);
7617 assert_eq!(pending[0].sequence, 1);
7618 assert_eq!(pending[1].sequence, 2);
7619
7620 let _ = fs::remove_file(path);
7621 }
7622
7623 #[test]
7624 fn update_creates_operation_with_vector_clock() {
7625 let path = temp_file();
7626 let db = open(&path).expect("open");
7627
7628 db.insert(
7630 "users:1",
7631 User {
7632 email: "user1@feltdb.dev".to_string(),
7633 age: 25,
7634 active: true,
7635 },
7636 )
7637 .expect("insert");
7638
7639 db.update(
7641 "users:1",
7642 User {
7643 email: "user1@feltdb.dev".to_string(),
7644 age: 26,
7645 active: true,
7646 },
7647 )
7648 .expect("update");
7649
7650 let pending = db
7652 .get_pending_for_peer("peer-1", 0)
7653 .expect("get_pending_for_peer");
7654 assert_eq!(pending.len(), 2);
7655 assert_eq!(pending[0].op_type, OperationType::Insert);
7656 assert_eq!(pending[1].op_type, OperationType::Update);
7657
7658 assert!(pending[0].vector_clock.is_some());
7660 assert!(pending[1].vector_clock.is_some());
7661
7662 let _ = fs::remove_file(path);
7663 }
7664
7665 #[test]
7666 fn delete_creates_operation_with_vector_clock() {
7667 let path = temp_file();
7668 let db = open(&path).expect("open");
7669
7670 db.insert(
7672 "users:1",
7673 User {
7674 email: "user1@feltdb.dev".to_string(),
7675 age: 25,
7676 active: true,
7677 },
7678 )
7679 .expect("insert");
7680
7681 db.delete("users:1").expect("delete");
7683
7684 let pending = db
7686 .get_pending_for_peer("peer-1", 0)
7687 .expect("get_pending_for_peer");
7688 assert_eq!(pending.len(), 2);
7689 assert_eq!(pending[0].op_type, OperationType::Insert);
7690 assert_eq!(pending[1].op_type, OperationType::Delete);
7691
7692 assert!(pending[0].vector_clock.is_some());
7694 assert!(pending[1].vector_clock.is_some());
7695
7696 let _ = fs::remove_file(path);
7697 }
7698
7699 #[test]
7700 fn apply_remote_operation_merges_vector_clocks() {
7701 let path = temp_file();
7702 let db = open(&path).expect("open");
7703
7704 let remote_op = Operation::insert(
7706 1,
7707 "remote-instance".to_string(),
7708 1,
7709 "users:100".to_string(),
7710 serde_json::json!({"email": "remote@feltdb.dev", "age": 30, "active": true}),
7711 "User".to_string(),
7712 "users".to_string(),
7713 )
7714 .with_vector_clock({
7715 let mut vc = std::collections::HashMap::new();
7716 vc.insert("remote-instance".to_string(), 1);
7717 vc
7718 });
7719
7720 db.apply_remote_operation(remote_op)
7722 .expect("apply_remote_operation");
7723
7724 let clock = db.get_vector_clock().expect("get_vector_clock");
7726 assert!(clock.is_some());
7727 let clock = clock.unwrap();
7728 assert_eq!(clock.get("remote-instance"), Some(&1));
7729
7730 let user: Option<User> = db.get("users:100").expect("get");
7732 assert!(user.is_some());
7733
7734 let _ = fs::remove_file(path);
7735 }
7736
7737 #[test]
7738 fn operation_history_survives_restart_and_relays_transitively() {
7739 let source_path = temp_file();
7740 let relay_path = temp_file();
7741 let destination_path = temp_file();
7742 let source = open(&source_path).expect("open source");
7743 source
7744 .insert("docs:1", serde_json::json!({"title": "one"}))
7745 .unwrap();
7746 source
7747 .insert("docs:2", serde_json::json!({"title": "two"}))
7748 .unwrap();
7749
7750 let relay = open(&relay_path).expect("open relay");
7751 for operation in source.operations_since(&HashMap::new()).unwrap() {
7752 relay.apply_remote_operation(operation).unwrap();
7753 }
7754 drop(relay);
7755
7756 let relay = open(&relay_path).expect("reopen relay");
7757 let relayed = relay.operations_since(&HashMap::new()).unwrap();
7758 assert_eq!(relayed.len(), 2);
7759 let destination = open(&destination_path).expect("open destination");
7760 for operation in relayed {
7761 destination.apply_remote_operation(operation).unwrap();
7762 }
7763 assert_eq!(
7764 destination.get_value("docs:2").unwrap(),
7765 Some(serde_json::json!({"title": "two"}))
7766 );
7767
7768 let _ = fs::remove_file(source_path);
7769 let _ = fs::remove_file(relay_path);
7770 let _ = fs::remove_file(destination_path);
7771 }
7772
7773 #[test]
7774 fn remote_operation_sequence_gaps_are_rejected() {
7775 let path = temp_file();
7776 let db = open(&path).unwrap();
7777 let operation = Operation::insert(
7778 2,
7779 "remote".to_string(),
7780 2,
7781 "docs:2".to_string(),
7782 serde_json::json!({"title": "two"}),
7783 "Value".to_string(),
7784 "docs".to_string(),
7785 );
7786 assert!(db.apply_remote_operation(operation).is_err());
7787 assert!(db.get_value("docs:2").unwrap().is_none());
7788 let _ = fs::remove_file(path);
7789 }
7790
7791 #[test]
7792 fn peer_version_acknowledgements_survive_restart() {
7793 let path = temp_file();
7794 let db = open(&path).unwrap();
7795 db.insert("docs:1", serde_json::json!({"title": "one"}))
7796 .unwrap();
7797 let versions = db.operation_versions().unwrap();
7798 db.acknowledge_peer_versions("peer-a".to_string(), versions.clone())
7799 .unwrap();
7800 drop(db);
7801
7802 let reopened = open(&path).unwrap();
7803 assert_eq!(
7804 reopened.acknowledged_peer_versions("peer-a").unwrap(),
7805 versions
7806 );
7807 assert_eq!(
7808 reopened
7809 .acknowledged_operation_count(&["peer-a".to_string()])
7810 .unwrap(),
7811 1
7812 );
7813 let _ = fs::remove_file(sync_metadata_path(&path));
7814 let _ = fs::remove_file(path);
7815 }
7816
7817 #[test]
7818 fn compacted_history_bootstraps_a_new_peer_without_sequence_gaps() {
7819 let source_path = temp_file();
7820 let destination_path = temp_file();
7821 let source = open(&source_path).unwrap();
7822 source
7823 .insert("docs:1", serde_json::json!({"title": "one"}))
7824 .unwrap();
7825 source
7826 .insert("docs:2", serde_json::json!({"title": "two"}))
7827 .unwrap();
7828 let versions = source.operation_versions().unwrap();
7829 source
7830 .acknowledge_peer_versions("peer-a".to_string(), versions.clone())
7831 .unwrap();
7832 assert_eq!(
7833 source
7834 .compact_operation_log(&["peer-a".to_string()])
7835 .unwrap(),
7836 2
7837 );
7838 assert!(source.operations_since(&HashMap::new()).unwrap().is_empty());
7839 drop(source);
7840
7841 let source = open(&source_path).unwrap();
7842 assert_eq!(source.operation_versions().unwrap(), versions);
7843 let snapshot = source.export_snapshot().unwrap();
7844 let destination = open(&destination_path).unwrap();
7845 destination.install_snapshot(snapshot).unwrap();
7846 assert_eq!(destination.operation_versions().unwrap(), versions);
7847 assert_eq!(
7848 destination.get_value("docs:2").unwrap(),
7849 Some(serde_json::json!({"title": "two"}))
7850 );
7851
7852 source
7853 .insert("docs:3", serde_json::json!({"title": "three"}))
7854 .unwrap();
7855 let next = source.operations_since(&versions).unwrap();
7856 assert_eq!(next.len(), 1);
7857 destination.apply_remote_operation(next[0].clone()).unwrap();
7858 assert!(destination.get_value("docs:3").unwrap().is_some());
7859
7860 let _ = fs::remove_file(sync_metadata_path(&source_path));
7861 let _ = fs::remove_file(sync_metadata_path(&destination_path));
7862 let _ = fs::remove_file(source_path);
7863 let _ = fs::remove_file(destination_path);
7864 }
7865
7866 #[test]
7867 fn maintained_cardinality_unfiltered_count_basic() {
7868 let path = temp_file();
7869 let db = open(&path).unwrap();
7870 let capability = "test_collection";
7871
7872 let mutations = vec![
7873 AtomicMutation {
7874 capability: capability.to_string(),
7875 key: "rec-1".to_string(),
7876 value: Some(serde_json::json!({"id": 1, "name": "Record 1"})),
7877 },
7878 AtomicMutation {
7879 capability: capability.to_string(),
7880 key: "rec-2".to_string(),
7881 value: Some(serde_json::json!({"id": 2, "name": "Record 2"})),
7882 },
7883 ];
7884
7885 db.apply_atomic_transaction("txn-1", None, &[], &mutations, None)
7886 .unwrap();
7887
7888 let cardinality = db.collection_cardinality(capability).unwrap();
7889 assert_eq!(cardinality, 2, "Cardinality should be 2 after 2 inserts");
7890
7891 let _ = fs::remove_file(path);
7892 }
7893
7894 #[test]
7895 fn maintained_cardinality_insert_increments() {
7896 let path = temp_file();
7897 let db = open(&path).unwrap();
7898 let capability = "test_collection";
7899
7900 for i in 1..=5 {
7901 let mutations = vec![AtomicMutation {
7902 capability: capability.to_string(),
7903 key: format!("rec-{}", i),
7904 value: Some(serde_json::json!({"id": i, "name": format!("Record {}", i)})),
7905 }];
7906
7907 db.apply_atomic_transaction(&format!("txn-{}", i), None, &[], &mutations, None)
7908 .unwrap();
7909 let cardinality = db.collection_cardinality(capability).unwrap();
7910 assert_eq!(
7911 cardinality, i as u64,
7912 "Cardinality should be {} after {} inserts",
7913 i, i
7914 );
7915 }
7916
7917 let _ = fs::remove_file(path);
7918 }
7919
7920 #[test]
7921 fn maintained_cardinality_delete_decrements() {
7922 let path = temp_file();
7923 let db = open(&path).unwrap();
7924 let capability = "test_collection";
7925
7926 let insert_mutations = vec![
7927 AtomicMutation {
7928 capability: capability.to_string(),
7929 key: "rec-1".to_string(),
7930 value: Some(serde_json::json!({"id": 1})),
7931 },
7932 AtomicMutation {
7933 capability: capability.to_string(),
7934 key: "rec-2".to_string(),
7935 value: Some(serde_json::json!({"id": 2})),
7936 },
7937 AtomicMutation {
7938 capability: capability.to_string(),
7939 key: "rec-3".to_string(),
7940 value: Some(serde_json::json!({"id": 3})),
7941 },
7942 ];
7943
7944 db.apply_atomic_transaction("txn-insert", None, &[], &insert_mutations, None)
7945 .unwrap();
7946 assert_eq!(db.collection_cardinality(capability).unwrap(), 3);
7947
7948 let delete_mutations = vec![AtomicMutation {
7949 capability: capability.to_string(),
7950 key: "rec-1".to_string(),
7951 value: None,
7952 }];
7953
7954 db.apply_atomic_transaction("txn-delete", None, &[], &delete_mutations, None)
7955 .unwrap();
7956 assert_eq!(
7957 db.collection_cardinality(capability).unwrap(),
7958 2,
7959 "Cardinality should be 2 after deleting 1 record"
7960 );
7961
7962 let _ = fs::remove_file(path);
7963 }
7964
7965 #[test]
7966 fn maintained_cardinality_update_does_not_change_count() {
7967 let path = temp_file();
7968 let db = open(&path).unwrap();
7969 let capability = "test_collection";
7970
7971 let insert_mutations = vec![AtomicMutation {
7972 capability: capability.to_string(),
7973 key: "rec-1".to_string(),
7974 value: Some(serde_json::json!({"id": 1, "status": "active"})),
7975 }];
7976
7977 db.apply_atomic_transaction("txn-insert", None, &[], &insert_mutations, None)
7978 .unwrap();
7979 assert_eq!(db.collection_cardinality(capability).unwrap(), 1);
7980
7981 let update_mutations = vec![AtomicMutation {
7982 capability: capability.to_string(),
7983 key: "rec-1".to_string(),
7984 value: Some(serde_json::json!({"id": 1, "status": "inactive"})),
7985 }];
7986
7987 db.apply_atomic_transaction("txn-update", None, &[], &update_mutations, None)
7988 .unwrap();
7989 assert_eq!(
7990 db.collection_cardinality(capability).unwrap(),
7991 1,
7992 "Cardinality should remain 1 after update"
7993 );
7994
7995 let _ = fs::remove_file(path);
7996 }
7997
7998 #[test]
7999 fn maintained_cardinality_survives_restart() {
8000 let path = temp_file();
8001
8002 {
8003 let db = open(&path).unwrap();
8004 let capability = "test_collection";
8005
8006 let mutations = vec![
8007 AtomicMutation {
8008 capability: capability.to_string(),
8009 key: "rec-1".to_string(),
8010 value: Some(serde_json::json!({"id": 1})),
8011 },
8012 AtomicMutation {
8013 capability: capability.to_string(),
8014 key: "rec-2".to_string(),
8015 value: Some(serde_json::json!({"id": 2})),
8016 },
8017 ];
8018
8019 db.apply_atomic_transaction("txn-1", None, &[], &mutations, None)
8020 .unwrap();
8021 assert_eq!(db.collection_cardinality(capability).unwrap(), 2);
8022 }
8023
8024 let reopened = open(&path).unwrap();
8025 let capability = "test_collection";
8026 let cardinality = reopened.collection_cardinality(capability).unwrap();
8027 assert_eq!(
8028 cardinality, 2,
8029 "Cardinality should survive restart and be recomputed from rows"
8030 );
8031
8032 let _ = fs::remove_file(path);
8033 }
8034
8035 #[test]
8036 fn maintained_cardinality_empty_collection() {
8037 let path = temp_file();
8038 let db = open(&path).unwrap();
8039 let capability = "empty_collection";
8040
8041 let cardinality = db.collection_cardinality(capability).unwrap();
8042 assert_eq!(cardinality, 0, "Empty collection should have cardinality 0");
8043
8044 let _ = fs::remove_file(path);
8045 }
8046
8047 #[test]
8048 fn maintained_cardinality_large_collection() {
8049 let path = temp_file();
8050 let db = open(&path).unwrap();
8051 let capability = "large_collection";
8052
8053 let mut mutations = Vec::new();
8054 for i in 1..=100 {
8055 mutations.push(AtomicMutation {
8056 capability: capability.to_string(),
8057 key: format!("rec-{}", i),
8058 value: Some(serde_json::json!({"id": i, "name": format!("Record {}", i)})),
8059 });
8060 }
8061
8062 db.apply_atomic_transaction("txn-large", None, &[], &mutations, None)
8063 .unwrap();
8064 let cardinality = db.collection_cardinality(capability).unwrap();
8065 assert_eq!(
8066 cardinality, 100,
8067 "Large collection with 100 records should have cardinality 100"
8068 );
8069
8070 let _ = fs::remove_file(path);
8071 }
8072
8073 #[test]
8074 fn maintained_cardinality_multiple_collections_isolated() {
8075 let path = temp_file();
8076 let db = open(&path).unwrap();
8077
8078 let col1 = "collection_1";
8079 let col2 = "collection_2";
8080
8081 let mutations1 = vec![
8082 AtomicMutation {
8083 capability: col1.to_string(),
8084 key: "rec-1".to_string(),
8085 value: Some(serde_json::json!({"id": 1})),
8086 },
8087 AtomicMutation {
8088 capability: col1.to_string(),
8089 key: "rec-2".to_string(),
8090 value: Some(serde_json::json!({"id": 2})),
8091 },
8092 ];
8093
8094 db.apply_atomic_transaction("txn-col1", None, &[], &mutations1, None)
8095 .unwrap();
8096
8097 let mutations2 = vec![
8098 AtomicMutation {
8099 capability: col2.to_string(),
8100 key: "rec-1".to_string(),
8101 value: Some(serde_json::json!({"id": 1})),
8102 },
8103 AtomicMutation {
8104 capability: col2.to_string(),
8105 key: "rec-2".to_string(),
8106 value: Some(serde_json::json!({"id": 2})),
8107 },
8108 AtomicMutation {
8109 capability: col2.to_string(),
8110 key: "rec-3".to_string(),
8111 value: Some(serde_json::json!({"id": 3})),
8112 },
8113 ];
8114
8115 db.apply_atomic_transaction("txn-col2", None, &[], &mutations2, None)
8116 .unwrap();
8117
8118 assert_eq!(db.collection_cardinality(col1).unwrap(), 2);
8119 assert_eq!(db.collection_cardinality(col2).unwrap(), 3);
8120
8121 let _ = fs::remove_file(path);
8122 }
8123}
8124
8125#[cfg(test)]
8133mod durability_tests {
8134 use super::fault::{arm, disarm, Fault};
8135 use super::*;
8136 use serde_json::json;
8137
8138 const ENOSPC: i32 = 28;
8139 const EFBIG: i32 = 27;
8140 const EIO: i32 = 5;
8141
8142 fn database() -> (PathBuf, FeltDb) {
8143 let directory = std::env::temp_dir().join(format!(
8144 "feltdb-durability-{}-{}",
8145 std::process::id(),
8146 SystemTime::now()
8147 .duration_since(UNIX_EPOCH)
8148 .unwrap()
8149 .as_nanos()
8150 ));
8151 fs::create_dir_all(&directory).unwrap();
8152 let path = directory.join("state.log");
8153 let db = FeltDb::open(&path).unwrap();
8154 db.set_durability_mode(DurabilityMode::Synced);
8155 (path, db)
8156 }
8157
8158 fn reopen(path: &Path, db: FeltDb) -> FeltDb {
8159 drop(db);
8160 let db = FeltDb::open(path).expect("the log reopens");
8161 db.set_durability_mode(DurabilityMode::Synced);
8162 db
8163 }
8164
8165 fn failure(result: Result<impl std::fmt::Debug>) -> DurabilityFailure {
8166 disarm();
8167 match result {
8168 Err(FlowError::Durability(failure)) => *failure,
8169 other => panic!("expected a durability failure, got {other:?}"),
8170 }
8171 }
8172
8173 fn insert_tx(db: &FeltDb, id: &str) -> Result<AtomicCommit> {
8174 db.apply_atomic_transaction(
8175 id,
8176 None,
8177 &[],
8178 &[AtomicMutation {
8179 capability: "notes".into(),
8180 key: format!("notes:{id}"),
8181 value: Some(json!({ "id": id })),
8182 }],
8183 None,
8184 )
8185 }
8186
8187 #[test]
8188 fn a_full_disk_fails_the_write_completely_and_the_next_write_lands() {
8189 let (path, db) = database();
8190 db.insert("notes:a", json!({ "id": "a" })).unwrap();
8191 let length = fs::metadata(&path).unwrap().len();
8192 let operations = db.operations_since(&HashMap::new()).unwrap().len();
8193 let sequence = db.sequence().unwrap();
8194
8195 arm(Fault::ShortWrite { bytes: 40, errno: ENOSPC });
8197 let refused = failure(db.insert("notes:b", json!({ "id": "b" })));
8198 assert_eq!(refused.kind, DurabilityFailureKind::StorageFull);
8199 assert_eq!(refused.code(), "STORAGE_FULL");
8200 assert_eq!(refused.committed, Some(false));
8201
8202 assert_eq!(fs::metadata(&path).unwrap().len(), length, "the partial bytes were removed");
8203 assert_eq!(db.get_value("notes:b").unwrap(), None, "not visible");
8204 assert_eq!(db.sequence().unwrap(), sequence, "the reserved sequence was given back");
8205 assert_eq!(
8206 db.operations_since(&HashMap::new()).unwrap().len(),
8207 operations,
8208 "replication was never offered the refused write"
8209 );
8210
8211 db.insert("notes:c", json!({ "id": "c" })).unwrap();
8213 let db = reopen(&path, db);
8214 assert_eq!(db.log_recovery(), LogRecovery::Clean);
8215 assert!(db.get_value("notes:a").unwrap().is_some());
8216 assert_eq!(db.get_value("notes:b").unwrap(), None);
8217 assert!(db.get_value("notes:c").unwrap().is_some());
8218 }
8219
8220 #[test]
8221 fn a_partial_transaction_record_never_splices_onto_the_next_commit() {
8222 let (path, db) = database();
8225 insert_tx(&db, "t1").unwrap();
8226 arm(Fault::ShortWrite { bytes: 64, errno: EFBIG });
8227 let refused = failure(insert_tx(&db, "t2"));
8228 assert_eq!(refused.kind, DurabilityFailureKind::StorageFull);
8229 assert_eq!(refused.committed, Some(false));
8230 insert_tx(&db, "t3").unwrap();
8231
8232 let db = reopen(&path, db);
8233 assert_eq!(db.log_recovery(), LogRecovery::Clean);
8234 assert!(db.get_value("notes:t1").unwrap().is_some());
8235 assert_eq!(db.get_value("notes:t2").unwrap(), None);
8236 assert!(db.get_value("notes:t3").unwrap().is_some());
8237 assert!(!insert_tx(&db, "t2").unwrap().duplicate);
8239 }
8240
8241 #[test]
8242 fn a_failed_barrier_is_rolled_back_so_a_reported_failure_never_resurrects() {
8243 let (path, db) = database();
8246 arm(Fault::Sync);
8247 let refused = failure(insert_tx(&db, "t1"));
8248 assert_eq!(refused.kind, DurabilityFailureKind::SyncFailed);
8249 assert_eq!(refused.code(), "STORAGE_SYNC_FAILED");
8250 assert_eq!(refused.committed, Some(false));
8251
8252 arm(Fault::Sync);
8253 let refused = failure(db.insert("notes:s", json!({ "id": "s" })));
8254 assert_eq!(refused.kind, DurabilityFailureKind::SyncFailed);
8255
8256 let db = reopen(&path, db);
8257 assert_eq!(db.get_value("notes:t1").unwrap(), None);
8258 assert_eq!(db.get_value("notes:s").unwrap(), None);
8259 assert_eq!(db.log_recovery(), LogRecovery::Clean);
8260 }
8261
8262 #[test]
8263 fn a_write_that_cannot_be_rolled_back_stops_writes_until_reopen_decides() {
8264 let (path, db) = database();
8265 db.insert("notes:a", json!({ "id": "a" })).unwrap();
8266
8267 arm(Fault::Sync);
8269 arm(Fault::Rollback);
8270 let unknown = failure(insert_tx(&db, "t1"));
8271 assert_eq!(unknown.kind, DurabilityFailureKind::Indeterminate);
8272 assert_eq!(unknown.code(), "DURABILITY_FAILURE");
8273 assert_eq!(unknown.committed, None, "the outcome is unknown, and said so");
8274
8275 let length = fs::metadata(&path).unwrap().len();
8277 for refused in [
8278 failure(db.insert("notes:b", json!({ "id": "b" }))),
8279 failure(db.delete("notes:a")),
8280 failure(insert_tx(&db, "t2")),
8281 failure(db.compact_operation_log(&[])),
8282 ] {
8283 assert_eq!(refused.kind, DurabilityFailureKind::RecoveryRequired);
8284 assert_eq!(refused.code(), "RECOVERY_REQUIRED");
8285 assert_eq!(refused.committed, Some(false));
8286 }
8287 assert_eq!(fs::metadata(&path).unwrap().len(), length, "nothing more was written");
8288 assert!(db.get_value("notes:a").unwrap().is_some(), "reads keep serving acknowledged writes");
8289 let health = db.health();
8290 assert_eq!(health.writes.label(), "recovery-required");
8291 assert!(!health.is_nominal());
8292
8293 let db = reopen(&path, db);
8296 assert_eq!(db.health().writes, WriteAvailability::Available);
8297 assert!(db.get_value("notes:t1").unwrap().is_some());
8298 assert!(insert_tx(&db, "t1").unwrap().duplicate, "a same-id retry reconciles");
8299 db.insert("notes:b", json!({ "id": "b" })).unwrap();
8300 }
8301
8302 #[test]
8303 fn an_io_error_on_write_is_typed_and_rolled_back() {
8304 let (path, db) = database();
8305 arm(Fault::ShortWrite { bytes: 0, errno: EIO });
8306 let refused = failure(db.update("notes:x", json!({ "id": "x" })));
8307 assert_eq!(refused.kind, DurabilityFailureKind::WriteFailed);
8308 assert_eq!(refused.code(), "STORAGE_WRITE_FAILED");
8309 assert_eq!(refused.committed, Some(false));
8310 let db = reopen(&path, db);
8311 assert_eq!(db.get_value("notes:x").unwrap(), None);
8312 }
8313
8314 #[test]
8315 fn a_single_record_write_and_its_revision_are_one_append() {
8316 let (path, db) = database();
8319 let before = fs::metadata(&path).unwrap().len();
8320 let record_bytes = {
8321 db.insert("notes:a", json!({ "id": "a" })).unwrap();
8322 fs::metadata(&path).unwrap().len() - before
8323 };
8324 let length = fs::metadata(&path).unwrap().len();
8326 arm(Fault::ShortWrite { bytes: (record_bytes as usize) - 20, errno: ENOSPC });
8327 let refused = failure(db.insert("notes:b", json!({ "id": "b" })));
8328 assert_eq!(refused.committed, Some(false));
8329 assert_eq!(fs::metadata(&path).unwrap().len(), length);
8330 assert_eq!(db.get_value("notes:b").unwrap(), None);
8331 let db = reopen(&path, db);
8332 assert_eq!(db.get_value("notes:b").unwrap(), None, "the failure is the truth after restart too");
8333 assert_eq!(db.log_recovery(), LogRecovery::Clean);
8334 }
8335
8336 #[test]
8337 fn a_failed_group_barrier_is_indeterminate_for_the_writes_it_covered() {
8338 let (path, db) = database();
8339 db.set_durability_mode(DurabilityMode::Grouped { every: 2 });
8340 db.insert("notes:a", json!({ "id": "a" })).unwrap(); arm(Fault::Sync);
8342 let unknown = failure(db.insert("notes:b", json!({ "id": "b" })));
8343 assert_eq!(unknown.kind, DurabilityFailureKind::Indeterminate);
8344 assert_eq!(db.health().writes.label(), "recovery-required");
8345 drop(reopen(&path, db));
8346 }
8347
8348 #[test]
8349 fn an_interrupted_restore_leaves_no_database_at_the_target() {
8350 let (path, db) = database();
8351 for id in ["a", "b", "c"] {
8352 db.insert(&format!("notes:{id}"), json!({ "id": id })).unwrap();
8353 }
8354 let directory = path.parent().unwrap().to_path_buf();
8355 let backup = directory.join("state.backup");
8356 db.write_backup(&backup).unwrap();
8357
8358 let target = directory.join("restored.log");
8360 let staging = sibling(&target, "restoring");
8361 fs::write(&staging, b"{\"record_type\":\"feltdb.format\"").unwrap();
8362 assert!(!target.exists());
8363
8364 let (restored, verification) = FeltDb::restore_backup(&backup, &target).unwrap();
8366 assert_eq!(verification.rows, 6, "three records and their three revisions");
8367 assert!(!staging.exists());
8368 assert!(restored.get_value("notes:c").unwrap().is_some());
8369 assert_eq!(
8370 restored.state_digest().unwrap(),
8371 db.state_digest().unwrap()
8372 );
8373 drop(restored);
8375 assert!(matches!(
8376 FeltDb::restore_backup(&backup, &target),
8377 Err(FlowError::BackupRejected(_))
8378 ));
8379 }
8380}
8381
8382
8383#[cfg(test)]
8390mod growth_tests {
8391 use super::fault::{arm, disarm, Fault, Stage};
8392 use super::*;
8393 use serde_json::json;
8394
8395 fn store(name: &str) -> (PathBuf, FeltDb) {
8396 let directory = std::env::temp_dir().join(format!(
8397 "feltdb-growth-{name}-{}-{}",
8398 std::process::id(),
8399 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
8400 ));
8401 fs::create_dir_all(&directory).unwrap();
8402 let path = directory.join("state.log");
8403 let db = FeltDb::open(&path).unwrap();
8404 (path, db)
8405 }
8406
8407 fn reopen(path: &Path, db: FeltDb) -> FeltDb {
8408 drop(db);
8409 FeltDb::open(path).expect("the store reopens")
8410 }
8411
8412 fn tx(db: &FeltDb, id: &str, key: &str, value: Option<Value>) {
8413 db.apply_atomic_transaction(
8414 id,
8415 None,
8416 &[],
8417 &[AtomicMutation { capability: "notes".into(), key: key.into(), value }],
8418 None,
8419 )
8420 .unwrap();
8421 }
8422
8423 fn churn(db: &FeltDb, records: usize, rounds: usize) {
8426 for round in 0..rounds {
8427 for id in 0..records {
8428 let value = json!({ "id": id, "round": round, "body": "x".repeat(200) });
8431 if id % 2 == 0 {
8432 tx(db, &format!("t-{id}-{round}"), &format!("notes:{id}"), Some(value));
8433 } else if round == 0 {
8434 db.insert(&format!("notes:{id}"), value).unwrap();
8435 } else {
8436 db.update(&format!("notes:{id}"), value).unwrap();
8437 }
8438 }
8439 }
8440 for id in (0..records).step_by(4) {
8441 if id % 2 == 0 {
8442 tx(db, &format!("d-{id}"), &format!("notes:{id}"), None);
8443 } else {
8444 db.delete(&format!("notes:{id}")).unwrap();
8445 }
8446 }
8447 }
8448
8449 #[derive(Debug, PartialEq)]
8451 struct Meaning {
8452 state: String,
8453 revisions: u64,
8454 commits: usize,
8455 authority_revision: u64,
8456 sequence: u64,
8457 versions: HashMap<String, u64>,
8458 }
8459
8460 fn meaning(db: &FeltDb) -> Meaning {
8461 Meaning {
8462 state: db.state_digest().unwrap(),
8463 revisions: db
8464 .list_cardinalities()
8465 .unwrap()
8466 .into_iter()
8467 .find(|(name, _)| name == "state")
8468 .map(|(_, count)| count)
8469 .unwrap_or(0),
8470 commits: db.committed_transactions_after(0, usize::MAX).unwrap().len(),
8471 authority_revision: db.current_revision().unwrap(),
8472 sequence: db.sequence().unwrap(),
8473 versions: db.operation_versions().unwrap(),
8474 }
8475 }
8476
8477 fn retained(db: &FeltDb) -> usize {
8478 db.operations_since(&HashMap::new()).unwrap().len()
8479 }
8480
8481 #[test]
8482 fn a_standalone_store_keeps_a_window_of_operations_and_compaction_preserves_meaning() {
8483 let (path, db) = store("window");
8484 churn(&db, 3000, 3);
8485 let before = meaning(&db);
8486 let log_before = fs::metadata(&path).unwrap().len();
8487 assert!(retained(&db) > STANDALONE_OPERATION_WINDOW);
8488
8489 let outcome = db.maybe_compact_operation_log(&[]).unwrap();
8490 assert!(outcome.rewrote_log(), "{outcome:?}");
8491 assert_eq!(retained(&db), STANDALONE_OPERATION_WINDOW);
8492 assert_eq!(meaning(&db), before, "compaction changes no meaning");
8493 let log_after = fs::metadata(&path).unwrap().len();
8496 assert!(log_after < log_before, "{log_after} vs {log_before}");
8497 let current_rows: u64 = db.list_cardinalities().unwrap().iter().map(|(_, n)| n).sum();
8498 let lines = fs::read_to_string(&path).unwrap().lines().count() as u64;
8499 assert_eq!(
8500 lines,
8501 1 + STANDALONE_OPERATION_WINDOW as u64 + current_rows,
8502 "header + retained window + current rows: no superseded value, no tombstone"
8503 );
8504
8505 let db = reopen(&path, db);
8507 assert_eq!(meaning(&db), before);
8508 assert_eq!(retained(&db), STANDALONE_OPERATION_WINDOW, "retained operations survive reopen");
8509 assert!(db
8511 .apply_atomic_transaction("t-0-0", None, &[], &[AtomicMutation {
8512 capability: "notes".into(), key: "notes:0".into(), value: Some(json!({ "id": 0, "round": 0 })),
8513 }], None)
8514 .unwrap()
8515 .duplicate);
8516 db.insert("notes:new", json!({ "n": 1 })).unwrap();
8518 assert_eq!(db.current_revision().unwrap(), before.authority_revision + 1);
8519 let after_write = meaning(&db);
8520 let db = reopen(&path, db);
8521 assert_eq!(meaning(&db), after_write);
8522 }
8523
8524 #[test]
8525 fn interrupting_compaction_at_any_stage_before_the_switch_changes_nothing() {
8526 for stage in [Stage::CompactionWrite, Stage::CompactionMidWrite, Stage::CompactionSync, Stage::CompactionRename] {
8527 let (path, db) = store("stage");
8528 churn(&db, 2500, 2);
8529 let before = meaning(&db);
8530 let bytes = fs::read(&path).unwrap();
8531 arm(Fault::At(stage));
8532 let error = db.compact_operation_log(&[]);
8533 disarm();
8534 assert!(error.is_err(), "{stage:?}: the injected failure surfaced");
8535 assert_eq!(fs::read(&path).unwrap(), bytes, "{stage:?}: the log is untouched");
8536 assert!(!sibling(&path, "compact.tmp").exists(), "{stage:?}: no partial snapshot left");
8537 assert_eq!(meaning(&db), before, "{stage:?}");
8538 db.insert("notes:after", json!({ "after": true })).unwrap();
8540 db.compact_operation_log(&[]).unwrap();
8541 let expected = meaning(&db);
8542 let db = reopen(&path, db);
8543 assert_eq!(meaning(&db), expected, "{stage:?}");
8544 }
8545 }
8546
8547 #[test]
8548 fn a_switch_whose_directory_entry_is_not_durable_stops_writes_until_reopen() {
8549 let (path, db) = store("dirsync");
8550 churn(&db, 2500, 2);
8551 let before = meaning(&db);
8552 arm(Fault::At(Stage::CompactionDirectorySync));
8553 assert!(db.compact_operation_log(&[]).is_err());
8554 disarm();
8555 assert_eq!(db.health().writes.label(), "recovery-required");
8556 assert!(matches!(
8557 db.insert("notes:x", json!({})),
8558 Err(FlowError::Durability(failure)) if failure.kind == DurabilityFailureKind::RecoveryRequired
8559 ));
8560 assert_eq!(meaning(&db), before, "reads keep serving");
8561 let db = reopen(&path, db);
8562 assert_eq!(meaning(&db), before);
8563 db.insert("notes:x", json!({})).unwrap();
8564 }
8565
8566 #[test]
8567 fn a_process_killed_mid_compaction_leaves_the_old_log_authoritative() {
8568 for complete in [false, true] {
8571 let (path, db) = store("killed");
8572 churn(&db, 2500, 2);
8573 let before = meaning(&db);
8574 drop(db);
8575 let snapshot = sibling(&path, "compact.tmp");
8576 {
8577 let copy = path.with_file_name("copy.log");
8579 fs::copy(&path, ©).unwrap();
8580 let other = FeltDb::open(©).unwrap();
8581 other.compact_operation_log(&[]).unwrap();
8582 drop(other);
8583 let mut bytes = fs::read(©).unwrap();
8584 if !complete {
8585 bytes.truncate(bytes.len() / 2);
8586 }
8587 fs::write(&snapshot, bytes).unwrap();
8588 }
8589 let db = FeltDb::open(&path).unwrap();
8590 assert_eq!(meaning(&db), before, "complete={complete}");
8591 assert!(!snapshot.exists(), "the leftover snapshot was removed at open");
8592 }
8593 }
8594
8595 #[test]
8596 fn writes_during_repeated_compaction_are_all_preserved() {
8597 let (path, db) = store("concurrent");
8598 churn(&db, 2000, 2);
8599 let writer = {
8600 let db = db.clone();
8601 std::thread::spawn(move || {
8602 for i in 0..600 {
8603 db.insert(&format!("live:{i}"), json!({ "i": i })).unwrap();
8604 }
8605 })
8606 };
8607 let mut rewrites = 0;
8608 while !writer.is_finished() {
8609 if db.compact_operation_log(&[]).is_ok() {
8610 rewrites += 1;
8611 }
8612 std::thread::sleep(std::time::Duration::from_millis(20));
8614 }
8615 writer.join().unwrap();
8616 assert!(rewrites > 0);
8617 let expected = meaning(&db);
8618 let db = reopen(&path, db);
8619 assert_eq!(meaning(&db), expected);
8620 for i in 0..600 {
8621 assert!(db.get_value(&format!("live:{i}")).unwrap().is_some(), "live:{i}");
8622 }
8623 }
8624
8625 #[test]
8626 fn repeated_compaction_and_recovery_converge() {
8627 let (path, mut db) = store("repeat");
8628 for cycle in 0..5 {
8629 churn(&db, 1500, 2);
8630 db.compact_operation_log(&[]).unwrap();
8631 let expected = meaning(&db);
8632 db = reopen(&path, db);
8633 assert_eq!(meaning(&db), expected, "cycle {cycle}");
8634 db = reopen(&path, db);
8635 assert_eq!(meaning(&db), expected, "cycle {cycle}, second reopen");
8636 let log = fs::read(&path).unwrap();
8639 db.insert(&format!("cycle:{cycle}"), json!({})).unwrap();
8640 db.delete(&format!("cycle:{cycle}")).unwrap();
8641 db.compact_operation_log(&[]).unwrap();
8642 let again = fs::metadata(&path).unwrap().len() as i64;
8643 assert!((again - log.len() as i64).abs() < 4096, "cycle {cycle}: {again} vs {}", log.len());
8644 }
8645 }
8646
8647 #[test]
8648 fn backups_taken_before_and_after_compaction_restore_the_same_state() {
8649 let (path, db) = store("backup");
8650 churn(&db, 2500, 2);
8651 let directory = path.parent().unwrap().to_path_buf();
8652 db.write_backup(&directory.join("before.backup")).unwrap();
8653 let state = db.state_digest().unwrap();
8654 db.compact_operation_log(&[]).unwrap();
8655 db.write_backup(&directory.join("after.backup")).unwrap();
8656 for name in ["before", "after"] {
8657 let (restored, _) = FeltDb::restore_backup(
8658 &directory.join(format!("{name}.backup")),
8659 &directory.join(format!("{name}.log")),
8660 )
8661 .unwrap();
8662 assert_eq!(restored.state_digest().unwrap(), state, "{name}");
8663 }
8664 }
8665
8666 #[test]
8667 fn a_store_that_has_replicated_is_never_pruned_as_standalone() {
8668 let (path, db) = store("replicated");
8669 churn(&db, 2500, 2);
8670 let held = retained(&db);
8671 db.acknowledge_peer_versions("peer-1".into(), HashMap::new()).unwrap();
8672 assert!(matches!(db.compact_operation_log(&[]), Ok(0)));
8673 assert_eq!(retained(&db), held, "nothing pruned without the peer's acknowledgement");
8674 let db = reopen(&path, db);
8676 assert!(matches!(db.compact_operation_log(&[]), Ok(0)));
8677 }
8678
8679 #[test]
8680 fn a_log_rewritten_by_an_older_build_still_replays_as_before() {
8681 let (path, db) = store("legacy-header");
8684 churn(&db, 2500, 2);
8685 db.compact_operation_log(&[]).unwrap();
8686 drop(db);
8687 let text = fs::read_to_string(&path).unwrap();
8688 let (header, rest) = text.split_once('\n').unwrap();
8689 let mut header: Value = serde_json::from_str(header).unwrap();
8690 header.as_object_mut().unwrap().remove("authority_revision");
8691 fs::write(&path, format!("{header}\n{rest}")).unwrap();
8692 let first = FeltDb::open(&path).unwrap();
8693 let revision = first.current_revision().unwrap();
8694 let state = first.state_digest().unwrap();
8695 let first = reopen(&path, first);
8696 assert_eq!(first.current_revision().unwrap(), revision, "deterministic");
8697 assert_eq!(first.state_digest().unwrap(), state);
8698 }
8699}