1use std::sync::Arc;
12
13use super::query_graph::QueryGraph;
14use super::subquery_graph::SubqueryGraph;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TreeNodeType {
19 NodeScan,
20 RelScan,
21 BinaryJoin,
22 MultiwayJoin,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum JoinHintNode {
33 Scan { variable: String },
35 Binary {
37 left: Box<JoinHintNode>,
38 right: Box<JoinHintNode>,
39 },
40 Multiway {
42 probe: Box<JoinHintNode>,
43 builds: Vec<JoinHintNode>,
44 },
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct JoinHint {
50 pub root: JoinHintNode,
51}
52
53impl JoinHint {
54 pub fn scan(variable: impl Into<String>) -> Self {
55 Self {
56 root: JoinHintNode::Scan {
57 variable: variable.into(),
58 },
59 }
60 }
61
62 pub fn from_ast(hint: &crate::parser::ast::hint::JoinHintAst) -> Option<Self> {
69 use crate::parser::ast::hint::JoinHintAst as Ast;
70 match hint {
71 Ast::Binary { left, right } => Some(Self {
72 root: JoinHintNode::Binary {
73 left: Box::new(JoinHintNode::Scan {
74 variable: left.clone(),
75 }),
76 right: Box::new(JoinHintNode::Scan {
77 variable: right.clone(),
78 }),
79 },
80 }),
81 Ast::Multiway { probe, builds } => {
82 if builds.is_empty() {
83 return None;
84 }
85 Some(Self {
86 root: JoinHintNode::Multiway {
87 probe: Box::new(JoinHintNode::Scan {
88 variable: probe.clone(),
89 }),
90 builds: builds
91 .iter()
92 .map(|var| JoinHintNode::Scan {
93 variable: var.clone(),
94 })
95 .collect(),
96 },
97 })
98 }
99 }
100 }
101}
102
103#[derive(Debug, Clone, Default, PartialEq, Eq)]
105pub struct JoinTreeExtraInfo {
106 pub join_nodes: Vec<String>,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct JoinTreeNode {
113 pub node_type: TreeNodeType,
114 pub extra_info: JoinTreeExtraInfo,
115 pub children: Vec<JoinTreeNode>,
116}
117
118impl JoinTreeNode {
119 pub fn node_scan(variable: impl Into<String>) -> Self {
120 Self {
121 node_type: TreeNodeType::NodeScan,
122 extra_info: JoinTreeExtraInfo {
123 join_nodes: vec![variable.into()],
124 },
125 children: Vec::new(),
126 }
127 }
128
129 pub fn rel_scan(variable: impl Into<String>) -> Self {
130 Self {
131 node_type: TreeNodeType::RelScan,
132 extra_info: JoinTreeExtraInfo {
133 join_nodes: vec![variable.into()],
134 },
135 children: Vec::new(),
136 }
137 }
138
139 pub fn binary_join(left: JoinTreeNode, right: JoinTreeNode, join_nodes: Vec<String>) -> Self {
140 Self {
141 node_type: TreeNodeType::BinaryJoin,
142 extra_info: JoinTreeExtraInfo { join_nodes },
143 children: vec![left, right],
144 }
145 }
146
147 pub fn multiway_join(
148 probe: JoinTreeNode,
149 builds: Vec<JoinTreeNode>,
150 intersect_node: String,
151 ) -> Self {
152 let mut children = Vec::with_capacity(builds.len() + 1);
153 children.push(probe);
154 children.extend(builds);
155 Self {
156 node_type: TreeNodeType::MultiwayJoin,
157 extra_info: JoinTreeExtraInfo {
158 join_nodes: vec![intersect_node],
159 },
160 children,
161 }
162 }
163
164 pub fn is_leaf(&self) -> bool {
165 self.children.is_empty()
166 }
167
168 pub fn is_binary(&self) -> bool {
169 self.node_type == TreeNodeType::BinaryJoin
170 }
171
172 pub fn is_multiway(&self) -> bool {
173 self.node_type == TreeNodeType::MultiwayJoin
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct JoinTree {
180 pub root: JoinTreeNode,
181}
182
183impl JoinTree {
184 pub fn covered_subgraph(&self, query_graph: &Arc<QueryGraph>) -> SubqueryGraph {
186 covered_subgraph(&self.root, query_graph)
187 }
188}
189
190fn covered_subgraph(node: &JoinTreeNode, query_graph: &Arc<QueryGraph>) -> SubqueryGraph {
191 let mut subgraph = SubqueryGraph::new(Arc::clone(query_graph));
192 match node.node_type {
193 TreeNodeType::NodeScan => {
194 for var in &node.extra_info.join_nodes {
195 if let Some(pos) = query_graph.node_pos(var) {
196 subgraph.add_query_node(pos);
197 }
198 }
199 }
200 TreeNodeType::RelScan => {
201 for var in &node.extra_info.join_nodes {
207 if let Some(pos) = query_graph.rel_pos(var) {
208 subgraph.add_query_rel(pos);
209 if let Some((src, dst)) = query_graph.rel_endpoint_positions(pos) {
210 subgraph.add_query_node(src);
211 subgraph.add_query_node(dst);
212 }
213 }
214 }
215 }
216 TreeNodeType::BinaryJoin | TreeNodeType::MultiwayJoin => {
217 for child in &node.children {
218 subgraph.add_subquery_graph(&covered_subgraph(child, query_graph));
219 }
220 if node.node_type == TreeNodeType::MultiwayJoin {
222 for var in &node.extra_info.join_nodes {
223 if let Some(pos) = query_graph.node_pos(var) {
224 subgraph.add_query_node(pos);
225 }
226 }
227 }
228 }
229 }
230 subgraph
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
235pub enum JoinHintError {
236 #[error("unknown variable in join hint: {0}")]
237 UnknownVariable(String),
238 #[error("multi-way join needs at least one build side")]
239 MissingBuildSide,
240 #[error("multi-way join has no single common neighbor node")]
241 NoCommonIntersectNode,
242}
243
244#[derive(Debug, Default, Clone, Copy)]
246pub struct JoinTreeConstructor;
247
248impl JoinTreeConstructor {
249 pub fn construct(
250 query_graph: &Arc<QueryGraph>,
251 hint: &JoinHint,
252 ) -> Result<JoinTree, JoinHintError> {
253 Ok(JoinTree {
254 root: Self::construct_tree_node(query_graph, &hint.root)?,
255 })
256 }
257
258 fn construct_tree_node(
259 query_graph: &Arc<QueryGraph>,
260 hint_node: &JoinHintNode,
261 ) -> Result<JoinTreeNode, JoinHintError> {
262 match hint_node {
263 JoinHintNode::Scan { variable } => Self::construct_scan_node(query_graph, variable),
264 JoinHintNode::Binary { left, right } => {
265 let left = Self::construct_tree_node(query_graph, left)?;
266 let right = Self::construct_tree_node(query_graph, right)?;
267 let left_sg = covered_subgraph(&left, query_graph);
268 let right_sg = covered_subgraph(&right, query_graph);
269 let mut join_nodes: Vec<String> = left_sg
270 .get_connected_node_positions(&right_sg)
271 .into_iter()
272 .filter_map(|pos| query_graph.query_nodes.get(pos))
273 .map(|n| n.variable.clone())
274 .collect();
275 join_nodes.sort();
276 Ok(JoinTreeNode::binary_join(left, right, join_nodes))
277 }
278 JoinHintNode::Multiway { probe, builds } => {
279 Self::construct_multiway_join(query_graph, probe, builds)
280 }
281 }
282 }
283
284 fn construct_scan_node(
285 query_graph: &Arc<QueryGraph>,
286 variable: &str,
287 ) -> Result<JoinTreeNode, JoinHintError> {
288 if query_graph.node_pos(variable).is_some() {
289 return Ok(JoinTreeNode::node_scan(variable));
290 }
291 if query_graph.rel_pos(variable).is_some() {
292 return Ok(JoinTreeNode::rel_scan(variable));
293 }
294 Err(JoinHintError::UnknownVariable(variable.to_string()))
295 }
296
297 fn construct_multiway_join(
298 query_graph: &Arc<QueryGraph>,
299 probe: &JoinHintNode,
300 builds: &[JoinHintNode],
301 ) -> Result<JoinTreeNode, JoinHintError> {
302 if builds.is_empty() {
303 return Err(JoinHintError::MissingBuildSide);
304 }
305 let probe = Self::construct_tree_node(query_graph, probe)?;
306 let probe_subgraph = covered_subgraph(&probe, query_graph);
307 let mut build_nodes = Vec::with_capacity(builds.len());
308 let mut build_subgraphs = Vec::with_capacity(builds.len());
309 for child in builds {
310 let build = Self::construct_tree_node(query_graph, child)?;
311 build_subgraphs.push(covered_subgraph(&build, query_graph));
312 build_nodes.push(build);
313 }
314 let intersect = Self::common_neighbor(query_graph, &build_subgraphs, &probe_subgraph)?;
315 Ok(JoinTreeNode::multiway_join(probe, build_nodes, intersect))
316 }
317
318 fn common_neighbor(
324 query_graph: &Arc<QueryGraph>,
325 build_subgraphs: &[SubqueryGraph],
326 probe_subgraph: &SubqueryGraph,
327 ) -> Result<String, JoinHintError> {
328 let mut common: Option<Vec<usize>> = None;
329 for subgraph in build_subgraphs {
330 let mut adjacent = subgraph.get_node_neighbor_positions();
331 adjacent.extend(subgraph.node_positions());
332 adjacent.sort_unstable();
333 adjacent.dedup();
334 common = Some(match common {
335 None => adjacent,
336 Some(prev) => prev.into_iter().filter(|p| adjacent.contains(p)).collect(),
337 });
338 }
339 let common = common.unwrap_or_default();
340 let chosen = common
341 .iter()
342 .filter(|p| !probe_subgraph.contains_node(**p))
343 .min()
344 .copied();
345 chosen
346 .and_then(|pos| query_graph.query_nodes.get(pos))
347 .map(|n| n.variable.clone())
348 .ok_or(JoinHintError::NoCommonIntersectNode)
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 use crate::planning::join_order::query_graph::{QueryNode, QueryRel};
357
358 fn triangle() -> Arc<QueryGraph> {
359 let mut qg = QueryGraph::new();
360 qg.add_node(QueryNode::new("a", "a"));
361 qg.add_node(QueryNode::new("b", "b"));
362 qg.add_node(QueryNode::new("c", "c"));
363 qg.add_rel(QueryRel::new("e1", "e1", "a", "b"));
364 qg.add_rel(QueryRel::new("e2", "e2", "a", "c"));
365 qg.add_rel(QueryRel::new("e3", "e3", "b", "c"));
366 Arc::new(qg)
367 }
368
369 #[test]
370 fn scan_resolves_node_vs_rel() {
371 let qg = triangle();
372 let node = JoinTreeConstructor::construct(&qg, &JoinHint::scan("a")).expect("node");
373 assert_eq!(node.root.node_type, TreeNodeType::NodeScan);
374 let rel = JoinTreeConstructor::construct(&qg, &JoinHint::scan("e1")).expect("rel");
375 assert_eq!(rel.root.node_type, TreeNodeType::RelScan);
376 }
377
378 #[test]
379 fn unknown_variable_is_error() {
380 let qg = triangle();
381 let err = JoinTreeConstructor::construct(&qg, &JoinHint::scan("zzz")).unwrap_err();
382 assert_eq!(err, JoinHintError::UnknownVariable("zzz".to_string()));
383 }
384
385 #[test]
386 fn binary_join_collects_shared_nodes() {
387 let qg = triangle();
388 let hint = JoinHint {
389 root: JoinHintNode::Binary {
390 left: Box::new(JoinHintNode::Scan {
391 variable: "e1".to_string(),
392 }),
393 right: Box::new(JoinHintNode::Scan {
394 variable: "e2".to_string(),
395 }),
396 },
397 };
398 let tree = JoinTreeConstructor::construct(&qg, &hint).expect("binary");
399 assert_eq!(tree.root.node_type, TreeNodeType::BinaryJoin);
400 assert_eq!(tree.root.extra_info.join_nodes, vec!["a".to_string()]);
402 }
403
404 #[test]
405 fn multiway_join_finds_intersect_node() {
406 let qg = triangle();
407 let hint = JoinHint {
409 root: JoinHintNode::Multiway {
410 probe: Box::new(JoinHintNode::Binary {
411 left: Box::new(JoinHintNode::Scan {
412 variable: "a".to_string(),
413 }),
414 right: Box::new(JoinHintNode::Scan {
415 variable: "b".to_string(),
416 }),
417 }),
418 builds: vec![
419 JoinHintNode::Scan {
420 variable: "e2".to_string(),
421 },
422 JoinHintNode::Scan {
423 variable: "e3".to_string(),
424 },
425 ],
426 },
427 };
428 let tree = JoinTreeConstructor::construct(&qg, &hint).expect("multiway");
429 assert_eq!(tree.root.node_type, TreeNodeType::MultiwayJoin);
430 assert_eq!(tree.root.extra_info.join_nodes, vec!["c".to_string()]);
431 assert!(tree.root.is_multiway());
432 let covered = tree.covered_subgraph(&qg);
434 assert_eq!(covered.total_num_variables(), 5);
435 }
436
437 #[test]
438 fn from_ast_lowers_binary_and_multiway() {
439 use crate::parser::ast::hint::JoinHintAst as Ast;
440 let binary = JoinHint::from_ast(&Ast::Binary {
441 left: "e1".to_string(),
442 right: "e2".to_string(),
443 })
444 .expect("binary");
445 assert!(matches!(binary.root, JoinHintNode::Binary { .. }));
446 let multiway = JoinHint::from_ast(&Ast::Multiway {
447 probe: "e1".to_string(),
448 builds: vec!["e2".to_string(), "e3".to_string()],
449 })
450 .expect("multiway");
451 match &multiway.root {
452 JoinHintNode::Multiway { builds, .. } => assert_eq!(builds.len(), 2),
453 other => panic!("expected multiway, got {other:?}"),
454 }
455 let qg = triangle();
456 let tree = JoinTreeConstructor::construct(&qg, &binary).expect("tree");
457 assert_eq!(tree.root.node_type, TreeNodeType::BinaryJoin);
458 }
459
460 #[test]
461 fn multiway_without_builds_is_error() {
462 let qg = triangle();
463 let hint = JoinHint {
464 root: JoinHintNode::Multiway {
465 probe: Box::new(JoinHintNode::Scan {
466 variable: "a".to_string(),
467 }),
468 builds: vec![],
469 },
470 };
471 assert_eq!(
472 JoinTreeConstructor::construct(&qg, &hint).unwrap_err(),
473 JoinHintError::MissingBuildSide
474 );
475 }
476}