use crate::db::query::explain::{
ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
ExplainPropertyMap,
};
use super::{ExecutionNodeIdentity, ExecutionNodeLayer, collect_execution_node_identities};
fn extract_node_ids_from_canonical_json(json: &str) -> Vec<u64> {
let mut ids = Vec::new();
let mut remaining = json;
let node_id_prefix = "\"node_id\":";
while let Some(prefix_index) = remaining.find(node_id_prefix) {
let value_start = prefix_index.saturating_add(node_id_prefix.len());
let digits_start = value_start;
let digits_end = remaining[digits_start..]
.find(|ch: char| !ch.is_ascii_digit())
.map_or(remaining.len(), |relative| {
digits_start.saturating_add(relative)
});
let parsed = remaining[digits_start..digits_end]
.parse::<u64>()
.expect("node_id field must parse as u64");
ids.push(parsed);
remaining = &remaining[digits_end..];
}
ids
}
#[test]
fn execution_node_identity_from_explain_node_preserves_node_and_layer_contract() {
let descriptor = ExplainExecutionNodeDescriptor {
node_type: ExplainExecutionNodeType::IndexPrefixScan,
execution_mode: ExplainExecutionMode::Materialized,
access_strategy: None,
predicate_pushdown: None,
filter_expr: None,
residual_filter_expr: None,
residual_filter_predicate: None,
projection: None,
ordering_source: None,
limit: None,
cursor: None,
covering_scan: None,
rows_expected: None,
children: Vec::new(),
node_properties: ExplainPropertyMap::new(),
};
let identity = ExecutionNodeIdentity::from_explain_node(7, &descriptor);
assert_eq!(identity.node_id(), 7);
assert_eq!(identity.node_type(), "IndexPrefixScan");
assert_eq!(identity.layer(), ExecutionNodeLayer::Scan);
assert_eq!(identity.layer().as_str(), "scan");
}
#[test]
fn execution_node_identity_constructor_is_stable() {
let identity = ExecutionNodeIdentity::new(3, "LimitOffset", ExecutionNodeLayer::Terminal);
assert_eq!(identity.node_id(), 3);
assert_eq!(identity.node_type(), "LimitOffset");
assert_eq!(identity.layer(), ExecutionNodeLayer::Terminal);
assert_eq!(identity.layer().as_str(), "terminal");
}
#[test]
fn execution_node_identity_collection_preserves_node_id_order_and_layer_mapping() {
let descriptor = ExplainExecutionNodeDescriptor {
node_type: ExplainExecutionNodeType::TopNSeek,
execution_mode: ExplainExecutionMode::Streaming,
access_strategy: None,
predicate_pushdown: None,
filter_expr: None,
residual_filter_expr: None,
residual_filter_predicate: None,
projection: None,
ordering_source: None,
limit: None,
cursor: None,
covering_scan: None,
rows_expected: None,
children: vec![
ExplainExecutionNodeDescriptor {
node_type: ExplainExecutionNodeType::IndexPrefixScan,
execution_mode: ExplainExecutionMode::Materialized,
access_strategy: None,
predicate_pushdown: None,
filter_expr: None,
residual_filter_expr: None,
residual_filter_predicate: None,
projection: None,
ordering_source: None,
limit: None,
cursor: None,
covering_scan: None,
rows_expected: None,
children: Vec::new(),
node_properties: ExplainPropertyMap::new(),
},
ExplainExecutionNodeDescriptor {
node_type: ExplainExecutionNodeType::AggregateCount,
execution_mode: ExplainExecutionMode::Materialized,
access_strategy: None,
predicate_pushdown: None,
filter_expr: None,
residual_filter_expr: None,
residual_filter_predicate: None,
projection: None,
ordering_source: None,
limit: None,
cursor: None,
covering_scan: None,
rows_expected: None,
children: vec![ExplainExecutionNodeDescriptor {
node_type: ExplainExecutionNodeType::LimitOffset,
execution_mode: ExplainExecutionMode::Materialized,
access_strategy: None,
predicate_pushdown: None,
filter_expr: None,
residual_filter_expr: None,
residual_filter_predicate: None,
projection: None,
ordering_source: None,
limit: None,
cursor: None,
covering_scan: None,
rows_expected: None,
children: Vec::new(),
node_properties: ExplainPropertyMap::new(),
}],
node_properties: ExplainPropertyMap::new(),
},
],
node_properties: ExplainPropertyMap::new(),
};
let identities = collect_execution_node_identities(&descriptor);
let actual = identities
.iter()
.map(|identity| {
(
identity.node_id(),
identity.node_type().to_string(),
identity.layer().as_str().to_string(),
)
})
.collect::<Vec<_>>();
let expected = vec![
(0_u64, "TopNSeek".to_string(), "pipeline".to_string()),
(1_u64, "IndexPrefixScan".to_string(), "scan".to_string()),
(2_u64, "AggregateCount".to_string(), "aggregate".to_string()),
(3_u64, "LimitOffset".to_string(), "terminal".to_string()),
];
assert_eq!(
actual, expected,
"diagnostics identity collection must preserve canonical node ordering and layers",
);
}
#[test]
fn execution_node_identity_collection_node_ids_match_canonical_explain_json_ids() {
let descriptor = ExplainExecutionNodeDescriptor {
node_type: ExplainExecutionNodeType::TopNSeek,
execution_mode: ExplainExecutionMode::Streaming,
access_strategy: None,
predicate_pushdown: None,
filter_expr: None,
residual_filter_expr: None,
residual_filter_predicate: None,
projection: None,
ordering_source: None,
limit: None,
cursor: None,
covering_scan: None,
rows_expected: None,
children: vec![
ExplainExecutionNodeDescriptor {
node_type: ExplainExecutionNodeType::IndexPrefixScan,
execution_mode: ExplainExecutionMode::Materialized,
access_strategy: None,
predicate_pushdown: None,
filter_expr: None,
residual_filter_expr: None,
residual_filter_predicate: None,
projection: None,
ordering_source: None,
limit: None,
cursor: None,
covering_scan: None,
rows_expected: None,
children: Vec::new(),
node_properties: ExplainPropertyMap::new(),
},
ExplainExecutionNodeDescriptor {
node_type: ExplainExecutionNodeType::LimitOffset,
execution_mode: ExplainExecutionMode::Materialized,
access_strategy: None,
predicate_pushdown: None,
filter_expr: None,
residual_filter_expr: None,
residual_filter_predicate: None,
projection: None,
ordering_source: None,
limit: None,
cursor: None,
covering_scan: None,
rows_expected: None,
children: Vec::new(),
node_properties: ExplainPropertyMap::new(),
},
],
node_properties: ExplainPropertyMap::new(),
};
let identities = collect_execution_node_identities(&descriptor);
let identity_ids = identities
.iter()
.map(|identity| identity.node_id())
.collect::<Vec<_>>();
let json_ids = extract_node_ids_from_canonical_json(&descriptor.render_json_canonical());
assert_eq!(
identity_ids, json_ids,
"diagnostics node ids must match canonical explain JSON node ids",
);
}