use std::collections::HashSet;
use nodedb_types::Surrogate;
use nodedb_types::columnar::StrictSchema;
use crate::bridge::scan_filter::ScanFilter;
use crate::data::executor::core_loop::CoreLoop;
use crate::data::executor::core_loop::filter_match::matches_with_resolved_schema;
use crate::data::executor::handlers::transaction::overlay::{Staged, StagedTtl};
use crate::engine::document::store::{extract_index_values, surrogate_to_doc_id};
use crate::engine::kv::current_ms;
use crate::types::{DatabaseId, TenantId, TxnId};
pub(in crate::data::executor) struct IndexOverlayMergeParams<'a> {
pub txn_id: TxnId,
pub coll_key: &'a (DatabaseId, TenantId, String),
pub path: &'a str,
pub value: &'a str,
pub is_array: bool,
pub case_insensitive: bool,
pub residual: &'a [ScanFilter],
pub strict_schema: Option<&'a StrictSchema>,
}
impl CoreLoop {
pub(in crate::data::executor) fn merge_overlay_into_scan(
&self,
txn_id: TxnId,
coll_key: &(DatabaseId, TenantId, String),
rows: &mut Vec<(String, Vec<u8>)>,
matches: &dyn Fn(&[u8]) -> bool,
) {
self.touch_overlay(txn_id);
let Some(overlay) = self.txn_overlays.get(&txn_id) else {
return;
};
let mut seen: HashSet<u32> = rows
.iter()
.filter_map(|(k, _)| u32::from_str_radix(k, 16).ok())
.collect();
rows.retain_mut(|(row_key, body)| {
let Ok(surrogate) = u32::from_str_radix(row_key, 16) else {
return true;
};
match overlay.get(coll_key, surrogate) {
Some(Staged::Tombstone) => false,
Some(Staged::Put(staged_body)) => {
*body = staged_body.clone();
matches(body)
}
None => true,
}
});
for (surrogate, staged) in overlay.iter_for_collection(coll_key) {
if seen.contains(&surrogate) {
continue;
}
match staged {
Staged::Put(body) => {
if matches(body) {
rows.push((surrogate_to_doc_id(Surrogate::new(surrogate)), body.clone()));
seen.insert(surrogate);
}
}
Staged::Tombstone => {}
}
}
}
pub(in crate::data::executor) fn merge_kv_overlay_into_scan(
&self,
txn_id: TxnId,
coll_key: &(DatabaseId, TenantId, String),
rows: &mut Vec<(Vec<u8>, Vec<u8>)>,
matches: &dyn Fn(&[u8]) -> bool,
) {
self.touch_overlay(txn_id);
let Some(overlay) = self.txn_overlays.get(&txn_id) else {
return;
};
let mut seen: HashSet<String> = rows
.iter()
.map(|(key, _)| super::super::stage_write::hex_key(key))
.collect();
let now_ms = current_ms();
let staged_expired = |doc_id: &str| -> bool {
matches!(
overlay.get_ttl_by_doc_id(coll_key, doc_id),
Some(StagedTtl::ExpireAt(t)) if t <= now_ms
)
};
rows.retain_mut(|(key, value)| {
let doc_id = super::super::stage_write::hex_key(key);
if staged_expired(&doc_id) {
return false;
}
match overlay.get_by_doc_id(coll_key, &doc_id) {
Some(Staged::Tombstone) => false,
Some(Staged::Put(staged_value)) => {
*value = staged_value.clone();
matches(value)
}
None => true,
}
});
for (doc_id, staged) in overlay.iter_doc_entries_for_collection(coll_key) {
if seen.contains(doc_id) {
continue;
}
if let Staged::Put(value) = staged
&& !staged_expired(doc_id)
&& matches(value)
&& let Some(key) = super::super::stage_write::unhex_key(doc_id)
{
rows.push((key, value.clone()));
seen.insert(doc_id.to_string());
}
}
}
pub(in crate::data::executor) fn merge_overlay_into_index_lookup(
&self,
params: IndexOverlayMergeParams<'_>,
doc_ids: &mut Vec<String>,
decode: &dyn Fn(&[u8]) -> Option<serde_json::Value>,
) {
let IndexOverlayMergeParams {
txn_id,
coll_key,
path,
value,
is_array,
case_insensitive,
residual,
strict_schema,
} = params;
self.touch_overlay(txn_id);
let Some(overlay) = self.txn_overlays.get(&txn_id) else {
return;
};
let normalize = |s: String| -> String {
if case_insensitive {
s.to_lowercase()
} else {
s
}
};
let target = normalize(value.to_string());
let value_matches = |body: &[u8]| -> bool {
let Some(doc) = decode(body) else {
return false;
};
extract_index_values(&doc, path, is_array)
.into_iter()
.any(|v| normalize(v) == target)
};
let residual_matches = |body: &[u8]| -> bool {
residual.is_empty() || matches_with_resolved_schema(strict_schema, residual, body)
};
let mut seen: HashSet<u32> = doc_ids
.iter()
.filter_map(|id| u32::from_str_radix(id, 16).ok())
.collect();
doc_ids.retain(|doc_id| {
let Ok(surrogate) = u32::from_str_radix(doc_id, 16) else {
return true;
};
match overlay.get(coll_key, surrogate) {
Some(Staged::Tombstone) => false,
Some(Staged::Put(body)) => value_matches(body) && residual_matches(body),
None => true,
}
});
for (surrogate, staged) in overlay.iter_for_collection(coll_key) {
if seen.contains(&surrogate) {
continue;
}
match staged {
Staged::Put(body) => {
if value_matches(body) && residual_matches(body) {
doc_ids.push(surrogate_to_doc_id(Surrogate::new(surrogate)));
seen.insert(surrogate);
}
}
Staged::Tombstone => {}
}
}
}
pub(in crate::data::executor) fn overlay_or_base_body(
&self,
txn_id: Option<TxnId>,
coll_key: &(DatabaseId, TenantId, String),
doc_id: &str,
base: impl FnOnce() -> crate::Result<Option<Vec<u8>>>,
) -> crate::Result<Option<Vec<u8>>> {
if let Some(txn_id) = txn_id {
self.touch_overlay(txn_id);
if let Some(overlay) = self.txn_overlays.get(&txn_id)
&& let Ok(surrogate) = u32::from_str_radix(doc_id, 16)
{
match overlay.get(coll_key, surrogate) {
Some(Staged::Put(body)) => return Ok(Some(body.clone())),
Some(Staged::Tombstone) => return Ok(None),
None => {}
}
}
}
base()
}
pub(in crate::data::executor) fn index_path_flags(
&self,
config_key: &(DatabaseId, TenantId, String),
path: &str,
) -> (bool, bool) {
self.doc_configs
.get(config_key)
.and_then(|config| config.index_paths.iter().find(|ip| ip.path == path))
.map_or((false, false), |ip| (ip.is_array, ip.case_insensitive))
}
pub(in crate::data::executor) fn decode_indexed_body(
&self,
config_key: &(DatabaseId, TenantId, String),
body: &[u8],
) -> Option<serde_json::Value> {
let config = self.doc_configs.get(config_key)?;
self.decode_stored_document(config, body)
}
}