Skip to main content

feltdb/
lib.rs

1//! # FeltDB
2//!
3//! A durable, embedded state store. Records live under a *capability* (the part of a key before
4//! the first `:`, so `"orders:o-1"` is record `o-1` of capability `orders`). Every write goes
5//! through a durable log; several records can be changed in one atomic transaction; an update can
6//! be made conditional on the version you read; and a transaction can carry an identity so that a
7//! retry after an uncertain outcome is recognized instead of applied twice.
8//!
9//! ```
10//! use feltdb::{AtomicMutation, FeltDb, FlowError, PayloadBinding, RecordPrecondition};
11//! use serde_json::{json, Value};
12//!
13//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! let dir = tempfile::tempdir()?;
15//! let path = dir.path().join("app.db");
16//! let db = FeltDb::open(&path)?;
17//!
18//! // Create a record only if it is absent: a second create is refused, and refused in a way a
19//! // caller can branch on.
20//! db.insert_if_absent("orders:o-1", json!({ "status": "pending" }))?;
21//! assert!(matches!(
22//!     db.insert_if_absent("orders:o-1", json!({})),
23//!     Err(FlowError::PreconditionFailed(_))
24//! ));
25//!
26//! // Change several records atomically, and name the transaction so a retry is safe. The payload
27//! // hash is yours to compute: FeltDB stores it and compares it for equality.
28//! let ship = |hash: &str| {
29//!     db.apply_atomic_transaction_content_addressed(
30//!         "ship-o-1",
31//!         Some(hash),
32//!         None,
33//!         &[],
34//!         &[
35//!             AtomicMutation {
36//!                 capability: "orders".into(),
37//!                 key: "orders:o-1".into(),
38//!                 value: Some(json!({ "status": "shipped" })),
39//!             },
40//!             AtomicMutation {
41//!                 capability: "audit".into(),
42//!                 key: "audit:a-1".into(),
43//!                 value: Some(json!({ "order": "o-1", "event": "shipped" })),
44//!             },
45//!         ],
46//!         None,
47//!     )
48//! };
49//! let first = ship("sha256:payload-v1")?;
50//! assert!(!first.duplicate);
51//!
52//! // The same request again is recognized as the original, not applied a second time.
53//! let retry = ship("sha256:payload-v1")?;
54//! assert!(retry.duplicate);
55//! assert_eq!(retry.payload_binding, Some(PayloadBinding::Verified));
56//! assert_eq!(retry.commit_revision, first.commit_revision);
57//!
58//! // The same id with a different payload is refused, with a typed error. Nothing is written.
59//! assert!(matches!(
60//!     ship("sha256:payload-v2"),
61//!     Err(FlowError::TransactionIdPayloadMismatch { .. })
62//! ));
63//!
64//! // Close and reopen: what was acknowledged is there.
65//! drop(db);
66//! let db = FeltDb::open(&path)?;
67//! assert_eq!(db.get::<Value>("orders:o-1")?, Some(json!({ "status": "shipped" })));
68//! # let _ = RecordPrecondition::default();
69//! # Ok(())
70//! # }
71//! ```
72//!
73//! ## What is supported
74//!
75//! Only the items in this table are covered by the compatibility policy
76//! ([`docs/reference/feltdb-rust-api.md`] in the repository). While the version is `0.x`, a breaking
77//! change to them is a minor-version bump and is recorded in the changelog; everything else may
78//! change in any release.
79//!
80//! | To | Use |
81//! | --- | --- |
82//! | open, recover, reopen | [`FeltDb::open`], [`FeltDb::log_recovery`], [`FeltDb::health`] |
83//! | read | [`FeltDb::get`], [`FeltDb::query`] |
84//! | write unconditionally | [`FeltDb::insert`], [`FeltDb::update`], [`FeltDb::delete`] (all three are plain writes: see below) |
85//! | create only if absent | [`FeltDb::insert_if_absent`] |
86//! | update only if unchanged | [`FeltDb::compare_and_set_json`] → [`JsonCasResult`] |
87//! | change several records atomically | [`FeltDb::apply_atomic_transaction`], [`FeltDb::apply_atomic_transaction_guarded`] with [`AtomicMutation`] and [`RecordPrecondition`] → [`AtomicCommit`] |
88//! | make a retry safe | [`FeltDb::apply_atomic_transaction_content_addressed`], [`FeltDb::has_applied_transaction`], [`FeltDb::applied_transaction_payload_hash`], [`PayloadBinding`] |
89//! | tell failures apart | [`FlowError`] and its variants, [`PreconditionFailure`] |
90//! | a durable authority for several processes | the [`authority`] module (Unix) |
91//!
92//! **Behavior to know before relying on it.**
93//! `insert`, `update` and `delete` are unconditional: `insert` replaces an existing record, `update`
94//! of a missing key *creates* it, and `delete` of a missing key succeeds. A fenced write
95//! ([`FeltDb::compare_and_set_json`], a [`RecordPrecondition`] with `expected_version`) treats a record
96//! that has never been through one as version 1, and a record's `__version` becomes visible in its JSON
97//! after its first fenced write. A transaction refused for a failed precondition writes nothing.
98//! A transaction id committed *without* a payload hash can be retried (it is a duplicate, with no
99//! [`PayloadBinding`]), but retrying it *with* a hash is refused, because there is nothing to verify against.
100//!
101//! ## Durability
102//!
103//! An acknowledged write survives the process being killed. Whether it survives the machine losing
104//! power depends on the operating system and drive honouring a flush, and is **not** established.
105//! A log ending in an incomplete append is recovered by discarding that append, which was never
106//! acknowledged: [`FeltDb::log_recovery`] says so. A corrupt log, or one in a format this build does
107//! not understand, refuses to open and is left as found.
108//!
109//! ## Stability tiers
110//!
111//! * **Supported**: the table above.
112//! * **Experimental**: [`agent_state`], [`agent_transition_contract`], the deterministic-contract
113//!   adapters, [`semantic_decision`] and [`semantic_query`]. Public so they can be used, but they
114//!   may change shape before they are stabilized.
115//! * **Not yet classified**: the other public modules. They are public today and carry no promise;
116//!   each is listed with the decision still needed in the repository's Rust API audit.
117//! * **Internals**: implementation, fault-injection and diagnostic modules are private unless the
118//!   `unstable-internals` feature is enabled, and nothing behind that feature is covered by any promise.
119//!
120//! `FlowError` is `#[non_exhaustive]`: match on its variants, always with a catch-all arm, and never
121//! on message text.
122//!
123//! [`docs/reference/feltdb-rust-api.md`]: https://github.com/rkendel1/flow_db/blob/main/docs/reference/feltdb-rust-api.md
124
125macro_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/// Why an operation failed.
480///
481/// Branch on the variant, never on message text. The enum is `#[non_exhaustive]`: a later release
482/// may add variants, so a `match` needs a catch-all arm. `CapabilityError` carries a human-readable
483/// message, some of which begin with an upper-case code (`INVALID_READ_SET:`, `EMPTY_PRECONDITION:`);
484/// those prefixes are **not** a supported way to tell failures apart and may change.
485#[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    /// A transaction precondition did not hold. Nothing was written.
493    ///
494    /// Separate from `CapabilityError` because a conflict is an expected
495    /// outcome of a race and a caller is meant to branch on it, while a
496    /// capability error is not.
497    PreconditionFailed(Box<PreconditionFailure>),
498    /// A transaction id was already committed with a **different** payload. Nothing was written, and
499    /// the id still names the original transaction.
500    ///
501    /// Returned when a payload hash is supplied for an id that was committed with another one, and
502    /// also when an id that was committed *without* a hash is retried *with* one, since there is
503    /// nothing recorded to verify against. Retrying with the same id cannot succeed: use a new id
504    /// for a new transaction, or resend the original payload to confirm the original.
505    TransactionIdPayloadMismatch {
506        transaction_id: String,
507    },
508    /// The transaction was based on a revision which is no longer current.
509    RevisionConflict {
510        expected: u64,
511        actual: u64,
512    },
513    /// A backup artifact was rejected. Nothing was restored.
514    BackupRejected(Box<BackupProblem>),
515    /// The durable database is in a format this build does not understand.
516    ///
517    /// Returned by [`FeltDb::open`] **before any record is interpreted**, and
518    /// before anything is written. A database that returns this error is
519    /// untouched.
520    IncompatibleFormat(Box<FormatIncompatibility>),
521    /// A write could not be made durable, or was refused because an earlier one
522    /// left the durable log in a state this process can no longer vouch for.
523    ///
524    /// [`DurabilityFailure::committed`] says what is known about the write:
525    /// `Some(false)` means nothing of it is durable or visible, `None` means the
526    /// outcome is unknown until the database is reopened.
527    Durability(Box<DurabilityFailure>),
528}
529
530/// Why a write did not become durable.
531#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
532#[serde(rename_all = "snake_case")]
533#[non_exhaustive]
534pub enum DurabilityFailureKind {
535    /// The volume or the process's file-size or quota limit refused the bytes
536    /// (`ENOSPC`, `EFBIG`, `EDQUOT`). Rolled back; nothing was committed.
537    StorageFull,
538    /// Writing the record failed for another I/O reason. Rolled back.
539    WriteFailed,
540    /// The record was written but the stable-storage barrier failed. Rolled
541    /// back: the bytes were removed and the removal itself was made durable.
542    SyncFailed,
543    /// The write failed **and** removing its bytes failed. The record may or
544    /// may not be in the durable log; reopening the database decides. The
545    /// database refuses further writes until it is reopened.
546    Indeterminate,
547    /// Refused before anything was written: an earlier write left the log
548    /// indeterminate, and this process stopped writing. Reopen the database.
549    RecoveryRequired,
550}
551
552impl DurabilityFailureKind {
553    /// The stable code every surface (Rust, HTTP, TypeScript) reports.
554    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/// A write that did not become durable, and what is known about its outcome.
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567pub struct DurabilityFailure {
568    pub kind: DurabilityFailureKind,
569    /// `Some(false)`: not committed, not visible, safe to retry as a new write.
570    /// `None`: unknown until reopen; retry only with the same transaction id.
571    pub committed: Option<bool>,
572    /// The operating-system error, for an operator. Not for branching.
573    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)] // Reserved runtime subsystems are initialized before their public APIs land.
655struct 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    /// Monotonic, authority-assigned commit order. Unlike `sequence`, one
664    /// transaction advances this exactly once regardless of its mutation count.
665    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    /// Recent commits, and who asked for them.
680    ///
681    /// Every transaction already records its actor: the audit document written
682    /// into the durable log carries `subject`, tenant, application and
683    /// revision. Until this existed that document was written and never read
684    /// back — replay parsed the record and discarded the audit — so the
685    /// database could say *what* changed and never *who changed it*.
686    ///
687    /// Bounded on purpose. This answers "who did this, recently", which is the
688    /// question a human asks while operating a running system; it is not an
689    /// audit archive, and the durable log remains the record of what happened.
690    /// Keeping it bounded is what makes it safe to maintain on the commit path.
691    transaction_attributions: VecDeque<TransactionAttribution>,
692    /// Complete committed transaction order, rebuilt exclusively from the
693    /// durable transaction log. Unlike the bounded operator window above this
694    /// is the canonical replay/provenance index.
695    transaction_history: Vec<TransactionAttribution>,
696    collection_cardinality: HashMap<String, u64>,
697    /// Derived equality index over `rows`. Never durable, never replicated, and
698    /// never consulted for anything but candidate selection: it is maintained
699    /// inside the same state mutation boundary as `rows` and rebuilt from
700    /// `rows` after recovery.
701    equality_index: EqualityIndex,
702    /// What replaying the durable log had to do to succeed.
703    ///
704    /// Recorded rather than logged, so an operator can ask after the fact
705    /// whether this database opened cleanly.
706    log_recovery: LogRecovery,
707    /// The durable format this database was accepted as, recorded at the open
708    /// that accepted it.
709    durable_format: DurableFormat,
710    /// How far a single-record write is pushed before it returns.
711    durability_mode: DurabilityMode,
712    /// Writes since the last stable-storage barrier, for grouped durability.
713    writes_since_barrier: u32,
714    /// Set when a failed write could not be rolled back. From then on this
715    /// process cannot say what the durable log holds, so it refuses writes
716    /// until the database is reopened and replay decides. Never cleared in
717    /// place.
718    write_fault: Option<String>,
719    /// Size of the durable log right after its last rewrite (0 until one
720    /// happens in this process). A standalone store rewrites only once the log
721    /// has doubled since, which bounds disk to about twice the compacted size
722    /// and keeps rewrite cost proportional to what was appended.
723    log_bytes_after_rewrite: u64,
724    /// When a policy-driven compaction should rewrite the durable log.
725    compaction_policy: CompactionPolicy,
726    /// Operations pruned since the last log rewrite.
727    pruned_since_rewrite: usize,
728    /// Each resource's newest revision, as `(id, sequence)`.
729    ///
730    /// Derived from the `state` rows exactly like `equality_index` is derived
731    /// from `rows`: never durable, rebuilt after recovery. It exists so that
732    /// minting a revision does not have to scan a resource's history to find
733    /// the parent it descends from.
734    revision_heads: HashMap<String, (state_model::StateId, u64)>,
735    /// Each collection's write version: the value of `collection_clock` when a
736    /// record of it was last written, replaced or removed.
737    ///
738    /// Derived, in memory only, never replicated; rebuilt by replay because
739    /// replay writes through the same mutation boundary. It answers one
740    /// question inside the commit lock in O(1): has anything in this
741    /// collection changed since a decision read it? Values never repeat, so a
742    /// changed-and-changed-back collection never looks unchanged.
743    collection_versions: HashMap<String, u64>,
744    collection_clock: u64,
745}
746
747/// The state lock, held with its wait and hold time attributable.
748///
749/// FeltDB serializes every read and every write behind one mutex, so "how long
750/// did an operation take" and "how long did it make everything else wait" are
751/// different questions. PR34 showed the second one mattered: a query holding the
752/// lock while it cloned a collection inflated the cost of unrelated writes.
753///
754/// This guard answers both. It records the time spent *waiting* to acquire —
755/// which is pure contention, the one span where no work happens at all — and the
756/// time spent *holding*, which bounds how long anything else can be forced to
757/// wait. Both are off unless `FELTDB_WORKLOAD_DIAGNOSTICS=1`, where acquisition
758/// costs one relaxed atomic load and no clock read.
759///
760/// It dereferences to [`Inner`], so every existing use site reads unchanged.
761struct 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
790/// The single state mutation boundary for authoritative records.
791///
792/// Every path that changes a record — insert, update, delete, CAS, transaction
793/// commit, replicated operation apply, log replay, snapshot install — goes
794/// through these three methods, and they change the record map and the derived
795/// equality index together while the caller still holds the state lock. That is
796/// the whole atomicity argument: there is no window in which a committed record
797/// exists without its index entries, or an index entry survives its record,
798/// because no code path can write one without the other.
799///
800/// The index is deliberately maintained here rather than through an independent
801/// index API. An independent API would have to be called, correctly, by nine
802/// separate mutation paths; a missed call would be a silent divergence that only
803/// a query would reveal. Routing every write through one boundary makes the
804/// omission impossible to write in the first place.
805impl Inner {
806    /// Write one record, replacing any record already at its key.
807    ///
808    /// Returns `true` when this created a record rather than replacing one, which
809    /// is what collection cardinality accounting needs.
810    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        // From here on the work exists only because an index does. Measuring it
829        // apart from the record write is what makes "did index maintenance
830        // become a write-side cost?" a question with an answer.
831        let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
832        // Old entries first, then new ones. Doing it in this order means a
833        // record whose indexed field did not change is removed from and
834        // re-added to the same bucket, which is a no-op rather than a gap.
835        if let Some(previous) = &previous {
836            self.equality_index
837                .remove_record(&capability, &key, &previous.value);
838        }
839        // Disjoint field borrows: the record map is read while the index is
840        // written. Nothing is cloned to maintain the index.
841        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, &current.value);
848        }
849        previous.is_none()
850    }
851
852    /// Remove one record. Returns `true` when a record was actually removed.
853    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    /// Advance a collection's write version (see `collection_versions`).
876    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    /// Drop all records, as a snapshot install does. The index follows.
883    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/// What must be true of one record for a transaction to commit.
908///
909/// Distinct from `AtomicPrecondition`, and deliberately so. That one compares
910/// the row's internal operation *sequence*, and `None` on it means "this record
911/// must not exist". This one compares the fields a caller can actually read
912/// back -- the document's `__version`, its authority epoch, its lease -- which
913/// are the same fields single-record CAS compares. Plumbing a caller's
914/// `expectedVersion` into the sequence check would have compared a document
915/// version against a storage sequence: two different numbers that happen to
916/// share a name.
917///
918/// Every predicate is optional and only a supplied one is checked. That differs
919/// from CAS, where an absent `expected_lease_id` asserts the record is *not*
920/// leased; here an absent predicate asserts nothing at all. The difference is
921/// deliberate -- a transaction precondition is a fence the caller opts into,
922/// not a full description of the record -- and it is why the fields are named
923/// `expected_*` rather than describing a state.
924#[derive(Debug, Clone, Default, Serialize, Deserialize)]
925pub struct RecordPrecondition {
926    pub capability: String,
927    pub key: String,
928    /// The record must not exist. Mutually exclusive with the predicates below.
929    #[serde(default)]
930    pub require_absent: bool,
931    /// The document's `__version`, as `updateIfVersion` compares it.
932    #[serde(default)]
933    pub expected_version: Option<u64>,
934    /// The record's authority epoch, at `/authority/epoch`.
935    #[serde(default)]
936    pub expected_epoch: Option<u64>,
937    /// The id of an unexpired lease held on the record.
938    #[serde(default)]
939    pub expected_lease_id: Option<String>,
940}
941
942impl RecordPrecondition {
943    /// True when this precondition asks for nothing, which is a caller error
944    /// rather than a satisfied fence: a precondition that constrains nothing
945    /// would read as protection and provide none.
946    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/// State a decision was read from: records by identity, and equality
955/// predicates over a collection (every record of `collection` whose fields
956/// equal these values, including records that do not exist yet).
957///
958/// Generic: it names collections, ids, fields, and values, never what they mean.
959#[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/// A read set and what the authority held for it at one moment.
987///
988/// The token is opaque and meaningful only to the authority that issued it. A
989/// transaction fenced by a basis commits only if re-reading the same set,
990/// inside the commit's lock, yields the same token.
991#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
992pub struct ReadBasis {
993    pub reads: ReadSet,
994    pub token: String,
995}
996
997/// The `key` of a collection-fence refusal (`PreconditionFailure::Fence`).
998pub(crate) const COLLECTION_FENCE_KEY: &str = "collection-version";
999
1000/// Rows read under one acquisition of the state lock, with the write version
1001/// each whole collection read had at that moment (`collection_versions`).
1002pub(crate) struct ConsistentRows {
1003    pub(crate) view: HashMap<String, BTreeMap<String, StoredRow>>,
1004    pub(crate) versions: HashMap<String, u64>,
1005}
1006
1007/// Token prefix for read bases this authority issues.
1008pub const READ_BASIS_PREFIX: &str = "fr1:";
1009/// Largest number of records plus predicates one read set may name.
1010pub const READ_SET_LIMIT: usize = 1_000;
1011
1012impl ReadSet {
1013    /// Validate, de-duplicate, and order, so two descriptions of the same reads
1014    /// are one read set and one token.
1015    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/// Where a read set's logical collections and ids live in storage.
1081///
1082/// One read set, one token semantics; only the storage addressing differs
1083/// between the routes that commit transactions.
1084#[derive(Debug, Clone, PartialEq, Eq)]
1085pub enum ReadLayout {
1086    /// The direct transaction route: rows under `collection`, keyed `collection:id`.
1087    Direct,
1088    /// An application state namespace: rows under `namespace:collection`, keyed by id.
1089    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    /// The authority scope a token is valid in. A token is never evaluated in
1108    /// another scope: it is refused as invalid there, not compared.
1109    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
1127/// What the state a read set names is, now, as one token. Rows are read in
1128/// full; tombstoned rows are absent; only the named records and the named
1129/// collections' matching rows are read.
1130fn 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/// Which predicate failed, and what the authority actually holds.
1183///
1184/// Structured rather than a formatted string, because the caller has to tell a
1185/// lost race from a broken deployment: the first is retried with fresh state,
1186/// the second is not retried at all.
1187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1188#[serde(rename_all = "camelCase", tag = "predicate")]
1189pub enum PreconditionFailure {
1190    /// Required absent, but the record exists.
1191    Present { collection: String, key: String },
1192    /// A predicate was given for a record that does not exist.
1193    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    /// State named by a read basis changed after the basis was read.
1213    ///
1214    /// The tokens are kept for diagnostics inside the authority but never
1215    /// serialized: a refusal says that the basis changed, not what it digests.
1216    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    /// The storage key, which is namespaced as `collection:id`.
1238    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    /// The refusal as every transport serializes it: `predicate`, the
1252    /// application's `collection` (never a storage namespace), the record `id`,
1253    /// its `key` as `collection:id`, and `expected` / `actual` where the
1254    /// predicate has them. A fence names the first fenced collection and
1255    /// `key: "read-basis"`, with no `id` and no tokens.
1256    ///
1257    /// `layout` is where the authority addressed the record: the direct route
1258    /// keys rows `collection:id` under `collection`; an application namespace
1259    /// keys them by id under `namespace:collection`.
1260    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    /// How a duplicate was recognized. Present only on a duplicate, and only
1344    /// when the caller's request could be compared with what was committed
1345    /// (`Verified`) or was deliberately judged by id alone (`LegacyUnverified`).
1346    #[serde(default, skip_serializing_if = "Option::is_none")]
1347    pub payload_binding: Option<PayloadBinding>,
1348}
1349
1350/// What a duplicate transaction id was checked against.
1351#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1352#[serde(rename_all = "snake_case")]
1353pub enum PayloadBinding {
1354    /// The id and the payload hash both match what was committed: this request
1355    /// is the original one.
1356    Verified,
1357    /// The id was committed without a payload hash, so only the id could be
1358    /// checked. The caller must not read this as proof the payload matches.
1359    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}
1393/// How many recent commits keep their attribution in memory.
1394///
1395/// Sized to cover what an operator reads in one sitting rather than to be a
1396/// log. A larger window costs memory on every instance to answer a question the
1397/// durable log already answers more completely.
1398const TRANSACTION_ATTRIBUTION_WINDOW: usize = 512;
1399
1400/// How many of its newest operations a standalone store (one no peer has ever
1401/// acknowledged) keeps in its operation log. See `ChangeLog::prune_standalone`
1402/// and docs/reference/storage-growth.md.
1403pub const STANDALONE_OPERATION_WINDOW: usize = 4096;
1404
1405/// How many mutated keys one attribution names before it stops listing them.
1406///
1407/// A bulk transaction can touch thousands of records; naming them all would
1408/// make one commit's attribution larger than the rest of the window. The count
1409/// is always exact, so a truncated list is visible as a truncated list.
1410const TRANSACTION_ATTRIBUTION_KEYS: usize = 32;
1411
1412/// Who committed a transaction, and what it touched.
1413///
1414/// Projected from the audit document the transaction already writes. It carries
1415/// no record values: attribution answers *who, when, and to what*, and the
1416/// values are reachable through the record and revision APIs that authorize
1417/// each read.
1418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1419pub struct TransactionAttribution {
1420    pub transaction_id: String,
1421    /// Stable operation identities assigned to the rows in this transaction.
1422    /// These are distinct from both transaction and revision identity.
1423    #[serde(default)]
1424    pub operation_ids: Vec<String>,
1425    /// The actor the caller authenticated as, when the transaction named one.
1426    pub subject: Option<String>,
1427    pub tenant_id: Option<String>,
1428    pub application_id: Option<String>,
1429    /// The application contract revision the transaction was executed against.
1430    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    /// `capability:key` for each mutation, truncated at
1436    /// [`TRANSACTION_ATTRIBUTION_KEYS`].
1437    pub keys: Vec<String>,
1438    /// How many mutations the transaction made, whether or not `keys` lists
1439    /// them all.
1440    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    /// Milliseconds since the epoch, matching `StoredRow::unix_ms`.
1452    pub unix_ms: u128,
1453    /// Canonical transaction metadata exactly as committed. Record values are
1454    /// deliberately absent; stable work/session/agent/operation references
1455    /// live here so historical provenance survives process loss.
1456    #[serde(default)]
1457    pub audit: Option<Value>,
1458}
1459
1460impl TransactionAttribution {
1461    /// Builds an attribution from a transaction record's own audit document.
1462    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    /// Whether this transaction touched a record, as far as `keys` records it.
1512    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
1518/// Upgrade compacted attribution written before operation identities became a
1519/// first-class part of canonical history. The mapping is deterministic from
1520/// durable transaction data, so reopening never regenerates a different ID.
1521fn 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
1543/// Records an attribution, discarding the oldest once the window is full.
1544fn 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/// An event representing a change to the database (insert or update of a record).
1614///
1615/// This event is broadcast to all live query subscribers so they can update their results.
1616#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1617pub struct ChangeEvent {
1618    /// The capability namespace of the changed record.
1619    pub capability: String,
1620    /// The key of the changed record.
1621    pub key: String,
1622    /// The Rust type name of the changed record.
1623    pub rust_type: String,
1624    /// The timestamp of the change in milliseconds since UNIX epoch.
1625    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    /// Content hash for deduplication and content addressing
1636    #[serde(default)]
1637    pub content_hash: Option<String>,
1638    /// Reference identity if this row represents a referenced object
1639    #[serde(default)]
1640    pub flow_ref: Option<FlowRef>,
1641    /// Tombstone marker used to durably replay deletions.
1642    #[serde(default)]
1643    pub deleted: bool,
1644    /// Canonical mutation which produced this row. Older logs remain readable.
1645    #[serde(default)]
1646    pub operation: Option<Operation>,
1647}
1648
1649/// When a compaction should actually rewrite the durable log.
1650///
1651/// Pruning acknowledged operations from memory is cheap. **Rewriting the log is
1652/// not**: it serializes every row in the database and fsyncs it while holding
1653/// the one lock that serializes every read and every write. Doing that on a
1654/// short timer stops the world on that timer.
1655///
1656/// The policy separates the two. Pruning still happens whenever it can; the
1657/// rewrite waits until enough has accumulated to be worth the stall.
1658#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1659pub struct CompactionPolicy {
1660    /// Operations that must have been pruned since the last rewrite before the
1661    /// next one is worth its cost.
1662    pub rewrite_after_pruned: usize,
1663}
1664
1665impl Default for CompactionPolicy {
1666    fn default() -> Self {
1667        // Chosen so a busy database rewrites occasionally rather than
1668        // constantly. At one operation per write this is one rewrite per
1669        // thousand acknowledged writes instead of one every timer tick.
1670        CompactionPolicy {
1671            rewrite_after_pruned: 1000,
1672        }
1673    }
1674}
1675
1676/// What a policy-driven compaction did.
1677#[derive(Debug, Clone, PartialEq, Eq)]
1678pub enum CompactionOutcome {
1679    /// Nothing was acknowledged by every active peer; there was no work.
1680    Idle,
1681    /// Operations were pruned from memory and the log rewrite was **deferred**,
1682    /// because the accumulated gain does not yet justify the stall.
1683    Deferred {
1684        pruned: usize,
1685        pending_since_rewrite: usize,
1686    },
1687    /// The durable log was rewritten.
1688    Rewritten { pruned: usize },
1689}
1690
1691impl CompactionOutcome {
1692    /// Operations pruned from the in-memory log by this call.
1693    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    /// Whether this call rewrote the durable log.
1702    pub fn rewrote_log(&self) -> bool {
1703        matches!(self, CompactionOutcome::Rewritten { .. })
1704    }
1705}
1706
1707/// How far a single-record write is pushed before it returns.
1708///
1709/// This is a **contract**, not a tuning knob: it decides what a successful
1710/// mutation means. Nothing here makes a claim about surviving power loss —
1711/// see [`DurabilityMode::guarantee`] for what each mode does and does not
1712/// establish.
1713#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1714pub enum DurabilityMode {
1715    /// Write and flush to the operating system. **The default.**
1716    ///
1717    /// The bytes leave the process before the call returns, so a committed
1718    /// write survives the process dying. They are not forced to the device.
1719    Flushed,
1720    /// Write, flush, and `fsync` before returning.
1721    ///
1722    /// Issues a stable-storage barrier per mutation. Whether that barrier
1723    /// actually reaches the platter depends on the filesystem, the mount
1724    /// options and the drive's write cache — none of which this database can
1725    /// observe, so this mode buys a barrier, not a proof.
1726    Synced,
1727    /// Flush every write, and `fsync` once every `every` writes.
1728    ///
1729    /// Amortises the barrier. The cost is an explicit window: up to `every - 1`
1730    /// acknowledged writes may not yet have been through one.
1731    Grouped { every: u32 },
1732}
1733
1734impl Default for DurabilityMode {
1735    fn default() -> Self {
1736        DurabilityMode::Flushed
1737    }
1738}
1739
1740impl DurabilityMode {
1741    /// What a successful mutation guarantees under this mode, stated so that a
1742    /// caller can read the contract rather than infer it from an API name.
1743    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    /// How many acknowledged writes may not yet have been through a barrier.
1758    ///
1759    /// `None` where no barrier is issued at all.
1760    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/// What is actually known about this database's durable storage.
1770///
1771/// Every variant corresponds to a fact the open established. There is no
1772/// variant meaning "probably fine", and none that says anything about power
1773/// loss — that is unproven, and health may not manufacture a guarantee the
1774/// database does not have.
1775#[derive(Debug, Clone, PartialEq, Eq)]
1776pub enum StorageHealth {
1777    /// Every durable byte replayed as a complete, valid record.
1778    Clean,
1779    /// Operational, **and the last write did not land**.
1780    ///
1781    /// The log ended in an incomplete append, which was discarded. The database
1782    /// is usable and is not in the same condition as one that replayed cleanly,
1783    /// and reporting them identically is what makes an operator unable to tell
1784    /// a healthy database from one that lost its most recent write.
1785    RecoveredIncompleteWrite {
1786        byte_offset: u64,
1787        discarded_bytes: usize,
1788    },
1789}
1790
1791impl StorageHealth {
1792    /// Whether the durable log replayed without discarding anything.
1793    pub fn is_clean(&self) -> bool {
1794        matches!(self, StorageHealth::Clean)
1795    }
1796
1797    /// A short stable label for an operational surface.
1798    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/// What this database knows about itself, as facts rather than labels.
1823///
1824/// Deliberately small. It reports only conditions the open actually
1825/// established, and it is not an operations dashboard: replication health,
1826/// backup freshness and capacity are not here, because none of them has a
1827/// defined operational contract yet and health must not invent one.
1828#[derive(Debug, Clone, PartialEq, Eq)]
1829pub struct DatabaseHealth {
1830    /// The durable format this database was accepted as.
1831    ///
1832    /// A database in a format this build does not understand never opens, so
1833    /// reaching this at all is itself the compatibility fact.
1834    pub durable_format: DurableFormat,
1835    /// What replaying the durable log had to do.
1836    pub recovery: LogRecovery,
1837    /// The storage condition that follows from it.
1838    pub storage: StorageHealth,
1839    /// Whether this process can still accept writes.
1840    pub writes: WriteAvailability,
1841}
1842
1843/// Whether a database can accept writes, as this process knows it.
1844#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1845#[serde(tag = "state", rename_all = "kebab-case")]
1846pub enum WriteAvailability {
1847    /// Writes are accepted.
1848    Available,
1849    /// A failed write could not be rolled back, so what the durable log holds
1850    /// is unknown to this process. Every write is refused with
1851    /// `RECOVERY_REQUIRED` until the database is reopened; reads keep serving
1852    /// every acknowledged write. Reopening replays the log and decides.
1853    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    /// Whether this database is in the condition a clean open produces.
1867    ///
1868    /// **Not** a claim that the database is correct, replicated, backed up or
1869    /// durable against power loss. It says the durable log replayed with
1870    /// nothing discarded, and nothing more.
1871    pub fn is_nominal(&self) -> bool {
1872        self.storage.is_clean() && self.writes == WriteAvailability::Available
1873    }
1874}
1875
1876/// The backup artifact format this build writes and understands.
1877///
1878/// Versioned separately from the on-disk database: a backup is an artifact an
1879/// operator keeps, moves and restores long after the database that produced it,
1880/// so its compatibility boundary is its own.
1881pub const BACKUP_VERSION: u32 = 1;
1882
1883const BACKUP_RECORD_TYPE: &str = "feltdb.backup.v1";
1884
1885/// The first line of a backup artifact. Everything needed to verify the rest.
1886#[derive(Debug, Clone, Serialize, Deserialize)]
1887struct BackupHeader {
1888    record_type: String,
1889    backup_version: u32,
1890    /// The durable format the rows are in.
1891    format_version: u32,
1892    created_unix_ms: u64,
1893    row_count: usize,
1894    observed_versions: HashMap<String, u64>,
1895    /// Digest of the rows exactly as written. Detects any alteration.
1896    content_digest: String,
1897    /// Digest of what the database *means* — see [`BackupVerification`].
1898    meaning_digest: String,
1899}
1900
1901/// Why a backup artifact was rejected.
1902#[derive(Debug, Clone, PartialEq, Eq)]
1903pub struct BackupProblem {
1904    /// What is wrong, in terms of the artifact rather than the parser.
1905    pub reason: String,
1906    /// Where, when the problem has a location.
1907    pub line_number: Option<usize>,
1908    /// What the operator can do.
1909    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/// What a backup artifact contains, established by reading it.
1922///
1923/// `meaning_digest` is the part that matters. A content digest proves the file
1924/// is intact; it does not prove that restoring it produces the same database.
1925/// The meaning digest covers what the database *is*, independent of how it was
1926/// written down:
1927///
1928/// - every live record: capability, key and canonical value;
1929/// - every revision: its resource, identity, parent, sequence and content
1930///   identity, and the authority that produced it;
1931/// - every resource's retention policy and horizon.
1932///
1933/// It deliberately excludes wall-clock timestamps and sync envelopes, which
1934/// differ between two databases that mean the same thing. Restore recomputes it
1935/// from the **restored database** and refuses if it disagrees with the
1936/// artifact, so a restore proves equivalence rather than mere parseability.
1937#[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
1955/// Digest the rows exactly as they will be written.
1956fn 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
1965/// Digest what a set of rows *means*, independent of how it was written.
1966///
1967/// Canonical by construction: rows are ordered by capability and key, and only
1968/// the fields that carry meaning take part. Two databases with the same digest
1969/// hold the same records, the same revision graph and the same retention state.
1970fn 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
1997/// Read and check a backup artifact **without opening it as a database**.
1998///
1999/// Structural and semantic checks only, and it never writes: verifying a backup
2000/// must be something an operator can do to an artifact they are not yet willing
2001/// to restore.
2002pub 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/// Where durable corruption was found, and what is wrong with it.
2104///
2105/// Carries a location so an operator can find the damage, and an excerpt so
2106/// they can recognise it, without the error having to quote a whole record.
2107#[derive(Debug, Clone, PartialEq, Eq)]
2108pub struct LogCorruption {
2109    /// 1-based line number in the durable log.
2110    pub line_number: usize,
2111    /// Byte offset of the start of the record.
2112    pub byte_offset: u64,
2113    /// What is wrong, in terms of the record rather than the parser.
2114    pub reason: String,
2115    /// The beginning of the record, truncated.
2116    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/// What replaying the durable log had to do to succeed.
2131///
2132/// A clean open and an open that discarded an incomplete final append are
2133/// different facts, and the difference must survive past an internal debug
2134/// message: an operator needs to know their database opened *and* that its last
2135/// write did not land.
2136#[derive(Debug, Clone, PartialEq, Eq)]
2137pub enum LogRecovery {
2138    /// Every durable byte was a complete, valid record.
2139    Clean,
2140    /// The log ended in an incomplete append.
2141    ///
2142    /// The final bytes carried no record terminator, so they cannot be a record
2143    /// this database finished writing. They were not replayed, and the log was
2144    /// truncated back to the last complete record so that the next append does
2145    /// not splice onto a partial line.
2146    ///
2147    /// **This is the only condition under which durable bytes are discarded.**
2148    /// A *complete* record that is invalid is corruption, not a torn write, and
2149    /// refuses the open however close to the end of the file it sits.
2150    RecoveredTornTail {
2151        /// Byte offset where the incomplete append began.
2152        byte_offset: u64,
2153        /// How many bytes were discarded.
2154        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    /// Whether the open replayed the log without discarding anything.
2172    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
2193/// The durable format this build writes and understands.
2194///
2195/// Independent of the package version on purpose: an application release that
2196/// changes nothing about persistence must not appear to change the database.
2197///
2198/// - **1** — everything before revisions became resource-scoped. Never written
2199///   by any build; it names the era that predates format versioning.
2200/// - **2** — resource-scoped revisions: a `StateRevision` carries `resource`,
2201///   `content_id` and `sequence`.
2202pub const DURABLE_FORMAT_VERSION: u32 = 2;
2203
2204/// The log record that carries the format version.
2205const 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/// What format a durable database is in.
2214#[derive(Debug, Clone, PartialEq, Eq)]
2215pub enum DurableFormat {
2216    /// A format record states the version.
2217    Versioned(u32),
2218    /// No format record: the database predates format versioning.
2219    ///
2220    /// This is a version, not an absence of one. It is the only case where the
2221    /// records themselves are examined to decide compatibility, and that
2222    /// examination is a deliberate one-time probe rather than a deserialization
2223    /// attempt whose failure is discarded.
2224    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/// Why a durable database cannot be opened, and what to do about it.
2237#[derive(Debug, Clone, PartialEq, Eq)]
2238pub struct FormatIncompatibility {
2239    /// What the database is.
2240    pub found: DurableFormat,
2241    /// What this build requires.
2242    pub required: u32,
2243    /// What is wrong, in terms of the data rather than the code.
2244    pub reason: String,
2245    /// What the operator can do.
2246    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/// The result of examining a durable database without opening it.
2261#[derive(Debug, Clone, PartialEq, Eq)]
2262pub enum FormatCompatibility {
2263    /// Safe to open.
2264    Compatible(DurableFormat),
2265    /// Must not be opened.
2266    Incompatible(FormatIncompatibility),
2267}
2268
2269impl FormatCompatibility {
2270    /// Whether opening this database is safe.
2271    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    /// The durable format the rows are in.
2282    ///
2283    /// A snapshot carries whole `StoredRow`s, revisions included, so it is a
2284    /// durable interchange artifact and not merely a protocol value: installing
2285    /// one from a different format would reintroduce exactly the silent
2286    /// misinterpretation the on-disk version exists to prevent.
2287    ///
2288    /// Defaults to 0 so a snapshot serialized before versioning still
2289    /// deserializes; 0 is treated as unversioned and refused rather than
2290    /// assumed current. `content_hash` deliberately does not cover this field,
2291    /// so hashes computed before it existed remain valid.
2292    #[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    /// Present since format versioning. Absent in headers written before it,
2322    /// which deserialize as 0 and are classified as unversioned.
2323    #[serde(default)]
2324    format_version: u32,
2325    /// Canonical commit provenance retained when operation-log compaction
2326    /// rewrites current state. This is history, not a derived event store.
2327    #[serde(default)]
2328    transaction_history: Vec<TransactionAttribution>,
2329    /// Durable binding between a caller transaction identity and its semantic
2330    /// request. Compaction must retain this or an old id could be reused for a
2331    /// different mutation after restart.
2332    #[serde(default)]
2333    transaction_payload_hashes: HashMap<String, String>,
2334    /// The authority revision when the log was rewritten. Optional and
2335    /// additive: an older build ignores it, and a header without it (0) is
2336    /// replayed exactly as before.
2337    #[serde(default)]
2338    authority_revision: u64,
2339}
2340
2341/// [`LogHeader`] as written by compaction: the same JSON, from borrowed state.
2342#[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
2364/// Capabilities whose rows are not themselves versioned.
2365///
2366/// `state` holds the revisions, and `_retention` holds the watermark that says
2367/// how much of each resource's history survives. Minting a revision of either
2368/// would mint a revision of the mint.
2369const UNVERSIONED_CAPABILITIES: &[&str] = &["state", "_retention"];
2370
2371/// Whether a write to this capability creates a revision.
2372fn is_versioned(capability: &str) -> bool {
2373    !UNVERSIONED_CAPABILITIES.contains(&capability)
2374}
2375
2376/// Rebuild the derived head index from the durable revisions.
2377///
2378/// Called after recovery, for the same reason `equality_index` is: the index is
2379/// a convenience over `rows` and must never be the authority for what `rows`
2380/// contains.
2381fn 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
2404/// The derived row recording a revision.
2405///
2406/// Deliberately **not** a mutation: no sequence number, no vector clock tick,
2407/// no entry in the change log. A revision is a *derived* record of a mutation
2408/// that already has all three, and giving it its own would make every write
2409/// advance replication twice and double the operation stream.
2410///
2411/// It is still durable — a local write appends it in the same write and under
2412/// the same barrier as the record it describes (see [`revision_row_to_append`]),
2413/// so a reopened database has both, and a reader never observes a mutation
2414/// whose revision is missing.
2415///
2416/// The consequence is that revisions are **local and derived**, not replicated.
2417/// That is sound in principle, because a revision's identity is a function of
2418/// the resource, the content, the parent and the sequence — all of which a peer
2419/// that applies the same operation stream has. Whether the sync-apply path
2420/// actually mints is a separate question this change does not answer.
2421fn 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
2435/// Commit a local single-record write: its record and the revision it mints,
2436/// as one append under one barrier. On failure nothing of the write is applied
2437/// and the sequence number and clock tick it reserved are given back, so a
2438/// refused write leaves no trace in memory, in the change log or on disk.
2439fn 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
2459/// The revision row a local write appends together with its record, or `None`
2460/// when the write mints no revision or the revision is already stored.
2461fn 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
2477/// Work out the revision an authoritative write will produce, without writing
2478/// anything.
2479///
2480/// Separated from persisting it so the operation that carries this mutation can
2481/// name the revision it produced. A peer then **reconstructs that revision**
2482/// rather than minting a local substitute for the resulting state — which is
2483/// the difference between replicating history and replicating the present.
2484fn 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
2499/// What a peer needs to rebuild this revision exactly.
2500fn 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
2514/// Persist a revision under the caller's lock, and apply the resource's
2515/// retention policy.
2516///
2517/// Idempotent: a revision already stored is left exactly as it is. Committing
2518/// the same historical fact twice — which replication does routinely — must not
2519/// rewrite it.
2520fn 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        // Called once the record this revision describes is durable, so the
2535        // write has committed whatever happens here. If this append fails it is
2536        // rolled back (or the store fail-stops), and the revision is still
2537        // adopted: replay rebuilds it from the committed record's operation on
2538        // every open, so memory holds exactly what the next open will.
2539        let _ = append_event_locked(inner, &row);
2540        inner.put_row(row);
2541    }
2542
2543    // The head advances only forward. A fork leaves two revisions at one
2544    // sequence; whichever is seen last does not become "the" head by accident.
2545    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    // Retention is policy over history, not part of the committed write: if
2556    // its append fails, nothing expires now (memory and log agree) and the
2557    // next write to the resource tries again.
2558    let _ = apply_retention_locked(inner, &resource);
2559    Ok(())
2560}
2561
2562/// Rebuild the revision an operation carries, and check it is the one the
2563/// originating authority actually committed.
2564///
2565/// The identity is recomputed from the parts rather than trusted: if the value
2566/// that arrived is not the value the revision was computed from, or the stated
2567/// identity does not follow from the stated resource, parent and sequence, the
2568/// operation is refused rather than turned into a plausible local history.
2569fn 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
2599/// Expire whatever the resource's configured policy no longer retains.
2600///
2601/// The common path is one map lookup: with no policy configured there is
2602/// nothing to scan. A resource's history is only walked once a policy exists to
2603/// bound it.
2604fn 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    // One append, and memory changes only once it succeeded: an expiry that
2670    // did not reach the log is not applied here either.
2671    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/// The durable shape of a resource's retention state.
2684///
2685/// Mirrors `state_model`'s private record so the write boundary can read and
2686/// advance the horizon without the store type.
2687#[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        // A rewrite this process's predecessor did not finish left its
2746        // temporary file behind. The log it was replacing is still the log
2747        // (the switch is the rename), and this process holds the owner lock,
2748        // so nothing else can be writing it: it is garbage.
2749        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        // Fail closed. The format is decided before any record is interpreted
2758        // and before anything is written, so a database this build does not
2759        // understand is refused with its bytes untouched rather than opened
2760        // with whatever happened to deserialize.
2761        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            // A new database states its format in its first record, so it never
2771            // has to be inferred again.
2772            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            // The file's name must survive a power loss as well as its bytes,
2780            // or the first acknowledged write could vanish with the file.
2781            file.sync_data()?;
2782            sync_parent_directory(&path)?;
2783        }
2784
2785        // Generate instance ID from path hash
2786        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        // Initialize sync state with instance ID
2800        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    /// Write a self-contained durable backup artifact.
2824    ///
2825    /// The artifact does not depend on this database remaining present: it
2826    /// carries every live record, the revision graph, retention state and the
2827    /// observed operation versions, plus the digests needed to check all of it
2828    /// without a live database.
2829    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        // Written beside the destination and renamed over it only once complete
2863        // and durable: an interrupted backup never replaces a good artifact
2864        // with a partial one.
2865        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    /// Restore a backup artifact into a **new** database.
2892    ///
2893    /// Refuses to write over an existing database: a restore that could
2894    /// overwrite the thing an operator is trying to recover is not a recovery
2895    /// tool. The artifact is verified first, and the restored database is
2896    /// verified again afterwards — its meaning is recomputed from the live
2897    /// database and compared with the artifact's. A restore therefore proves
2898    /// that the result *means the same thing*, not merely that the file parsed.
2899    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        // Restored beside the target and renamed into place only after it has
2917        // been reopened and proven equivalent. An interrupted restore therefore
2918        // never leaves a database at `target` -- in particular not a truncated
2919        // one that would open cleanly holding a subset of the backup.
2920        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        // The equivalence check, on the staged database reopened from disk.
2945        // Anything that changed the database's meaning between the artifact
2946        // and the live result fails here, and nothing is placed at `target`.
2947        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    /// A digest of this database's **application state**: the records an
2988    /// application reads, and nothing else.
2989    ///
2990    /// Excludes revisions and retention state, so it answers only "do these two
2991    /// databases hold the same values?". Pair it with
2992    /// `StateStore::history_digest` to ask the separate and stronger question
2993    /// of whether they hold the same history — two databases can match on this
2994    /// and differ completely on that, which is exactly the failure replication
2995    /// used to have.
2996    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    /// How far a single-record write is pushed before it returns.
3025    pub fn durability_mode(&self) -> DurabilityMode {
3026        self.state().durability_mode
3027    }
3028
3029    /// Choose what a successful single-record write guarantees.
3030    ///
3031    /// This changes the database's contract, not merely its speed. Transaction
3032    /// commits always issue a stable-storage barrier and are unaffected.
3033    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    /// What this database knows about itself.
3040    ///
3041    /// Reports facts the open established — the durable format it was accepted
3042    /// as, and whether replay discarded an incomplete final append. It reports
3043    /// nothing it has not observed: **health can expose a proven fact, it
3044    /// cannot create a stronger guarantee.**
3045    ///
3046    /// In particular this says nothing about power-loss durability, which is
3047    /// unproven, nor about replication or backup state, which have no
3048    /// operational contract yet.
3049    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    /// What replaying the durable log had to do for this database to open.
3076    ///
3077    /// [`LogRecovery::Clean`] means every durable byte was a complete, valid
3078    /// record. [`LogRecovery::RecoveredTornTail`] means the log ended in an
3079    /// incomplete append that was discarded — the database is usable and its
3080    /// **last write did not land**, which is a different fact from healthy and
3081    /// one an operator has to be able to learn.
3082    ///
3083    /// A corrupt log does not reach here at all: it refuses the open.
3084    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    /// Insert only if the logical record is absent, deciding presence under the
3099    /// same lock that appends the operation and mints its first revision.
3100    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    /// Update an existing record with vector clock tracking
3110    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    /// Atomically compare the logical record version and authority epoch before
3121    /// durably replacing a JSON object. Failed predicates append no operation
3122    /// and emit no change event.
3123    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    /// Delete a record with vector clock tracking
3270    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    /// Evaluate authorization for a mutation
3281    fn evaluate_authorization(&self, context: &AuthorizationContext) -> Result<()> {
3282        // Check basic capabilities
3283        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        // Tenant isolation check happens implicitly at mutation boundary
3290        // by only checking against current database tenant_id
3291
3292        Ok(())
3293    }
3294
3295    /// Insert a record with authorization context
3296    pub fn insert_with_authorization<T: Serialize>(
3297        &self,
3298        key: &str,
3299        value: T,
3300        context: &AuthorizationContext,
3301    ) -> Result<()> {
3302        // Authorize mutation inside FeltDB boundary
3303        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        // Check if capability is writable
3312        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        // Authorized: apply mutation
3322        self.insert_internal(capability, key.to_string(), value, false)?;
3323        Ok(())
3324    }
3325
3326    /// Update a record with authorization context
3327    pub fn update_with_authorization<T: Serialize>(
3328        &self,
3329        key: &str,
3330        value: T,
3331        context: &AuthorizationContext,
3332    ) -> Result<()> {
3333        // Authorize mutation inside FeltDB boundary
3334        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        // Check if capability is writable
3343        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        // Authorized: apply mutation
3353        // Pre-state evaluation would happen here in full implementation
3354        self.update_internal(capability, key.to_string(), value)?;
3355        Ok(())
3356    }
3357
3358    /// Delete a record with authorization context
3359    pub fn delete_with_authorization(
3360        &self,
3361        key: &str,
3362        context: &AuthorizationContext,
3363    ) -> Result<()> {
3364        // Authorize mutation inside FeltDB boundary
3365        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        // Check if capability is writable
3374        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        // Authorized: apply mutation
3384        // Pre-state evaluation would happen here in full implementation
3385        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    /// Get the instance ID for this FeltDB instance
3449    pub fn instance_id(&self) -> Result<String> {
3450        let inner = self.state();
3451        Ok(inner.instance_id.clone())
3452    }
3453
3454    /// Get the current sequence number for this instance
3455    pub fn sequence(&self) -> Result<u64> {
3456        let inner = self.state();
3457        Ok(inner.sequence)
3458    }
3459
3460    /// Revision of the snapshot held by this embedded authority instance.
3461    /// In multi-process topology callers must use `AuthorityClient::read` to
3462    /// cross the authority boundary and obtain a current authoritative value.
3463    pub fn current_revision(&self) -> Result<u64> {
3464        Ok(self.state().authority_revision)
3465    }
3466
3467    /// Capture revision and rows under one lock. Authority reads must never
3468    /// pair metadata from one commit with state from another.
3469    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    /// Get the maintained cardinality (record count) for a collection capability.
3480    /// Returns the count of non-deleted records in the collection.
3481    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    /// Diagnostic: List all collection capabilities and their cardinalities.
3491    /// Used to detect capability format mismatches.
3492    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)); // Sort by count descending
3500        Ok(result)
3501    }
3502
3503    /// Diagnostic: Count actual rows for a given capability pattern.
3504    /// Used to verify that rows exist even if cardinality is 0.
3505    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    /// Diagnostic: Get actual persisted capability strings matching a pattern.
3516    /// Used to identify capability identity formats in use.
3517    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    /// The record write path's head per resource: the revision its next
3530    /// authoritative write will name as parent. Garbage collection treats each
3531    /// one as a root, so a write can never mint a child of a collected revision.
3532    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    /// The original commit of a transaction id, if it committed, exactly as a
3541    /// retry of it through `apply_atomic_transaction_*` would report it: the
3542    /// original revisions and positions (`duplicate: true`), or
3543    /// `TransactionIdPayloadMismatch` when `payload_hash` differs from the one
3544    /// the id was committed with. Read-only.
3545    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    /// Atomically validates and durably applies an ordered mutation batch.
3574    /// The complete transaction is represented by one log record and becomes
3575    /// visible in memory only after that record has been appended successfully.
3576    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    /// Applies a transaction whose identifier is durably bound to a canonical
3595    /// payload hash. The same ID with a different payload is always a conflict.
3596    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    /// Commit a transaction only if every record precondition holds.
3617    ///
3618    /// The preconditions are evaluated inside the same lock as the writes and
3619    /// before any of them, so there is no state in which one record is checked
3620    /// while the others commit. A failure returns
3621    /// `FlowError::PreconditionFailed` and applies nothing -- not a prefix, not
3622    /// the guarded record, nothing.
3623    ///
3624    /// A failed precondition does **not** consume the transaction id. The id is
3625    /// recorded only after a commit, so a caller that loses a race may retry
3626    /// the same id once it has re-read state, and a caller that wants the
3627    /// retry to be a distinct decision may use a new one. Both work; the choice
3628    /// is the caller's and is asserted in the tests rather than left implied.
3629    #[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    /// Capture a read basis on the direct route, under the same lock every commit takes.
3653    pub fn read_basis(&self, reads: &ReadSet) -> Result<ReadBasis> {
3654        self.read_basis_in(reads, &ReadLayout::Direct)
3655    }
3656
3657    /// Capture a read basis in a storage layout, under the same lock every commit takes.
3658    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    /// `apply_atomic_transaction_guarded`, additionally fenced by read bases.
3666    ///
3667    /// Each basis is re-evaluated inside the same lock as the writes, after the
3668    /// record preconditions and before anything is staged. If the state it
3669    /// names differs from what was read -- whichever write path changed it --
3670    /// the transaction fails with `PreconditionFailed(Fence)` and applies
3671    /// nothing. State outside every basis is never read and never conflicts.
3672    #[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    /// `apply_atomic_transaction_fenced`, with the read bases addressed in
3698    /// `layout`. The same evaluation, inside the same lock, for every route.
3699    #[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    /// `apply_atomic_transaction_fenced_in`, plus collection fences: each
3727    /// `(capability, version)` commits only if nothing in that collection was
3728    /// written, replaced or removed since the decision read it at `version`
3729    /// (`consistent_rows`). For a decision that depends on the absence of any
3730    /// matching record where no equality predicate can name the match -- a
3731    /// non-sparse unique tuple that is entirely null or missing.
3732    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            // Record preconditions, evaluated before anything is staged and
3753            // under the same lock the writes take. A caller reads state,
3754            // decides, and commits; this is the fence that makes the decision
3755            // still true at the moment of the write.
3756            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                // The same fields single-record CAS compares, read the same way.
3786                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                    // An expired lease is not held, so naming it is a conflict:
3829                    // the caller believes it owns something it no longer does.
3830                    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            // Every precondition held. The authority owns both ends of the
3892            // public document-version lifecycle inside this lock:
3893            //
3894            // - a create protected by `requireAbsent` writes version 1;
3895            // - a write fenced by `expected_version = N` writes N + 1.
3896            //
3897            // An unconditional write remains verbatim, and a guard-only
3898            // precondition writes nothing. See
3899            // docs/architecture/transaction-version-contract.md.
3900            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                        // A delete has no record left to carry a version, so a
3939                        // fenced delete is fence-then-remove and nothing more.
3940                        let Some(Value::Object(fields)) = mutation.value.clone() else {
3941                            return mutation.clone();
3942                        };
3943                        let mut fields = fields;
3944                        // The caller cannot manufacture either the initial or
3945                        // next authoritative version.
3946                        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            // Transaction bookkeeping is measured as one span per commit even
3958            // though it brackets two regions: everything before the durable
3959            // append, and the revision, dedup and payload-hash accounting after
3960            // the rows are applied. The append itself is `Persistence` and the
3961            // apply loop is `RecordMutation` + `IndexMaintenance`, so summing
3962            // this with those does not double-count any of them.
3963            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            // Retained from the record that was just made durable, so the
4065            // in-memory answer to "who committed this" cannot describe a
4066            // transaction the log does not hold.
4067            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                    // Record and index change together, inside the commit that
4090                    // is already holding the state lock: a transaction that is
4091                    // refused leaves neither, and one that commits publishes both.
4092                    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    /// Apply atomic transaction with authorization context (atomic rejection on deny)
4145    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        // Authorize ALL mutations before applying ANY of them (atomicity guarantee)
4155        self.evaluate_authorization(context)?;
4156
4157        // Check writable collections for all mutations
4158        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        // All authorized: proceed with atomic transaction (no partial application)
4170        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        // Canonical because `serde_json` is built without `preserve_order`, so
4189        // `Map` is a `BTreeMap` and `to_string` emits keys in sorted order.
4190        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            // Get current vector clock to attach to operation
4226            let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
4227
4228            // The revision this write produces is worked out first, so the
4229            // operation can name it and a peer can rebuild the same one.
4230            let planned =
4231                is_versioned(&capability).then(|| plan_revision(&inner, &key, canonical.clone()));
4232
4233            // Create an operation with vector clock for convergence
4234            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            // The operation enters the change log only once its record is
4247            // durable: replication must never send a write the log lacks.
4248            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        // Canonical because `serde_json` is built without `preserve_order`, so
4285        // `Map` is a `BTreeMap` and `to_string` emits keys in sorted order.
4286        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            // Get current vector clock to attach to operation
4307            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            // Create an update operation with vector clock for convergence
4313            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            // The operation enters the change log only once its record is
4326            // durable: replication must never send a write the log lacks.
4327            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            // Get the row to determine its type
4355            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            // Get current vector clock to attach to operation
4367            let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
4368
4369            // Create a delete operation with vector clock for convergence
4370            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            // Persist a tombstone through the same append-only log used by
4381            // inserts and updates. Without this, deleted rows reappeared after
4382            // reopening the database.
4383            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            // Only a durable tombstone is offered to replication.
4396            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            // Remove from in-memory store, index entries included.
4407            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    ///
4421    /// The subscription returns an initial set of matching results, and provides a stream
4422    /// of updated results whenever records matching the predicate are inserted or updated.
4423    ///
4424    /// # Example
4425    ///
4426    /// ```ignore
4427    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
4428    /// use feltdb::open;
4429    /// let db = open("app.data")?;
4430    /// let mut subscription = db.live_query::<User, _>(|u| u.active).await?;
4431    /// let initial_users = subscription.initial_results();
4432    ///
4433    /// while let Some(users) = subscription.next().await {
4434    ///     println!("Active users: {:?}", users);
4435    /// }
4436    /// # Ok(())
4437    /// # }
4438    /// ```
4439    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    /// Get current sync state information
4485    pub fn sync_info(&self) -> Result<SyncState> {
4486        let inner = self.state();
4487        Ok(inner.sync_state.clone())
4488    }
4489
4490    /// The authority's wall-clock time in milliseconds.
4491    ///
4492    /// The same reading the commit-time lease predicate uses, exposed so a
4493    /// decision that depends on lease expiry can be made against the clock that
4494    /// will later judge the commit. A caller's own clock is never a substitute.
4495    pub fn authority_time_ms(&self) -> u64 {
4496        now_ms() as u64
4497    }
4498
4499    /// Read an untyped value for protocol adapters such as the HTTP server.
4500    pub fn get_value(&self, key: &str) -> Result<Option<Value>> {
4501        self.get::<Value>(key)
4502    }
4503
4504    /// One live row of a collection by its key, cloned under the state lock.
4505    ///
4506    /// A keyed lookup, not a scan: callers that know the canonical key of the
4507    /// record they need (Durable Work's projection refresh, for one) use this
4508    /// rather than a predicate over the whole collection.
4509    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    /// List the canonical values in one collection/capability namespace.
4520    ///
4521    /// This clones the whole collection, so its cost is linear in collection
4522    /// size. Readers that discard most of what they receive — a filtered query,
4523    /// for example — should use [`FeltDb::query_collection`] instead, which
4524    /// evaluates borrowed rows and clones only what it keeps.
4525    /// Whole capabilities and single records, read under one acquisition of
4526    /// the state lock: one consistent view, for a decision whose read basis is
4527    /// then fenced at commit (`state_contract`). A whole capability is returned
4528    /// as stored, tombstones included, like `list_collection`; a single record
4529    /// only when it is live, like `get_collection_record`.
4530    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    /// Evaluate a predicate over one collection without materializing it.
4582    ///
4583    /// The predicate observes each live row *borrowed* from state, so a record
4584    /// the predicate rejects is never cloned. Only retained rows are cloned,
4585    /// which happens while state protection is still held: the returned rows are
4586    /// owned, so nothing the caller keeps can reference storage a concurrent
4587    /// mutation may move or drop after the lock is released.
4588    ///
4589    /// Traversal is the collection's own record-key order, the same order
4590    /// [`FeltDb::list_collection_page`] walks. `limit` bounds accumulation and
4591    /// stops the traversal as soon as it is satisfied; because early stopping
4592    /// truncates in record-key order, a caller that must order results by some
4593    /// other field has to pass `None` and bound its own already-filtered set.
4594    ///
4595    /// The predicate runs under the state lock, so it must not call back into
4596    /// the database or block.
4597    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                    // The same predicate phase the indexed execution reports to,
4618                    // so the two executions' predicate cost is directly
4619                    // comparable rather than being two different measurements.
4620                    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    // ----------------------------------------------------------------------
4635    // Equality index — lifecycle and execution.
4636    //
4637    // The index is derived state. Every method below either declares what is
4638    // derived, re-derives it from authoritative records, or reads candidate
4639    // record keys out of it. None of them is a source of truth, and none of
4640    // them can answer a query on its own: `query_collection_by_equality` still
4641    // evaluates the caller's full predicate against authoritative records.
4642    // ----------------------------------------------------------------------
4643
4644    /// Acquire the state lock, attributing the wait and the hold.
4645    ///
4646    /// Every path in this file that touches authoritative state goes through
4647    /// here, which is what makes lock contention a measured quantity rather than
4648    /// a residual inferred from wall-clock time.
4649    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    /// Declare an equality index on one collection field and populate it from
4673    /// authoritative state. Returns whether this call created the index.
4674    ///
4675    /// Creation and population happen under one lock, so an index is never
4676    /// observable in a half-built state. Declaring an index that already exists
4677    /// is not an error and does not rebuild it.
4678    ///
4679    /// `recordId` is refused: on the query surface it is authority metadata that
4680    /// shadows a document field of the same name on object-valued records only,
4681    /// so an index over the document field would answer a different question
4682    /// from the one the predicate asks.
4683    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    /// Drop an equality index and its buckets. Returns whether one existed.
4701    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    /// Every declared index, as `(collection, field)` in a stable order.
4707    pub fn equality_indexes(&self) -> Result<Vec<(String, String)>> {
4708        let inner = self.state();
4709        Ok(inner.equality_index.declarations())
4710    }
4711
4712    /// Re-derive every declared index from authoritative records.
4713    ///
4714    /// This is the recovery path. An index is never read from disk and never
4715    /// received from a peer: after a restart, a snapshot install, or any other
4716    /// reload of authoritative state, the index is whatever this produces.
4717    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    /// Index size, as counts. Never a value, never a record id.
4729    pub fn equality_index_stats(&self) -> Result<EqualityIndexStats> {
4730        let inner = self.state();
4731        Ok(inner.equality_index.stats())
4732    }
4733
4734    /// A fully ordered rendering of the live index.
4735    ///
4736    /// Test instrumentation. It exists so a test can compare the live index
4737    /// against one rebuilt from authoritative state; nothing in execution reads
4738    /// it, and it is not part of any product surface.
4739    #[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    /// The index invariant: the live index equals one derived from records now.
4748    ///
4749    /// Returns a description of the first divergence rather than a bare bool, so
4750    /// a failing assertion says what diverged. This is the strongest statement
4751    /// available about index maintenance, because it compares the maintained
4752    /// structure against the authoritative state it claims to describe rather
4753    /// than against a second copy of its own bookkeeping.
4754    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    /// Candidate record keys for one equality condition, copied out of the index.
4783    ///
4784    /// `None` means the index cannot answer that condition — the field is not
4785    /// indexed, or the value is not an indexable scalar — and the caller must
4786    /// fall back to a scan. `Some(empty)` is an answer, not a failure.
4787    ///
4788    /// The keys are copied while the state lock is held. Nothing borrowed from
4789    /// the index or from the record map escapes this call.
4790    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    /// Execute a bounded query through the equality index when one applies.
4804    ///
4805    /// `equalities` are the query's equality conditions, in the caller's order.
4806    /// At least one of them must be indexed and hold an indexable value, or this
4807    /// returns `Ok(None)` and the caller falls back to [`FeltDb::query_collection`].
4808    /// Returning `None` rather than an empty result is the difference between
4809    /// "the index does not apply" and "nothing matches", and conflating the two
4810    /// would silently drop records.
4811    ///
4812    /// When the index does apply, every applicable condition contributes a
4813    /// candidate set and the sets are intersected smallest-first. The result is
4814    /// still only *candidates*: `predicate` — the caller's existing, unmodified
4815    /// predicate — decides what matches, evaluated against authoritative records.
4816    /// The index therefore cannot change what a query returns, only how many
4817    /// records the predicate has to look at.
4818    ///
4819    /// Candidate keys, the record lookups they drive and the predicate all run
4820    /// under one acquisition of the state lock, so a query observes one coherent
4821    /// committed state exactly as a scan does. Records are cloned only when the
4822    /// predicate keeps them, and nothing borrowed from state outlives the lock.
4823    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            // Candidate selection, timed apart from record access: PR36 has to
4839            // be able to say whether the index lookup itself has become a cost,
4840            // and "index query time" alone could not distinguish a slow lookup
4841            // from a large candidate set.
4842            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            // Smallest first. This is not cost estimation: a set's length is
4857            // already held by the set, so intersecting from the smallest is
4858            // simply the cheaper order to do the same work.
4859            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                    // An index entry with no record behind it would be a
4874                    // maintenance defect. Skipping it keeps the query correct
4875                    // rather than surfacing a record that does not exist.
4876                    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    /// Traverse one collection in immutable record-key order without materializing it.
4892    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    /// Fetch one live collection record without materializing collection state.
4917    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    /// Fetch several live collection records without materializing collection state.
4928    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    /// Subscribe to canonical mutation events.
4946    pub fn subscribe_changes(&self) -> broadcast::Receiver<ChangeEvent> {
4947        self.event_tx.subscribe()
4948    }
4949
4950    /// Add a peer for synchronization
4951    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    /// Remove a peer from synchronization
4958    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    /// Get pending operations for a peer
4965    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    /// Return operations missing from a peer's per-origin version vector.
4981    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    /// Return the highest durable operation sequence observed from each origin.
4990    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    /// Current materialized rows including their causal operation envelopes.
5016    /// State-contract execution uses this for record-version preconditions;
5017    /// portable snapshots intentionally continue to strip those envelopes.
5018    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    /// Materialized rows belonging to one application state namespace.
5028    ///
5029    /// State-contract capabilities are encoded as `<state namespace>:<collection>`.
5030    /// Selecting the capability buckets before cloning prevents one application's
5031    /// read snapshot from materializing every other application's durable rows.
5032    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    /// Install a verified bootstrap snapshot into a pristine node. A node with
5050    /// local history must converge through operations rather than overwrite it.
5051    pub fn install_snapshot(&self, snapshot: DatabaseSnapshot) -> Result<()> {
5052        // A snapshot carries whole rows, revisions included, so installing one
5053        // from another format would reintroduce exactly the silent
5054        // misinterpretation the on-disk version prevents.
5055        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                // A bootstrap snapshot replays as it always has.
5105                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    /// Acknowledge receipt of operations from a peer
5138    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    /// Durably record a peer's per-origin acknowledgement vector.
5146    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    /// Earliest operation still available for incremental replay, by origin.
5162    /// Recent commits and who made them, newest first.
5163    ///
5164    /// Bounded by [`TRANSACTION_ATTRIBUTION_WINDOW`]. A transaction older than
5165    /// the window is not absent from history — it is in the durable log — it is
5166    /// simply no longer answerable from memory, and callers that present this
5167    /// to a human must say which of the two they are showing.
5168    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    /// One transaction's durable attribution, regardless of the operator
5180    /// window. This lookup is canonical historical provenance.
5181    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    /// Canonical commit history in authority order, suitable for resumable
5195    /// event projection and historical provenance reconstruction.
5196    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    /// Durable commits that named this record, newest first.
5212    ///
5213    /// This deliberately reads complete canonical history rather than the
5214    /// bounded operator window. Historical provenance must not disappear when
5215    /// a cache rolls over.
5216    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    /// How much of the attribution window is in use, and what it can hold.
5234    ///
5235    /// Reported so a reader can tell an idle system from a window that has
5236    /// already rolled over.
5237    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    /// Atomically replace acknowledged history with a state snapshot and any
5256    /// operations still needed by at least one configured peer.
5257    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    /// Compact under the database's [`CompactionPolicy`].
5263    ///
5264    /// Prunes acknowledged operations from memory whenever it can, and rewrites
5265    /// the durable log **only when enough has accumulated to justify the
5266    /// stall**. Use this on a timer; use
5267    /// [`compact_operation_log`](Self::compact_operation_log) when a caller is
5268    /// explicitly asking for a rewrite now.
5269    ///
5270    /// The distinction is the whole point. Pruning is cheap and in memory.
5271    /// Rewriting serializes every row in the database and fsyncs it while
5272    /// holding the one lock that serializes every read and every write, so
5273    /// doing it on a short timer stops the world on that timer.
5274    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    /// The policy deciding when a timer-driven compaction rewrites the log.
5282    pub fn compaction_policy(&self) -> CompactionPolicy {
5283        self.state().compaction_policy
5284    }
5285
5286    /// Set the policy deciding when a timer-driven compaction rewrites the log.
5287    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        // Rewriting the log from memory would silently decide an indeterminate
5294        // write as "not committed". Only a reopen gets to decide that.
5295        refuse_if_faulted(&inner)?;
5296        // A store that has never replicated bounds its operation log to a
5297        // window; one with replication history prunes only what every active
5298        // peer acknowledged.
5299        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        // The cheap half is done. The expensive half runs only when asked for
5312        // explicitly, or when the accumulated gain has earned it. A standalone
5313        // store prunes on every write once its window is full, so it also
5314        // waits for the log to double: rewriting costs the whole store, and
5315        // doubling makes that cost proportional to what was appended.
5316        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    /// Register a peer in the distributed fabric
5336    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    /// Get a peer's advertisement
5345    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    /// Find all peers providing a namespace
5354    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    /// Find all peers providing a capability
5363    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    /// Get all registered peers
5372    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    /// Resolve a FlowRef locally
5381    pub fn resolve_ref<T: DeserializeOwned>(&self, flow_ref: &FlowRef) -> Result<Option<T>> {
5382        // First, try to resolve locally
5383        let key = format!("{}:{}", flow_ref.namespace, flow_ref.object_id);
5384        self.get::<T>(&key)
5385    }
5386
5387    /// Get current sync state
5388    pub fn sync_state(&self) -> Result<SyncState> {
5389        let inner = self.state();
5390        Ok(inner.sync_state.clone())
5391    }
5392
5393    /// Apply an operation from a remote peer
5394    /// This merges the vector clock, resolves conflicts, and applies the mutation
5395    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        // Merge the remote vector clock into our own
5421        if let Some(remote_clock) = &op.vector_clock {
5422            inner.sync_state.merge_vector_clock(remote_clock);
5423        }
5424
5425        // Check if we have a local operation for this key that hasn't been synced yet
5426        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        // Determine which operation to apply (remote or local if conflict)
5433        let op_to_apply = if let Some(local_op) = local_op_for_key {
5434            // We have a conflict - use LWW with vector clock awareness
5435            // Create causal histories for conflict resolution
5436            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            // Resolve using LWW strategy
5452            let resolution = ConflictResolution::resolve_lww(&[local_history, remote_history]);
5453
5454            // Winner index: 0 = local, 1 = remote
5455            if resolution.winner_index == Some(1) {
5456                op.clone()
5457            } else {
5458                // The local value wins, but the received operation still belongs
5459                // to the durable causal history so peers do not resend it forever.
5460                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        // Apply the winning operation to our state
5479        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        // Reconstruct the historical fact the originating authority recorded.
5516        //
5517        // Not a local mint of the resulting state: the identity, parent and
5518        // sequence come from the operation, so the peer ends up holding the
5519        // *same* revision rather than an equivalent-looking one of its own.
5520        // Deliberately independent of which value won any last-writer race
5521        // above — the origin committed this revision whether or not its value
5522        // is what is current here, and dropping it would lose the divergence
5523        // that reconciliation needs to see.
5524        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    /// Get the current vector clock state
5546    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    /// Creates a live query subscription scoped to this capability.
5640    ///
5641    /// The subscription returns initial matching results from this capability only, and streams
5642    /// updates whenever records within this capability matching the predicate are inserted or updated.
5643    ///
5644    /// # Example
5645    ///
5646    /// ```ignore
5647    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
5648    /// use feltdb::open;
5649    /// let db = open("app.data")?;
5650    /// let users = db.capability("users");
5651    /// let mut subscription = users.live_find::<User, _>(|u| u.active).await?;
5652    /// let initial_users = subscription.initial_results();
5653    ///
5654    /// while let Some(users) = subscription.next().await {
5655    ///     println!("Active users: {:?}", users);
5656    /// }
5657    /// # Ok(())
5658    /// # }
5659    /// ```
5660    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
5707// Type alias for boxed predicates
5708type PredicateBox<T> = Arc<Mutex<Option<Box<dyn Fn(&T) -> bool + Send + Sync>>>>;
5709
5710/// A subscription to a live query that streams updates whenever matching records change.
5711///
5712/// This struct holds the initial query results and provides a stream interface to receive
5713/// updates whenever records matching the query predicate are inserted or updated.
5714pub struct LiveQuerySubscription<T> {
5715    /// The initial set of records matching the query predicate at subscription time.
5716    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    /// Waits for the next change event and returns the updated query results.
5735    ///
5736    /// This method is async and will block until a change event is received for the
5737    /// subscribed type. When an event arrives, it re-evaluates the full dataset and
5738    /// returns all matching records.
5739    ///
5740    /// Returns `None` if the broadcast channel has been dropped (all senders are gone).
5741    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    /// Returns a reference to the initial query results captured at subscription time.
5775    ///
5776    /// This provides access to the baseline set of matching records before any changes
5777    /// have been streamed through `next()`.
5778    pub fn initial_results(&self) -> &[T] {
5779        &self.initial_results
5780    }
5781}
5782
5783/// A capability-scoped subscription to a live query that streams updates for records within a specific capability.
5784///
5785/// Similar to `LiveQuerySubscription`, but only receives updates for records within the specified capability.
5786/// This provides more granular control over subscription scope.
5787pub 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    /// Waits for the next change event within this capability and returns the updated query results.
5808    ///
5809    /// Similar to `LiveQuerySubscription::next()`, but only responds to changes within the
5810    /// scoped capability.
5811    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
5850/// Read the log, separating a torn tail from real corruption.
5851///
5852/// A process that dies while appending leaves a final record that was never
5853/// terminated. That record was never committed, so recovery discards it and
5854/// truncates it away: leaving the bytes in place would make the next append
5855/// splice onto them and turn a normal crash into permanent corruption.
5856///
5857/// Damage anywhere earlier is not an interrupted write. Recovery refuses it
5858/// rather than silently dropping committed history.
5859/// Examine a durable database's format **without opening or modifying it**.
5860///
5861/// This runs before any record is interpreted, and reads the file only. A
5862/// database this reports as incompatible is left exactly as it was found.
5863///
5864/// The order matters: an explicit format record decides, and the records
5865/// themselves are examined *only* when no format record exists — the
5866/// unversioned era. Compatibility is never inferred from whether a
5867/// deserialization happened to succeed.
5868pub fn inspect_durable_format(path: &Path) -> Result<FormatCompatibility> {
5869    if !path.exists() {
5870        // A database that does not exist yet will be created in the current
5871        // format.
5872        return Ok(FormatCompatibility::Compatible(DurableFormat::Versioned(
5873            DURABLE_FORMAT_VERSION,
5874        )));
5875    }
5876
5877    // Read-only: inspecting a database it is about to reject must not touch it.
5878    // Streamed, and each line is probed for the three fields that matter here
5879    // without materializing the rest of it.
5880    #[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                // The unversioned-era probe: a revision record that predates
5912                // resource-scoped identity. Deliberate and specific — it looks
5913                // for a named absence rather than trying to parse and shrugging
5914                // at failure. Only revision lines are read in full.
5915                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    // No format record and no legacy revision: nothing in this database depends
5978    // on the difference.
5979    Ok(FormatCompatibility::Compatible(DurableFormat::Unversioned))
5980}
5981
5982/// One complete record in the durable log, with where it came from.
5983struct ScannedLine {
5984    text: String,
5985    line_number: usize,
5986    byte_offset: u64,
5987}
5988
5989/// The bytes at the end of a log that are not a complete record.
5990#[derive(Clone)]
5991struct TornTail {
5992    byte_offset: u64,
5993    bytes: usize,
5994}
5995
5996/// Read the durable log one complete record at a time. **This never writes.**
5997///
5998/// Each complete (terminated) record is handed to `visit` and dropped before
5999/// the next is read, so reading a log costs memory proportional to its largest
6000/// record, not its size. The only thing separated out here is the
6001/// **terminator**: a final record with no newline cannot be one this database
6002/// finished writing, so it is returned as a torn tail and never visited.
6003/// Everything else — including a complete final record that happens to be
6004/// invalid — is visited, to be validated by the caller and refused rather than
6005/// quietly dropped for being near the end.
6006///
6007/// Being read-only is load-bearing twice over: a refused open must leave the
6008/// database byte-identical, and format inspection must be able to run on a
6009/// database it is about to reject.
6010fn 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            // An unterminated final record: the write did not finish. Empty
6028            // trailing bytes are nothing at all and are simply ignored.
6029            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/// Just enough of a log record to decide how to read the rest of it. Every
6051/// other field is skipped without being materialized.
6052#[derive(Deserialize)]
6053struct RecordProbe {
6054    #[serde(default)]
6055    record_type: Option<Value>,
6056}
6057
6058/// Classify a line that is not a record object: invalid JSON, or JSON that is
6059/// not a record this build understands.
6060fn 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
6068/// Discard an incomplete final append, so the next append does not splice onto
6069/// a partial record.
6070///
6071/// Called only after the whole log has replayed successfully. A log that is
6072/// going to be refused is never truncated.
6073fn 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            // Applied after every line has replayed: the operations compaction
6109            // retained follow this header, and replaying them against these
6110            // versions first would discard them as already seen.
6111            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            // Already honoured by `inspect_durable_format` before the open.
6138            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            // Older records did not carry a commit revision. Their durable
6146            // order in the log supplies an unambiguous upgrade path.
6147            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            // Attribution is restored on replay for the same reason it is kept
6170            // on commit: an operator who restarts a server must not lose the
6171            // answer to "who did this" for work the server already holds.
6172            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        // In a log whose header records the authority revision, the rows that
6218        // follow the header -- the snapshot, and the operations compaction
6219        // retained -- are not new commits: the header already counts them.
6220        // Only what was appended after the rewrite advances the revision.
6221        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            // A revision is a deterministic function of the operation that
6228            // produced it, so it never has to be lost. If the crash landed
6229            // between a record and the revision recording it, the revision is
6230            // rebuilt below rather than left missing. Its own row normally
6231            // follows on the next line and removes it from this set again, so
6232            // the set stays small however long the log is.
6233            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            // Pre-operation-envelope logs were produced only by this local
6247            // store. Preserve a monotonic sequence when upgrading them.
6248            inner.sequence += 1;
6249        }
6250        if !derived_revision && !counted_by_header {
6251            let base_revision = inner.authority_revision;
6252            inner.authority_revision += 1;
6253            // A row appended after a rewrite is a commit like any other and
6254            // keeps its attribution. (A header without the authority revision
6255            // cannot tell those rows apart and replays as it always has.)
6256            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    // The versions the snapshot header recorded, merged now that the retained
6279    // operations have been replayed.
6280    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    // Rebuild any revision whose record survived but whose own row did not.
6290    //
6291    // This is the crash window between appending a record and appending the
6292    // revision that records it. The revision is derivable from the operation —
6293    // resource, content, parent, sequence and authority all travel with it —
6294    // so recovery reconstructs it instead of leaving an authority holding less
6295    // history than its own operation stream implies. Nothing is invented: the
6296    // identity is recomputed from its parts and discarded if it does not follow
6297    // from them.
6298    //
6299    // The rebuilt rows are not appended here. They are derived, and stay
6300    // derivable from the same operations on every subsequent open.
6301    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        // A revision below the resource's retention horizon is absent because
6313        // it was expired on purpose. Rebuilding it would make recovery
6314        // resurrect history a policy deliberately discarded, which is a
6315        // different kind of wrong from losing it.
6316        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    // Compute collection cardinality from final row state (count non-deleted rows per capability)
6345    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    // Only now, with every complete record replayed, is it safe to discard an
6353    // incomplete final append. A log that was going to be refused never reaches
6354    // this point, so a refused open leaves the file byte-identical.
6355    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
6430/// Append one record under the database's configured durability mode.
6431fn append_event_locked(inner: &mut Inner, event: &StoredRow) -> Result<()> {
6432    append_rows_locked(inner, &[event])
6433}
6434
6435/// Append records as **one** write under the configured durability mode.
6436///
6437/// One write and at most one barrier, so a single-record write and the revision
6438/// it mints cost one fsync, and either both lines are durable or, after a
6439/// failure, neither is in the log.
6440fn 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/// What an append asks of stable storage before it returns.
6466#[derive(Clone, Copy, PartialEq, Eq)]
6467enum Barrier {
6468    /// Flushed to the operating system only.
6469    None,
6470    /// `fdatasync` before returning; this write is durable when it succeeds.
6471    Now,
6472    /// `fdatasync` closing a group of earlier, already acknowledged writes.
6473    Group,
6474}
6475
6476/// The persistence boundary. Every byte appended to the durable log, by every
6477/// write path, goes through here.
6478///
6479/// The append is all-or-nothing as far as the log is concerned. On any failure
6480/// — a short or refused write, a failed flush, a failed barrier — the file is
6481/// cut back to the length it had before this append, and that cut is itself
6482/// made durable. Only then is the failure reported as "nothing was
6483/// committed": the log is exactly what it was, so a later append cannot splice
6484/// onto a partial record, and a restart cannot resurrect a write that was
6485/// reported as failed.
6486///
6487/// If the cut fails too, this process no longer knows what the log holds. It
6488/// says so (`Indeterminate`, outcome unknown), records the fault, and refuses
6489/// every later write (`RecoveryRequired`) until the database is reopened and
6490/// replay — the only authority on the log — decides.
6491fn 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            // A failed group barrier also covers earlier writes that were
6512            // already acknowledged under `Grouped`. Removing this write's bytes
6513            // does not make theirs durable, so the log is not known-good.
6514            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
6544/// `path` with `.suffix` appended to its file name.
6545fn 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
6552/// Remove a restore's staging database and the files opening it created. They
6553/// are this restore's own intermediate artifacts and are never served.
6554fn 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
6565/// Refuse an operation that rewrites the durable log while a failed write has
6566/// left it indeterminate.
6567fn 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
6578/// Replace the durable log with a snapshot of current state, atomically.
6579///
6580/// Stages, each of which a crash can interrupt:
6581///
6582/// 1. write `<log>.compact.tmp` (named after the whole log file name, so two
6583///    stores in one directory never share it): the header (canonical commit history,
6584///    idempotency bindings, versions, authority revision), the operations the
6585///    operation log still retains, then every current row;
6586/// 2. fsync it;
6587/// 3. rename it over the log -- **the switch**;
6588/// 4. fsync the directory, so the switch survives power loss.
6589///
6590/// Before 3 the old log is untouched and complete, so an interruption loses
6591/// nothing; a failure removes the temporary file. After 3 the new log holds
6592/// everything the old one meant. If 4 fails, a power loss could still bring
6593/// back the old log, and a write appended to the new one would be lost with
6594/// it, so the store stops accepting writes (`RECOVERY_REQUIRED`) until it is
6595/// reopened.
6596///
6597/// Memory: one row at a time, plus the header serialized from borrowed
6598/// state. Temporary disk: the size of the compacted log.
6599fn 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        // Written from borrowed state: the header carries the whole canonical
6610        // commit history, and cloning it would double that in memory for the
6611        // length of the rewrite.
6612        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        // The old log is untouched; only the partial snapshot goes.
6644        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
6676/// `ENOSPC`, `EFBIG` and `EDQUOT` are a full volume or an exhausted limit, which
6677/// an operator fixes by freeing space; anything else is an I/O failure.
6678fn 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
6688/// Make a rename or a newly created file's directory entry durable.
6689///
6690/// A rename is atomic but, until its directory is synced, not durable: after a
6691/// power loss the old name can come back. Unsupported platforms report `Ok`.
6692fn 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
6708/// The three operations the persistence boundary performs on the log, with a
6709/// deterministic fault hook in test builds. Production builds compile to the
6710/// plain calls: there is no runtime switch that can inject a fault.
6711mod fault {
6712    use std::fs::File;
6713    use std::io::Write;
6714
6715    #[cfg(test)]
6716    pub(crate) use injection::*;
6717
6718    /// A named point in a multi-step persistence operation where a test can
6719    /// make it fail as if the process had stopped or the volume had failed.
6720    #[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    /// Cut the log back to `length` and make the cut durable.
6755    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        /// A fault armed for the next matching operation on this thread.
6771        #[derive(Clone, Copy, PartialEq, Eq, Debug)]
6772        pub(crate) enum Fault {
6773            /// Write this many bytes, then fail with the given OS error.
6774            ShortWrite { bytes: usize, errno: i32 },
6775            /// The barrier fails with `EIO`.
6776            Sync,
6777            /// Cutting the log back fails with `EIO`.
6778            Rollback,
6779            /// The named stage fails with `EIO`.
6780            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
6825/// The answer to a retried transaction id, read under the commit lock.
6826///
6827/// `None` when the id has not committed. Otherwise the *original* commit's
6828/// revisions and positions, never the store's current ones, or
6829/// `TransactionIdPayloadMismatch` when a payload hash is given and differs from
6830/// the one the id was committed with. Nothing is evaluated and nothing is
6831/// applied: a duplicate is answered before any precondition is read.
6832fn 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
6876/// Append a transaction's commit record. A commit always issues a barrier,
6877/// whatever the single-record durability mode: this append *is* the commit.
6878fn 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    /// A commit is attributable: the audit document a transaction already
6919    /// writes is retained, so a human can ask who made a change.
6920    #[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        // Attribution says who and what, never what was written.
6950        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        // Attribution survives a restart, because it is rebuilt from the same
6972        // durable records the rest of recovery replays.
6973        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    /// The window is bounded, and reports that it is.
6984    #[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        // The oldest commit rolled out of the recent window but remains
7019        // addressable through canonical history.
7020        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    /// A bulk transaction names a bounded sample of keys and an exact count.
7249    #[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        // Initial vector clock should be empty or have default values
7580        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        // Insert first record
7584        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        // Check pending operations have vector clock
7595        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        // Insert second record
7602        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        // Check we have 2 operations with increasing sequence numbers
7613        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        // Insert initial record
7629        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        // Update the record
7640        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        // Check we have operations for both insert and update
7651        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        // Both should have vector clocks
7659        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        // Insert record
7671        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        // Delete the record
7682        db.delete("users:1").expect("delete");
7683
7684        // Check we have operations for both insert and delete
7685        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        // Both should have vector clocks
7693        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        // Create a remote operation with a vector clock
7705        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        // Apply the remote operation
7721        db.apply_remote_operation(remote_op)
7722            .expect("apply_remote_operation");
7723
7724        // Check the local vector clock was merged
7725        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        // Verify the data was applied
7731        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/// Deterministic failures at the persistence boundary.
8126///
8127/// Each test arms a fault for the next write, barrier or rollback on this
8128/// thread, performs a real write against a real log file, and then checks the
8129/// three places a write can live: the response, memory, and the log after a
8130/// reopen. A write reported as not committed must be in none of them, and the
8131/// log must keep accepting writes.
8132#[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        // Half the record reaches the file, then the volume is full.
8196        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        // Space is back: the next write appends cleanly, with no splice.
8212        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        // Before rollback-on-failure, this exact sequence produced a log that
8223        // refused to open: the next commit's bytes completed the partial line.
8224        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        // The refused id is free: retrying it commits, once.
8238        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        // Before: a failed fsync left the record in the page cache; the caller
8244        // was told it failed, and the next start replayed it as committed.
8245        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        // The barrier fails and so does the cut: the bytes are in the file.
8268        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        // Fail-stop: every write path refuses, before writing anything.
8276        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        // Reopen decides. Here the record's bytes did reach the log intact, so
8294        // replay holds it committed -- the "unknown" the caller was given.
8295        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        // Before: the record was appended, then the revision append failed, and
8317        // the caller was told a write failed that was committed and visible.
8318        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        // Fail inside the revision line: past the record, short of the end.
8325        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(); // acknowledged, not yet synced
8341        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        // A restore that died after writing part of its staging file.
8359        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        // Rerunning the restore replaces the stale staging and completes.
8365        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        // A second restore to the same target is refused, and changes nothing.
8374        drop(restored);
8375        assert!(matches!(
8376            FeltDb::restore_backup(&backup, &target),
8377            Err(FlowError::BackupRejected(_))
8378        ));
8379    }
8380}
8381
8382
8383/// Bounded history: the operation log of a standalone store is a window,
8384/// compaction reclaims what live state no longer needs, and no interruption of
8385/// compaction can lose acknowledged data or canonical history.
8386///
8387/// See docs/reference/storage-growth.md. Every compaction-stage fault is
8388/// injected deterministically through `fault::Stage`.
8389#[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    /// A history-heavy store: every record written through both write paths
8424    /// several times, and a quarter of them deleted.
8425    fn churn(db: &FeltDb, records: usize, rounds: usize) {
8426        for round in 0..rounds {
8427            for id in 0..records {
8428                // Values the size of a typical application record, so the
8429                // reclaimable superseded state is what it is in practice.
8430                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    /// Everything a compaction must preserve, as one comparable value.
8450    #[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        // What is reclaimed is superseded state and the operations beyond the
8494        // window; canonical history (revisions, commit attributions) stays.
8495        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        // Reopen: the same meaning, the same revision, the same window.
8506        let db = reopen(&path, db);
8507        assert_eq!(meaning(&db), before);
8508        assert_eq!(retained(&db), STANDALONE_OPERATION_WINDOW, "retained operations survive reopen");
8509        // Idempotency survives: an old transaction id answers as a duplicate.
8510        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        // New commits continue the revision sequence, and survive another reopen.
8517        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            // The store keeps working and the next compaction succeeds.
8539            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        // The two on-disk states a kill can leave before the switch: a partial
8569        // snapshot, and a complete one that was never renamed.
8570        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                // Produce the real snapshot bytes from a copy, then plant them.
8578                let copy = path.with_file_name("copy.log");
8579                fs::copy(&path, &copy).unwrap();
8580                let other = FeltDb::open(&copy).unwrap();
8581                other.compact_operation_log(&[]).unwrap();
8582                drop(other);
8583                let mut bytes = fs::read(&copy).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            // Let the writer in between rewrites; the rewrite holds the lock.
8613            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            // A rewrite of a rewritten log is the same log: its size is a
8637            // function of state and history, not of how often it was rewritten.
8638            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        // The fact survives restart (sync metadata), so it is not forgotten.
8675        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        // A header without `authority_revision` replays with the old counting,
8682        // so revisions a store already reported do not change under upgrade.
8683        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}