use super::core_loop::CoreLoop;
use crate::engine::kv::RegisterIndexParams;
impl CoreLoop {
pub(super) fn try_replay_kv_index(
&mut self,
payload: &[u8],
tenant_id: u64,
database_id: u64,
now_ms: u64,
record_lsn: u64,
tombstones: &nodedb_wal::TombstoneSet,
) -> Option<usize> {
if let Some(applied) = self.try_replay_kv_register_index(
payload,
tenant_id,
database_id,
now_ms,
record_lsn,
tombstones,
) {
return Some(applied);
}
self.try_replay_kv_drop_index(payload, tenant_id, database_id, record_lsn, tombstones)
}
fn try_replay_kv_register_index(
&mut self,
payload: &[u8],
tenant_id: u64,
database_id: u64,
now_ms: u64,
record_lsn: u64,
tombstones: &nodedb_wal::TombstoneSet,
) -> Option<usize> {
let (disc, collection, field, field_position, backfill) =
zerompk::from_msgpack::<(&str, String, String, usize, bool)>(payload).ok()?;
if disc != "kv_register_index" {
return None;
}
let tombstones = &tombstones.for_database(database_id);
if self.skip_kv_replay_record(tombstones, tenant_id, &collection, record_lsn) {
return Some(0);
}
let backfilled = self.kv_engine.register_index(RegisterIndexParams {
database_id,
tenant_id,
collection: &collection,
field: &field,
field_position,
backfill,
now_ms,
});
Some(backfilled)
}
fn try_replay_kv_drop_index(
&mut self,
payload: &[u8],
tenant_id: u64,
database_id: u64,
record_lsn: u64,
tombstones: &nodedb_wal::TombstoneSet,
) -> Option<usize> {
let (disc, collection, field) =
zerompk::from_msgpack::<(&str, String, String)>(payload).ok()?;
if disc != "kv_drop_index" {
return None;
}
let tombstones = &tombstones.for_database(database_id);
if self.skip_kv_replay_record(tombstones, tenant_id, &collection, record_lsn) {
return Some(0);
}
let dropped = self
.kv_engine
.drop_index(database_id, tenant_id, &collection, &field);
Some(dropped)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::bridge::envelope::PhysicalPlan;
use crate::control::server::wal_dispatch::wal_append_if_write;
use crate::types::{DatabaseId, TenantId, VShardId};
use crate::wal::manager::WalManager;
use nodedb_physical::physical_plan::KvOp;
use nodedb_types::Surrogate;
use nodedb_wal::TombstoneSet;
use super::CoreLoop;
const TID: u64 = 1;
struct CoreHarness {
core: CoreLoop,
_req_tx: nodedb_bridge::buffer::Producer<crate::bridge::dispatch::BridgeRequest>,
_resp_rx: nodedb_bridge::buffer::Consumer<crate::bridge::dispatch::BridgeResponse>,
_dir: tempfile::TempDir,
}
fn make_core() -> CoreHarness {
use crate::bridge::dispatch::{BridgeRequest, BridgeResponse};
use nodedb_bridge::buffer::RingBuffer;
let dir = tempfile::tempdir().expect("tempdir");
let (req_tx, req_rx) = RingBuffer::channel::<BridgeRequest>(64);
let (resp_tx, resp_rx) = RingBuffer::channel::<BridgeResponse>(64);
let core = CoreLoop::open(
0,
req_rx,
resp_tx,
dir.path(),
Arc::new(nodedb_types::OrdinalClock::new()),
)
.expect("open core");
CoreHarness {
core,
_req_tx: req_tx,
_resp_rx: resp_rx,
_dir: dir,
}
}
fn append_via_autocommit(plans: &[PhysicalPlan]) -> Vec<nodedb_wal::WalRecord> {
let dir = tempfile::tempdir().expect("wal tempdir");
let wal = WalManager::open_for_testing(&dir.path().join("wal")).expect("open wal");
for plan in plans {
let outcome = wal_append_if_write(
&wal,
TenantId::new(TID),
VShardId::new(0),
DatabaseId::DEFAULT,
plan,
)
.expect("wal append");
assert!(
outcome.lsn.is_some(),
"kv index writes must produce a durable WAL record"
);
}
wal.sync().expect("wal sync");
wal.replay().expect("wal replay read")
}
fn seed_put(collection: &str, key: &[u8], name: &str) -> PhysicalPlan {
PhysicalPlan::Kv(KvOp::Put {
collection: collection.into(),
key: key.to_vec(),
value: nodedb_types::json_to_msgpack(&serde_json::json!({ "name": name }))
.expect("encode seed doc"),
ttl_ms: 0,
surrogate: Surrogate::new(1),
})
}
fn lookup_by_name(core: &CoreLoop, collection: &str, name: &str) -> Vec<Vec<u8>> {
core.kv_engine.index_lookup_eq(
DatabaseId::DEFAULT.as_u64(),
TID,
collection,
"name",
name.as_bytes(),
)
}
#[test]
fn register_index_with_backfill_true_indexes_pre_existing_rows_after_replay() {
let plans = &[
seed_put("players", b"p1", "alice"),
seed_put("players", b"p2", "bob"),
PhysicalPlan::Kv(KvOp::RegisterIndex {
collection: "players".into(),
field: "name".into(),
field_position: 0,
backfill: true,
}),
];
let records = append_via_autocommit(plans);
let mut h = make_core();
h.core.replay_kv_wal(&records, 1, &TombstoneSet::new());
assert_eq!(
lookup_by_name(&h.core, "players", "alice"),
vec![b"p1".to_vec()],
"backfill=true must index rows that existed before registration"
);
assert_eq!(
lookup_by_name(&h.core, "players", "bob"),
vec![b"p2".to_vec()],
"backfill=true must index rows that existed before registration"
);
}
#[test]
fn register_index_with_backfill_false_does_not_index_pre_existing_rows_after_replay() {
let plans = &[
seed_put("players", b"p1", "alice"),
seed_put("players", b"p2", "bob"),
PhysicalPlan::Kv(KvOp::RegisterIndex {
collection: "players".into(),
field: "name".into(),
field_position: 0,
backfill: false,
}),
];
let records = append_via_autocommit(plans);
let mut h = make_core();
h.core.replay_kv_wal(&records, 1, &TombstoneSet::new());
assert!(
lookup_by_name(&h.core, "players", "alice").is_empty(),
"backfill=false must NOT index rows that existed before registration"
);
assert!(
lookup_by_name(&h.core, "players", "bob").is_empty(),
"backfill=false must NOT index rows that existed before registration"
);
}
#[test]
fn register_index_with_backfill_false_still_indexes_rows_written_after_registration() {
let plans = &[
seed_put("players", b"p1", "alice"),
PhysicalPlan::Kv(KvOp::RegisterIndex {
collection: "players".into(),
field: "name".into(),
field_position: 0,
backfill: false,
}),
seed_put("players", b"p2", "bob"),
];
let records = append_via_autocommit(plans);
let mut h = make_core();
h.core.replay_kv_wal(&records, 1, &TombstoneSet::new());
assert!(
lookup_by_name(&h.core, "players", "alice").is_empty(),
"pre-registration row must remain unindexed"
);
assert_eq!(
lookup_by_name(&h.core, "players", "bob"),
vec![b"p2".to_vec()],
"a row written after backfill=false registration must be indexed live, \
proving the index is not merely absent"
);
}
#[test]
fn drop_index_removes_index_after_replay() {
let plans = &[
seed_put("players", b"p1", "alice"),
PhysicalPlan::Kv(KvOp::RegisterIndex {
collection: "players".into(),
field: "name".into(),
field_position: 0,
backfill: true,
}),
PhysicalPlan::Kv(KvOp::DropIndex {
collection: "players".into(),
field: "name".into(),
}),
];
let records = append_via_autocommit(plans);
let mut h = make_core();
h.core.replay_kv_wal(&records, 1, &TombstoneSet::new());
assert!(
!h.core
.kv_engine
.has_indexes(DatabaseId::DEFAULT.as_u64(), TID, "players"),
"drop_index must remove the index after replay"
);
assert!(
lookup_by_name(&h.core, "players", "alice").is_empty(),
"a dropped index must not return lookups after replay"
);
}
}