tidaldb/tidal/src/db/open.rs
jx12n 9728194f16 fix: M0-M10 code-review pass2 remediation — all 91 findings
Resolves every finding in docs/reviews/M0-M10-code-review-2026-06-08-pass2.md
across the engine, network, server, and CLI crates: session restore,
replication/CRDT, WAL format and recovery, storage indexes, query/ranking
executors, cohort/community governance, and scatter-gather routing.

Adds regression tests:
- review_pass2_creator_search_filter
- review_pass2_d_replication
- review_pass2_query_for_session
- review_pass2_storage_indexes_bitmap_cache
- review_pass2_zone_a_sessions

Verified: cargo clippy -D warnings and full test suite green across all crates.
2026-06-09 12:21:00 -06:00

304 lines
13 KiB
Rust

//! Schema-aware database open logic.
use std::sync::{Arc, atomic::AtomicU64};
use roaring::RoaringBitmap;
use super::{
config::StorageMode,
state_rebuild::{rebuild_entity_state, rebuild_item_indexes},
storage_box::StorageBox,
wal_bridge::WalHandleWriter,
};
use crate::{
entities::{
CreatorItemsBitmap, HardNegIndex, InteractionLedger, PreferenceVectors, UserStateIndex,
},
ranking::{builtins::register_builtins, profile::RankingProfile, registry::ProfileRegistry},
schema::{DurabilityError, EntityId, Schema, TidalError, Timestamp},
signals::{NoopWalWriter, SignalLedger, SignalTypeId},
storage::{
InMemoryBackend,
indexes::{bitmap::BitmapIndex, range::RangeIndex},
vector::registry::EmbeddingSlotRegistry,
},
wal::{WalConfig, WalHandle},
};
/// Default preference-vector dimensionality used when no schema embedding slot
/// declares one.
///
/// The cold-start path ([`TidalDb::new`], no schema) and the
/// no-embedding-slot schema path both fall back to this single value so a future
/// change to the default cannot drift between the two open branches and the
/// schema-less constructor. Keep it in sync with the typical embedding width the
/// engine ships against.
pub const DEFAULT_PREFERENCE_DIM: usize = 128;
/// Resolve the preference-vector dimensionality from a schema.
///
/// Uses the first declared embedding slot's dimensions, falling back to
/// [`DEFAULT_PREFERENCE_DIM`] when the schema declares no embedding slot. This is
/// the single source of truth for the fallback used by every schema-aware open
/// branch.
pub fn preference_dim_for(schema: &Schema) -> usize {
schema
.embedding_slots()
.first()
.map_or(DEFAULT_PREFERENCE_DIM, |s| s.dimensions)
}
/// Bundle returned by [`TidalDb::open_with_schema`] to avoid a fragile tuple.
///
/// Each field is consumed by [`TidalDb::from_parts`] during construction.
pub struct OpenResult {
pub storage: StorageBox,
pub ledger: Arc<SignalLedger>,
pub wal: Option<WalHandle>,
pub last_seq: Arc<AtomicU64>,
pub profile_registry: ProfileRegistry,
pub category_index: BitmapIndex,
pub format_index: BitmapIndex,
pub creator_index: BitmapIndex,
pub tag_index: BitmapIndex,
pub duration_index: RangeIndex<u32>,
pub created_at_index: RangeIndex<u64>,
pub universe: RoaringBitmap,
pub embedding_registry: EmbeddingSlotRegistry,
pub schema_def: Schema,
// M3 entity state rebuilt from durable storage.
pub creator_items: CreatorItemsBitmap,
pub user_state: UserStateIndex,
pub hard_negatives: HardNegIndex,
pub interaction_ledger: InteractionLedger,
pub preference_vectors: PreferenceVectors,
/// Session journal events recovered on startup (for session crash recovery).
pub session_events: Vec<crate::wal::format::SessionWalEvent>,
}
impl super::TidalDb {
/// Open storage and ledger components for the given schema.
///
/// Called from `TidalDbBuilder::open()` when `with_schema()` was called.
/// Returns an `OpenResult` containing all components needed by `from_parts`.
#[allow(clippy::too_many_lines)] // Construction logic is inherently verbose; splitting would scatter initialization flow.
pub(crate) fn open_with_schema(
config: &super::Config,
schema: Schema,
schema_profiles: Vec<RankingProfile>,
) -> crate::Result<OpenResult> {
let last_seq = Arc::new(AtomicU64::new(0));
// Initialize profile registry with builtins.
let mut profile_registry = ProfileRegistry::new();
register_builtins(&mut profile_registry).map_err(|e| {
TidalError::internal("open", format!("failed to register builtin profiles: {e}"))
})?;
// Bind the schema so schema-defined profiles are validated against the
// actual signal vocabulary (builtins are exempt -- already registered,
// and they intentionally reference a superset of any one schema).
profile_registry.with_schema(schema.clone());
// Register schema-defined profiles, overriding matching builtins.
for profile in schema_profiles {
let name = profile.name.clone();
profile_registry.override_register(profile).map_err(|e| {
TidalError::internal(
"open",
format!("failed to register schema profile '{name}': {e}"),
)
})?;
}
// Initialize M2 indexes (empty -- populated as items are written).
let category_index = BitmapIndex::new("category");
let format_index = BitmapIndex::new("format");
let creator_index = BitmapIndex::new("creator");
let tag_index = BitmapIndex::new("tags");
let duration_index = RangeIndex::new("duration");
let created_at_index = RangeIndex::new("created_at");
let universe = RoaringBitmap::new();
let embedding_registry = EmbeddingSlotRegistry::new();
let schema_def = schema.clone();
match config.mode {
StorageMode::Ephemeral => {
let storage = StorageBox::Memory {
items: InMemoryBackend::default(),
users: InMemoryBackend::default(),
creators: InMemoryBackend::default(),
};
let ledger = Arc::new(SignalLedger::new(schema, Box::new(NoopWalWriter)));
// Read preference vector dimensionality from the schema.
let pref_dim = preference_dim_for(&schema_def);
Ok(OpenResult {
storage,
ledger,
wal: None,
last_seq,
profile_registry,
category_index,
format_index,
creator_index,
tag_index,
duration_index,
created_at_index,
universe,
embedding_registry,
schema_def,
creator_items: CreatorItemsBitmap::new(),
user_state: UserStateIndex::new(),
hard_negatives: HardNegIndex::new(),
interaction_ledger: InteractionLedger::new(),
preference_vectors: PreferenceVectors::new(pref_dim),
session_events: Vec::new(),
})
}
StorageMode::Persistent => {
let data_dir = config.data_dir.as_ref().ok_or_else(|| {
TidalError::internal("open", "persistent mode requires data_dir")
})?;
// Open fjall storage.
let fjall_storage =
crate::storage::FjallStorage::open(data_dir).map_err(TidalError::from)?;
let storage = StorageBox::Fjall(fjall_storage);
// Build WAL config. `WalConfig.dir` is the *parent* of the
// "wal/" subdirectory; the WAL therefore defaults to
// {data_dir}/wal. When the builder supplied an explicit `wal_dir`
// override, honor it via `wal_dir_override` so the WAL is created
// at exactly that path — and so the readers below
// (`config.resolved_wal_dir()`) point at the same place. The
// builder resolves `config.wal_dir` to the override-or-default at
// open time, so this threads one source of truth to the writer.
let wal_config = WalConfig {
dir: data_dir.clone(),
wal_dir_override: config.wal_dir.clone(),
..WalConfig::default()
};
let (wal, replayed_events, session_events) =
WalHandle::open(wal_config).map_err(|e| {
TidalError::Durability(DurabilityError {
message: format!("WAL open failed: {e}"),
})
})?;
// Build the WAL bridge for the ledger.
let wal_writer =
Box::new(WalHandleWriter::new(wal.sender(), Arc::clone(&last_seq)));
// Construct the ledger.
let ledger = Arc::new(SignalLedger::new(schema, wal_writer));
// Restore signal state from the last checkpoint.
if let Err(e) = ledger.restore(storage.items_engine()) {
tracing::warn!(
error = %e,
"signal ledger restore failed; starting from empty state"
);
}
// Replay WAL events that post-date the checkpoint.
for event in replayed_events {
let type_id = SignalTypeId::new(u16::from(event.signal_type));
let entity_id = EntityId::new(event.entity_id);
let weight = f64::from(event.weight);
let timestamp = Timestamp::from_nanos(event.timestamp_nanos);
ledger.apply_wal_event(type_id, entity_id, weight, timestamp);
}
// Rebuild in-memory entity state from durable storage.
// This reconstructs blocked/hidden/follows user state and
// interaction weights from persisted relationship edges. The
// creator-items bitmap is populated by `rebuild_item_indexes`
// below (shared with the write path) so it cannot drift.
let user_state = UserStateIndex::new();
let creator_items = CreatorItemsBitmap::new();
let interaction_ledger = InteractionLedger::new();
rebuild_entity_state(&storage, &user_state, &interaction_ledger)?;
// Restore the signal-driven UserStateIndex maps (completion /
// saved-timestamp / liked) from their durable Tag::UserState rows
// so the InProgress filter and DateSaved sort survive a restart.
if let Err(e) = user_state.restore(storage.items_engine()) {
tracing::warn!(
error = %e,
"user-state restore failed; InProgress/DateSaved start empty"
);
}
// Restore hard negatives from durable Tag::HardNeg rows so a
// skip/dislike-rejected item stays excluded after a crash.
let hard_negatives = HardNegIndex::new();
if let Err(e) = hard_negatives.restore(storage.items_engine()) {
tracing::warn!(
error = %e,
"hard-negative restore failed; rejected items may resurface"
);
}
// Rebuild item indexes (universe bitmap + bitmap/range indexes +
// creator-items) from persisted metadata so queries and /health
// work immediately. The shared `ItemIndexes::index` is the SAME
// path the live write uses, so a restart cannot diverge.
let mut universe = universe;
rebuild_item_indexes(
&storage,
&mut universe,
&super::state_rebuild::ItemIndexes {
category: &category_index,
format: &format_index,
creator: &creator_index,
tag: &tag_index,
duration: &duration_index,
created_at: &created_at_index,
creator_items: &creator_items,
},
)?;
// Read preference vector dimensionality from the schema.
let pref_dim = preference_dim_for(&schema_def);
// Restore preference vectors from their Tag::Preference checkpoint
// so personalized ranking does not snap to cold-start after a crash.
let preference_vectors = PreferenceVectors::new(pref_dim);
if let Err(e) = preference_vectors.restore(storage.items_engine()) {
tracing::warn!(
error = %e,
"preference vector restore failed; starting from cold-start"
);
}
Ok(OpenResult {
storage,
ledger,
wal: Some(wal),
last_seq,
profile_registry,
category_index,
format_index,
creator_index,
tag_index,
duration_index,
created_at_index,
universe,
embedding_registry,
schema_def,
creator_items,
user_state,
hard_negatives,
interaction_ledger,
preference_vectors,
session_events,
})
}
}
}
}