Skip to main content

linkrs_query/planning/join_order/
subquery_graph.rs

1//! Bitset-encoded subquery graph for DP join enumeration.
2//!
3//! A `SubqueryGraph` selects a subset of the query graph nodes and rels with
4//! two `u64` selectors. All DP operations (neighbor lookup, subgraph union,
5//! connected-key extraction) work on these selectors without cloning the
6//! underlying pattern.
7//!
8//! Variables form a bipartite adjacency: a rel touches its two endpoint
9//! nodes, and a node touches its incident rels. Base scans seed the DP table
10//! with singletons (one node or one rel), and larger subgraphs grow by
11//! joining connected pairs, so every table entry is a connected subgraph.
12
13use std::sync::Arc;
14
15use super::query_graph::QueryGraph;
16
17/// Maximum number of query variables addressable by the bitset selectors.
18pub const MAX_NUM_QUERY_VARIABLES: usize = 64;
19
20/// Bitset-encoded subgraph of a [`QueryGraph`].
21#[derive(Debug, Clone)]
22pub struct SubqueryGraph {
23    query_graph: Arc<QueryGraph>,
24    /// Bit `i` set when query node `i` is in the subgraph.
25    pub query_nodes_selector: u64,
26    /// Bit `i` set when query rel `i` is in the subgraph.
27    pub query_rels_selector: u64,
28}
29
30impl SubqueryGraph {
31    pub fn new(query_graph: Arc<QueryGraph>) -> Self {
32        Self {
33            query_graph,
34            query_nodes_selector: 0,
35            query_rels_selector: 0,
36        }
37    }
38
39    pub fn with_selectors(
40        query_graph: Arc<QueryGraph>,
41        query_nodes_selector: u64,
42        query_rels_selector: u64,
43    ) -> Self {
44        Self {
45            query_graph,
46            query_nodes_selector,
47            query_rels_selector,
48        }
49    }
50
51    /// Subgraph holding exactly one query node.
52    pub fn single_node(query_graph: &Arc<QueryGraph>, node_pos: usize) -> Self {
53        assert!(node_pos < 64, "node position out of bitset range");
54        Self {
55            query_graph: Arc::clone(query_graph),
56            query_nodes_selector: 1u64 << node_pos,
57            query_rels_selector: 0,
58        }
59    }
60
61    /// Subgraph holding exactly one query rel (endpoints excluded).
62    ///
63    /// Rel-only singletons keep the DP lattice connected: larger subgraphs
64    /// grow by attaching endpoint nodes (extend) or by hash-joining on
65    /// shared nodes once both sides carry them.
66    pub fn single_rel(query_graph: &Arc<QueryGraph>, rel_pos: usize) -> Self {
67        assert!(rel_pos < 64, "rel position out of bitset range");
68        Self {
69            query_graph: Arc::clone(query_graph),
70            query_nodes_selector: 0,
71            query_rels_selector: 1u64 << rel_pos,
72        }
73    }
74
75    pub fn query_graph(&self) -> &Arc<QueryGraph> {
76        &self.query_graph
77    }
78
79    pub fn add_query_node(&mut self, node_pos: usize) {
80        assert!(node_pos < 64, "node position out of bitset range");
81        self.query_nodes_selector |= 1u64 << node_pos;
82    }
83
84    pub fn add_query_rel(&mut self, rel_pos: usize) {
85        assert!(rel_pos < 64, "rel position out of bitset range");
86        self.query_rels_selector |= 1u64 << rel_pos;
87    }
88
89    pub fn add_subquery_graph(&mut self, other: &SubqueryGraph) {
90        debug_assert!(Arc::ptr_eq(&self.query_graph, &other.query_graph));
91        self.query_nodes_selector |= other.query_nodes_selector;
92        self.query_rels_selector |= other.query_rels_selector;
93    }
94
95    pub fn contains_node(&self, node_pos: usize) -> bool {
96        node_pos < 64 && (self.query_nodes_selector >> node_pos) & 1 == 1
97    }
98
99    pub fn contains_rel(&self, rel_pos: usize) -> bool {
100        rel_pos < 64 && (self.query_rels_selector >> rel_pos) & 1 == 1
101    }
102
103    pub fn is_empty(&self) -> bool {
104        self.query_nodes_selector == 0 && self.query_rels_selector == 0
105    }
106
107    /// Number of selected nodes plus selected rels.
108    pub fn total_num_variables(&self) -> usize {
109        self.query_nodes_selector.count_ones() as usize
110            + self.query_rels_selector.count_ones() as usize
111    }
112
113    pub fn num_nodes(&self) -> usize {
114        self.query_nodes_selector.count_ones() as usize
115    }
116
117    pub fn num_rels(&self) -> usize {
118        self.query_rels_selector.count_ones() as usize
119    }
120
121    pub fn node_positions(&self) -> Vec<usize> {
122        (0..self.query_graph.num_nodes())
123            .filter(|p| self.contains_node(*p))
124            .collect()
125    }
126
127    pub fn rel_positions(&self) -> Vec<usize> {
128        (0..self.query_graph.num_rels())
129            .filter(|p| self.contains_rel(*p))
130            .collect()
131    }
132
133    /// Endpoint node positions of every rel contained in this subgraph.
134    fn contained_endpoint_positions(&self) -> Vec<usize> {
135        let mut out = Vec::new();
136        for rel_pos in self.rel_positions() {
137            if let Some((src, dst)) = self.query_graph.rel_endpoint_positions(rel_pos) {
138                if !out.contains(&src) {
139                    out.push(src);
140                }
141                if !out.contains(&dst) {
142                    out.push(dst);
143                }
144            }
145        }
146        out
147    }
148
149    /// Node positions adjacent to this subgraph: endpoints of contained rels
150    /// plus endpoints of frontier rels, excluding nodes already inside.
151    pub fn get_node_neighbor_positions(&self) -> Vec<usize> {
152        let mut out = Vec::new();
153        for node_pos in self
154            .contained_endpoint_positions()
155            .into_iter()
156            .chain(self.frontier_endpoint_positions())
157        {
158            if !self.contains_node(node_pos) && !out.contains(&node_pos) {
159                out.push(node_pos);
160            }
161        }
162        out.sort_unstable();
163        out
164    }
165
166    /// Endpoints of frontier rels (rels touching this subgraph from outside).
167    fn frontier_endpoint_positions(&self) -> Vec<usize> {
168        let mut out = Vec::new();
169        for rel_pos in self.get_rel_neighbor_positions() {
170            if let Some((src, dst)) = self.query_graph.rel_endpoint_positions(rel_pos) {
171                if !self.contains_node(src) && !out.contains(&src) {
172                    out.push(src);
173                }
174                if !self.contains_node(dst) && !out.contains(&dst) {
175                    out.push(dst);
176                }
177            }
178        }
179        out
180    }
181
182    /// Rel positions adjacent to this subgraph: outside rels sharing at
183    /// least one endpoint node with the subgraph (directly or through a
184    /// contained rel).
185    pub fn get_rel_neighbor_positions(&self) -> Vec<usize> {
186        let mut out = Vec::new();
187        for rel_pos in 0..self.query_graph.num_rels() {
188            if self.contains_rel(rel_pos) {
189                continue;
190            }
191            if self.touches_subgraph(rel_pos) {
192                out.push(rel_pos);
193            }
194        }
195        out.sort_unstable();
196        out
197    }
198
199    fn touches_subgraph(&self, rel_pos: usize) -> bool {
200        let Some((src, dst)) = self.query_graph.rel_endpoint_positions(rel_pos) else {
201            return false;
202        };
203        if self.contains_node(src) || self.contains_node(dst) {
204            return true;
205        }
206        let contained = self.contained_endpoint_positions();
207        contained.contains(&src) || contained.contains(&dst)
208    }
209
210    /// Enumerate connected neighbor subgraphs of exactly `target_size`
211    /// fresh variables (disjoint from this subgraph) that touch it.
212    ///
213    /// Seeds grow one variable at a time from the bipartite frontier of
214    /// (self union seed): outside rels touching the combined node set and
215    /// outside endpoint nodes of combined rels.
216    pub fn get_neighbor_subgraphs(&self, target_size: usize) -> Vec<SubqueryGraph> {
217        if target_size == 0 || target_size > MAX_NUM_QUERY_VARIABLES {
218            return Vec::new();
219        }
220        let mut out = Vec::new();
221        let mut seeds = self.frontier_singletons();
222        while let Some(seed) = seeds.pop() {
223            let size = seed.total_num_variables();
224            if size == target_size {
225                if !out.iter().any(|s: &SubqueryGraph| s.key() == seed.key()) {
226                    out.push(seed);
227                }
228            } else if size < target_size {
229                seeds.extend(self.grown_seeds(&seed));
230            }
231        }
232        out
233    }
234
235    /// Single-variable frontier subgraphs disjoint from this subgraph.
236    fn frontier_singletons(&self) -> Vec<SubqueryGraph> {
237        let mut out = Vec::new();
238        for rel_pos in self.get_rel_neighbor_positions() {
239            let mut seed = SubqueryGraph::new(Arc::clone(&self.query_graph));
240            seed.add_query_rel(rel_pos);
241            out.push(seed);
242        }
243        for node_pos in self.get_node_neighbor_positions() {
244            let mut seed = SubqueryGraph::new(Arc::clone(&self.query_graph));
245            seed.add_query_node(node_pos);
246            if !out.iter().any(|s: &SubqueryGraph| s.key() == seed.key()) {
247                out.push(seed);
248            }
249        }
250        out
251    }
252
253    /// One-variable extensions of `seed` from the frontier of
254    /// (self union seed), excluding variables already in either side.
255    fn grown_seeds(&self, seed: &SubqueryGraph) -> Vec<SubqueryGraph> {
256        let mut combined = self.clone();
257        combined.add_subquery_graph(seed);
258        let mut out = Vec::new();
259        for rel_pos in combined.get_rel_neighbor_positions() {
260            if seed.contains_rel(rel_pos) || self.contains_rel(rel_pos) {
261                continue;
262            }
263            let mut next = seed.clone();
264            next.add_query_rel(rel_pos);
265            out.push(next);
266        }
267        for node_pos in combined.get_node_neighbor_positions() {
268            if seed.contains_node(node_pos) || self.contains_node(node_pos) {
269                continue;
270            }
271            let mut next = seed.clone();
272            next.add_query_node(node_pos);
273            if !out.iter().any(|s: &SubqueryGraph| s.key() == next.key()) {
274                out.push(next);
275            }
276        }
277        out
278    }
279
280    /// Node positions present in both subgraphs (binary join keys).
281    pub fn get_connected_node_positions(&self, other: &SubqueryGraph) -> Vec<usize> {
282        let mut out = Vec::new();
283        for pos in 0..self.query_graph.num_nodes() {
284            if self.contains_node(pos) && other.contains_node(pos) {
285                out.push(pos);
286            }
287        }
288        out.sort_unstable();
289        out
290    }
291
292    /// Whether the two subgraphs touch through at least one shared node.
293    pub fn is_connected(&self, other: &SubqueryGraph) -> bool {
294        !self.get_connected_node_positions(other).is_empty()
295    }
296
297    /// Whether a rel on either side has an endpoint node on the other side.
298    ///
299    /// Extendable pairs without shared nodes are planned as index-nested-loop
300    /// extends rather than hash joins.
301    pub fn is_extendable_with(&self, other: &SubqueryGraph) -> bool {
302        for rel_pos in self.rel_positions() {
303            if let Some((src, dst)) = self.query_graph.rel_endpoint_positions(rel_pos) {
304                if other.contains_node(src) || other.contains_node(dst) {
305                    return true;
306                }
307            }
308        }
309        for rel_pos in other.rel_positions() {
310            if let Some((src, dst)) = self.query_graph.rel_endpoint_positions(rel_pos) {
311                if self.contains_node(src) || self.contains_node(dst) {
312                    return true;
313                }
314            }
315        }
316        false
317    }
318
319    /// Whether the pair can be joined at all: shared join keys or an
320    /// extend step in either direction.
321    pub fn is_joinable_with(&self, other: &SubqueryGraph) -> bool {
322        self.is_connected(other) || self.is_extendable_with(other)
323    }
324
325    /// Selector key used by the DP table.
326    pub fn key(&self) -> (u64, u64) {
327        (self.query_nodes_selector, self.query_rels_selector)
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::planning::join_order::query_graph::{QueryNode, QueryRel};
335
336    fn triangle() -> Arc<QueryGraph> {
337        let mut qg = QueryGraph::new();
338        qg.add_node(QueryNode::new("a", "a"));
339        qg.add_node(QueryNode::new("b", "b"));
340        qg.add_node(QueryNode::new("c", "c"));
341        qg.add_rel(QueryRel::new("e1", "e1", "a", "b"));
342        qg.add_rel(QueryRel::new("e2", "e2", "a", "c"));
343        qg.add_rel(QueryRel::new("e3", "e3", "b", "c"));
344        Arc::new(qg)
345    }
346
347    #[test]
348    fn add_and_count() {
349        let qg = triangle();
350        let mut sg = SubqueryGraph::new(Arc::clone(&qg));
351        assert!(sg.is_empty());
352        sg.add_query_node(0);
353        sg.add_query_rel(0);
354        assert_eq!(sg.total_num_variables(), 2);
355        assert!(sg.contains_node(0));
356        assert!(!sg.contains_node(1));
357    }
358
359    #[test]
360    fn single_rel_is_rel_only() {
361        let qg = triangle();
362        let sg = SubqueryGraph::single_rel(&qg, 0);
363        assert!(sg.contains_rel(0));
364        assert_eq!(sg.total_num_variables(), 1);
365        assert_eq!(sg.num_nodes(), 0);
366    }
367
368    #[test]
369    fn rel_only_neighbors_are_endpoints() {
370        let qg = triangle();
371        let sg = SubqueryGraph::single_rel(&qg, 0);
372        // Direct endpoints plus nodes behind frontier rels.
373        assert_eq!(sg.get_node_neighbor_positions(), vec![0, 1, 2]);
374        let mut rel_nbrs = sg.get_rel_neighbor_positions();
375        rel_nbrs.sort_unstable();
376        assert_eq!(rel_nbrs, vec![1, 2]);
377    }
378
379    #[test]
380    fn rel_neighbors() {
381        let qg = triangle();
382        let mut sg = SubqueryGraph::new(Arc::clone(&qg));
383        sg.add_query_node(0);
384        sg.add_query_node(1);
385        sg.add_query_rel(0);
386        let mut nbrs = sg.get_rel_neighbor_positions();
387        nbrs.sort_unstable();
388        assert_eq!(nbrs, vec![1, 2]);
389    }
390
391    #[test]
392    fn connected_nodes_are_join_keys() {
393        let qg = triangle();
394        let mut left = SubqueryGraph::single_rel(&qg, 0);
395        left.add_query_node(0);
396        let mut right = SubqueryGraph::single_rel(&qg, 1);
397        right.add_query_node(0);
398        assert_eq!(left.get_connected_node_positions(&right), vec![0]);
399        assert!(left.is_connected(&right));
400    }
401
402    #[test]
403    fn rel_node_pair_is_extendable_but_not_connected() {
404        let qg = triangle();
405        let rel = SubqueryGraph::single_rel(&qg, 0);
406        let node = SubqueryGraph::single_node(&qg, 0);
407        assert!(!rel.is_connected(&node));
408        assert!(rel.is_extendable_with(&node));
409        assert!(rel.is_joinable_with(&node));
410        let far = SubqueryGraph::single_node(&qg, 2);
411        assert!(!rel.is_joinable_with(&far));
412    }
413
414    #[test]
415    fn neighbor_subgraphs_size_one() {
416        let qg = triangle();
417        let left = SubqueryGraph::single_rel(&qg, 0);
418        let nbrs = left.get_neighbor_subgraphs(1);
419        assert!(!nbrs.is_empty());
420        for n in &nbrs {
421            assert_eq!(n.total_num_variables(), 1);
422        }
423        // Endpoints a and b must be reachable as size-1 neighbors.
424        assert!(nbrs.iter().any(|n| n.contains_node(0)));
425        assert!(nbrs.iter().any(|n| n.contains_node(1)));
426    }
427
428    #[test]
429    fn neighbor_subgraphs_grow_to_target_size() {
430        let qg = triangle();
431        let mut sg = SubqueryGraph::new(Arc::clone(&qg));
432        sg.add_query_node(0);
433        let nbrs = sg.get_neighbor_subgraphs(2);
434        assert!(!nbrs.is_empty());
435        for n in &nbrs {
436            assert_eq!(n.total_num_variables(), 2);
437        }
438    }
439}