use super::identity_group_keys::{memory_configs, random_graph, run, spill_directory};
use super::*;
use hawdb_plan_cypher::PatternProjection;
const QUERIES: &[(&str, bool)] = &[
(
"MATCH (a:A) RETURN id(a) AS i, [(a)-[:R]->(b) | b.name] AS v, NOT ((a)-[:S]->()) AS p",
false,
),
(
"MATCH (a:A) OPTIONAL MATCH (a)-[:R]->(b) WITH a, collect(b) AS bs \
RETURN id(a) AS i, [p IN bs | [(p)-[:S]->(c) | c.name]] AS v, \
size([p IN bs WHERE (p)-[:R]->(a)]) AS back",
false,
),
(
"MATCH (a)-[]->(b) WITH a, collect(b) AS bs \
RETURN id(a) AS i, [p IN bs WHERE (p)-[:R]->()<-[:S]-(a) | p.age] AS v",
false,
),
(
"MATCH (a)-[r:R]->(b) WITH a, collect(r) AS rs \
RETURN id(a) AS i, [q IN rs | [(x)-[q]->(y) | [id(x), id(y), q.w]]] AS v",
false,
),
(
"MATCH (a:A)-[r]->(b) WITH id(a) AS i, head(collect(b)) AS x, head(collect(r)) AS q \
RETURN i, [(x)-[:R]->(y) | y.age] AS v, [(s)-[q]->(t) | id(t)] AS w",
false,
),
(
"MATCH (a:A) WITH a AS x, a.name AS n RETURN n, [(x)-[]->(y) | id(y)] AS v",
false,
),
(
"MATCH (a)-[]->(b) WITH a, collect(b) AS bs UNWIND bs AS x \
RETURN id(a) AS i, x.name AS xn, [(x)-[:S]->(y) | y.name] AS v",
false,
),
(
"MATCH (a:B) OPTIONAL MATCH (a)-[:S]->(b:A) \
RETURN id(a) AS i, [(b)-[]->(y) | id(y)] AS v, NOT ((b)-[:R]->()) AS p",
false,
),
(
"MATCH (a:B) OPTIONAL MATCH (a)-[r:S]->(b:A) WITH a, head(collect(b)) AS x, head(collect(r)) AS q \
RETURN id(a) AS i, x IS NULL AS missing, [(x)-[]->(y) | id(y)] AS v, \
[p IN [x, null] | size([(p)-[]->() | 1])] AS n, [(s)-[q]->(t) | id(t)] AS w",
false,
),
(
"MATCH (x:A)-[r:R]->(y) RETURN id(r) AS i, \
[(a)-[:S]->(b) WHERE a = startNode(r) | id(b)] AS v, \
[(a)-[:R]->(b) WHERE b = endNode(r) | id(a)] AS w",
false,
),
(
"MATCH (x:A)-[r]->(y) RETURN id(r) AS i, \
[(a)-[:R]->(b) WHERE a = x OR a = y | [id(a), id(b)]] AS v",
false,
),
(
"MATCH (x)-[r:S]->(y) RETURN id(r) AS i, \
[(a)-[:R]->(m)-[:S]->(b) WHERE (a = x AND b = y) OR (a = y AND b = x) | id(m)] AS v",
false,
),
(
"MATCH (x:A)-[r]->(y) RETURN id(r) AS i, \
[(a)-[:R]->(b) WHERE a = r | id(b)] AS v, [(a)-[:R]->(b) WHERE a = null | id(b)] AS w, \
[(a)-[:R]->(b) WHERE a = 1 | id(b)] AS z",
false,
),
(
"MATCH (x:B) OPTIONAL MATCH (x)-[:S]->(y:A) \
RETURN id(x) AS i, [(a)-[:R]->(b) WHERE a = y | id(b)] AS v",
false,
),
(
"MATCH (a:A)-[]->(b) WITH a, collect(b) AS bs \
RETURN id(a) AS i, [p IN bs | [(c)-[:R]->(d) WHERE c = p | id(d)]] AS v",
false,
),
(
"MATCH (a:A)-[]->(b) WITH id(a) AS i, head(collect(b)) AS x \
RETURN i, [(c)-[:S]->(d) WHERE d = x | id(c)] AS v",
false,
),
(
"MATCH (a:A) RETURN id(a) AS i, \
[(a)-[:R]->(b) | [(c)-[:S]->(d) WHERE c = b | id(d)]] AS v",
false,
),
(
"MATCH (a)-[]->(b) WITH a, collect(b) AS bs \
WITH a, size([p IN bs WHERE (p)-[:R]->()]) AS k ORDER BY k DESC, id(a) LIMIT 5 \
RETURN id(a) AS i, k",
true,
),
];
fn plan_with(query: &str, legacy: bool) -> (LogicalPlan, PhysicalPlan) {
let planned = if legacy {
hawdb_plan_cypher::with_legacy_pattern_value_imports(|| {
hawdb_plan_cypher::plan_pipeline_query(query, &BTreeMap::new())
})
} else {
hawdb_plan_cypher::plan_pipeline_query(query, &BTreeMap::new())
};
let planned = planned.unwrap_or_else(|error| panic!("{query}: {error}"));
(planned.clone(), lower(planned))
}
fn patterns(plan: &PhysicalPlan) -> Vec<PatternProjection> {
fn collect(expression: &ProjectionExpression, found: &mut Vec<PatternProjection>) {
if let ProjectionExpression::Pattern(pattern) = expression {
found.push(pattern.as_ref().clone());
}
expression.all_children(|child| {
collect(child, found);
true
});
}
let mut found = Vec::new();
hawdb_plan_cypher::visit_plan(plan, &mut |node| {
if let PhysicalPlan::ProjectExec { items, .. } = node {
for item in items {
collect(&item.expression, &mut found);
}
}
});
found
}
#[test]
fn identity_imports_match_value_imports_on_random_graphs() {
let mut refused = Vec::new();
let mut runs = 0usize;
for seed in 1..=8u64 {
let graph = random_graph(seed);
let directory = spill_directory("pattern-identity-imports", seed);
for (config, memory) in memory_configs(&directory) {
for (query, ordered) in QUERIES {
let context = format!("seed {seed}, {config}: {query}");
let (_, current) = plan_with(query, false);
let (_, legacy) = plan_with(query, true);
let current_patterns = patterns(¤t);
assert!(!current_patterns.is_empty(), "{context}");
assert!(
current_patterns
.iter()
.any(|pattern| !pattern.identity_columns.is_empty()
|| pattern.arguments.is_empty()),
"{context}"
);
assert!(
patterns(&legacy)
.iter()
.all(|pattern| pattern.identity_columns.is_empty()),
"{context}"
);
runs += 1;
let actual = run(&graph, ¤t, &memory);
let expected = run(&graph, &legacy, &memory);
assert_eq!(actual.ledger.used_bytes, 0, "{context}");
assert_eq!(expected.ledger.used_bytes, 0, "{context}");
assert!(
actual.ledger.peak_bytes <= expected.ledger.peak_bytes,
"{context}: peak {} > {}",
actual.ledger.peak_bytes,
expected.ledger.peak_bytes
);
let (mut actual, mut expected) = match (actual.rows, expected.rows) {
(Ok(actual), Ok(expected)) => (actual, expected),
(Err(actual), Err(expected)) => {
assert_eq!(actual.to_string(), expected.to_string(), "{context}");
refused.push(context);
continue;
}
(actual, expected) => panic!(
"{context}: current {:?}, legacy {:?}",
actual.map(|rows| rows.len()),
expected.map(|rows| rows.len())
),
};
if !ordered {
actual.sort();
expected.sort();
}
assert_eq!(actual, expected, "{context}");
}
}
std::fs::remove_dir_all(&directory).ok();
}
assert!(
refused.len() * 10 <= runs,
"too many refused runs: {refused:#?}"
);
}
#[test]
fn slot_seeds_skip_the_existence_read() {
let graph = random_graph(3);
let memory = ExecutionMemoryConfig::default();
for query in [
"MATCH (x:A)-[r:R]->(y) RETURN [(a)-[:S]->(b) WHERE a = startNode(r) | id(b)] AS v",
"MATCH (x:A)-[r]->(y) RETURN [(a)-[:R]->(b) WHERE a = x OR a = y | id(b)] AS v",
] {
let current = run(&graph, &plan_with(query, false).1, &memory);
let legacy = run(&graph, &plan_with(query, true).1, &memory);
assert_eq!(current.rows.unwrap(), legacy.rows.unwrap(), "{query}");
assert_eq!(current.node_owned_reads, 0, "{query}");
assert!(legacy.node_owned_reads > 0, "{query}");
}
let query = "MATCH (a:A)-[]->(b) WITH a, collect(b) AS bs \
RETURN [p IN bs | [(c)-[:R]->(d) WHERE c = p | id(d)]] AS v";
let current = run(&graph, &plan_with(query, false).1, &memory);
let legacy = run(&graph, &plan_with(query, true).1, &memory);
assert_eq!(current.rows.unwrap(), legacy.rows.unwrap(), "{query}");
assert!(current.node_owned_reads > 0, "{query}");
assert_eq!(current.node_owned_reads, legacy.node_owned_reads, "{query}");
}
#[test]
fn import_errors_are_unchanged() {
let graph = random_graph(5);
let memory = ExecutionMemoryConfig::default();
let error = |plan: &PhysicalPlan| {
let outcome = run(&graph, plan, &memory);
assert_eq!(outcome.ledger.used_bytes, 0);
match outcome.rows {
Ok(rows) => panic!("expected an error, got {rows:?}"),
Err(error) => error.to_string(),
}
};
for (query, expected) in [
(
"MATCH ()-[r]->() WITH collect(r) AS rs RETURN [q IN rs | NOT ((q)-[]->())] AS v",
"MATCH variable 'q' must be bound to a node, found a map",
),
(
"MATCH (a)-[]->(b) WITH collect(b) AS bs RETURN [p IN bs | [(x)-[p]->(y) | 1]] AS v",
"MATCH variable 'p' must be bound to a relationship, found a map",
),
(
"MATCH (a)-[r]->(b) WITH head(collect(r)) AS x RETURN [(x)-[]->(y) | y] AS v",
"MATCH variable 'x' must be bound to a node, found a map",
),
(
"MATCH (a:A) WITH collect(a.name) AS ns RETURN [p IN ns | NOT ((p)-[]->())] AS v",
"must be bound to a node, found a string",
),
(
"MATCH (a:A) WITH {_id: 1} AS x RETURN [(x)-[]->(y) | y] AS v",
"MATCH variable 'x' must be bound to a node, found a map",
),
(
"MATCH (a:A) WITH {_id: 'one', labels: []} AS x RETURN [(x)-[]->(y) | y] AS v",
"typed graph value lacks identity field '_id'",
),
(
"MATCH (a:A) WITH {_id: 999999, labels: []} AS x RETURN [(x)-[]->(y) | y] AS v",
"bound node disappeared during MATCH",
),
] {
let current = error(&plan_with(query, false).1);
let legacy = error(&plan_with(query, true).1);
assert!(current.contains(expected), "{query}: {current}");
assert_eq!(current, legacy, "{query}");
}
let query = "MATCH (a:A)-[]->(b) WITH a, head(collect(b)) AS x \
RETURN [(x)-[]->(y) | 1] AS v, [(a)-[]->(y) | 1] AS w";
for (rename, expected) in [
("a", "missing variable 'zz' during pattern evaluation"),
("x", "missing column 'zz' during projection"),
] {
let mut errors = Vec::new();
for legacy in [false, true] {
let (logical, _) = plan_with(query, legacy);
errors.push(error(&lower(rename_reads(logical, rename))));
}
assert!(errors[0].contains(expected), "{rename}: {}", errors[0]);
assert_eq!(errors[0], errors[1], "{rename}");
}
}
fn rename_reads(plan: LogicalPlan, name: &str) -> LogicalPlan {
fn rename(expression: &mut ProjectionExpression, name: &str) {
match expression {
ProjectionExpression::Column(column) if column == name => *column = "zz".into(),
ProjectionExpression::Pattern(pattern) => {
for variable in &mut pattern.graph_variables {
if variable == name {
*variable = "zz".into();
}
}
for program in pattern.programs_mut() {
for step in &mut program.steps {
match step {
GraphMatchStep::Node(node) if node.variable == name => {
node.variable = "zz".into()
}
GraphMatchStep::Expand { source, .. } if source == name => {
*source = "zz".into()
}
_ => {}
}
}
}
}
_ => {}
}
let _ = expression.try_for_each_child_mut(|child| {
rename(child, name);
Ok::<(), std::convert::Infallible>(())
});
}
match plan {
LogicalPlan::Project { mut items, input } => {
for item in &mut items {
rename(&mut item.expression, name);
}
LogicalPlan::Project { items, input }
}
plan => panic!("unexpected plan: {plan:?}"),
}
}