Skip to main content

linkrs_query/executor/streaming/subquery/
executor.rs

1//! Per-operator subquery execution facility.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use crate::executor::expression::evaluator::expression_evaluator::ExpressionEvaluator;
7use crate::executor::expression::ExpressionError;
8use crate::executor::streaming::context::BorrowedRowContext;
9use crate::executor::streaming::instance::QueryBindings;
10use crate::executor::streaming::plan::materializer::PhysicalPlanMaterializer;
11use crate::executor::streaming::runtime::ExecutionRuntime;
12use crate::executor::streaming::slot::SlotLayout;
13use linkrs_core::types::expr::SubqueryBody;
14use linkrs_core::Value;
15
16use super::runner::{GroupJoinSpec, SubqueryCache, SubqueryRunner, SubqueryRunnerSpec};
17
18/// Per-operator subquery execution facility.
19///
20/// Each Filter/Project/Assign operator instance holds exactly one executor
21/// with its own runner instances; nothing is shared across partitions.
22/// Execution is always synchronous and serial within the operator's own
23/// `next()` call stack, so the runners need no cross-thread locks.
24#[derive(Debug)]
25pub struct SubqueryExecutor {
26    /// Shared runtime used to re-materialize nested sub-plans (storage,
27    /// cancellation, parameters).
28    pub runtime: Arc<ExecutionRuntime>,
29    /// Bindings used to materialize nested sub-plans (parameter-free).
30    pub bindings: Arc<QueryBindings>,
31    /// Compiled runners keyed by stable `SubqueryBody.id`.
32    pub runners: HashMap<u64, SubqueryRunner>,
33}
34
35impl SubqueryExecutor {
36    /// Build a per-operator executor from the immutable runner specs of an
37    /// arena spec. Each call creates fresh runner state, so every operator
38    /// instance (and every parallel partition) is isolated.
39    pub fn from_specs(
40        runtime: Arc<ExecutionRuntime>,
41        bindings: Arc<QueryBindings>,
42        specs: &[SubqueryRunnerSpec],
43    ) -> Self {
44        let runners = specs
45            .iter()
46            .map(|spec| (spec.id, SubqueryRunner::from_spec(spec)))
47            .collect();
48        Self {
49            runtime,
50            bindings,
51            runners,
52        }
53    }
54
55    /// Whether this executor hosts any compiled subqueries.
56    pub fn is_empty(&self) -> bool {
57        self.runners.is_empty()
58    }
59
60    /// EXISTS: whether the subquery produces at least one row.
61    ///
62    /// Non-correlated: evaluated once on the first call and cached.
63    /// Correlated: re-executed per row with the current row as the frame.
64    /// Group-Join form: answered from the materialized key→aggregate table
65    /// (the key is present iff at least one right row matched).
66    pub fn execute_exists(
67        &self,
68        body: &SubqueryBody,
69        layout: Arc<SlotLayout>,
70        row: Vec<Value>,
71    ) -> Result<bool, ExpressionError> {
72        let runner = self.runner(body)?;
73        if let Some(spec) = &runner.group_join {
74            return self
75                .run_group_join_lookup(runner, spec, layout, row)
76                .map(|found| found.is_some());
77        }
78        if !runner.correlated {
79            let mut cache = runner.cache.lock();
80            if let Some(SubqueryCache::Exists(value)) = &*cache {
81                return Ok(*value);
82            }
83            let exists = self.run_exists(runner, layout, row)?;
84            *cache = Some(SubqueryCache::Exists(exists));
85            return Ok(exists);
86        }
87        self.run_exists(runner, layout, row)
88    }
89
90    /// IN: whether `value` occurs in the subquery result column.
91    ///
92    /// NULL never matches, mirroring the conjunctive `keys_match`
93    /// path. Non-correlated: the result set is collected once (NULLs removed)
94    /// into a `HashSet` and probed per row. Group-Join form: the lookup value
95    /// is compared against the aggregated result of the row's key.
96    pub fn execute_contains(
97        &self,
98        body: &SubqueryBody,
99        layout: Arc<SlotLayout>,
100        row: Vec<Value>,
101        value: &Value,
102    ) -> Result<bool, ExpressionError> {
103        if value.is_null() {
104            return Ok(false);
105        }
106        let runner = self.runner(body)?;
107        if let Some(spec) = &runner.group_join {
108            let found = self.run_group_join_lookup(runner, spec, layout, row)?;
109            return Ok(matches!(found, Some(v) if !v.is_null() && &v == value));
110        }
111        if !runner.correlated {
112            let mut cache = runner.cache.lock();
113            if let Some(SubqueryCache::Contains(set)) = &*cache {
114                return Ok(set.contains(value));
115            }
116            let set: HashSet<Value> = self
117                .run_values(runner, layout, row)?
118                .into_iter()
119                .filter(|v| !v.is_null())
120                .collect();
121            let found = set.contains(value);
122            *cache = Some(SubqueryCache::Contains(set));
123            return Ok(found);
124        }
125        self.run_contains(runner, layout, row, value)
126    }
127
128    /// Scalar subquery: the first result value, or NULL when the subquery
129    /// returns no rows.
130    ///
131    /// Non-correlated: evaluated once on the first call and cached.
132    /// Correlated: re-executed per row with the current row as the frame.
133    /// Group-Join form: answered from the materialized key→aggregate table;
134    /// a missing key yields NULL.
135    pub fn execute_scalar(
136        &self,
137        body: &SubqueryBody,
138        layout: Arc<SlotLayout>,
139        row: Vec<Value>,
140    ) -> Result<Value, ExpressionError> {
141        let runner = self.runner(body)?;
142        if let Some(spec) = &runner.group_join {
143            let found = self.run_group_join_lookup(runner, spec, layout, row)?;
144            return Ok(found.unwrap_or(Value::Null(linkrs_core::NullType::Null)));
145        }
146        if !runner.correlated {
147            let mut cache = runner.cache.lock();
148            if let Some(SubqueryCache::Scalar(value)) = &*cache {
149                return Ok(value.clone());
150            }
151            let value = self.run_scalar(runner, layout, row)?;
152            *cache = Some(SubqueryCache::Scalar(value.clone()));
153            return Ok(value);
154        }
155        self.run_scalar(runner, layout, row)
156    }
157
158    /// Run the subquery and return its first result value (NULL when empty).
159    fn run_scalar(
160        &self,
161        runner: &SubqueryRunner,
162        layout: Arc<SlotLayout>,
163        row: Vec<Value>,
164    ) -> Result<Value, ExpressionError> {
165        let values = self.run_values(runner, layout, row)?;
166        Ok(values
167            .into_iter()
168            .next()
169            .unwrap_or(Value::Null(linkrs_core::NullType::Null)))
170    }
171
172    fn runner(&self, body: &SubqueryBody) -> Result<&SubqueryRunner, ExpressionError> {
173        self.runners.get(&body.id).ok_or_else(|| {
174            ExpressionError::type_error(
175                "Subquery execution not supported in this context".to_string(),
176            )
177        })
178    }
179
180    /// Group-Join lookup: evaluate the outer-side hash keys against the
181    /// current row and probe the materialized key→aggregate table.
182    ///
183    /// A NULL key component never matches (same convention as the conjunctive
184    /// `keys_match` path); a missing key yields `None` (EXISTS → false,
185    /// IN → no match).
186    fn run_group_join_lookup(
187        &self,
188        runner: &SubqueryRunner,
189        spec: &GroupJoinSpec,
190        layout: Arc<SlotLayout>,
191        row: Vec<Value>,
192    ) -> Result<Option<Value>, ExpressionError> {
193        let mut ctx = BorrowedRowContext::new(&row, layout);
194        let mut key = Vec::with_capacity(spec.hash_keys.len());
195        for expr in &spec.hash_keys {
196            let value = ExpressionEvaluator::evaluate(expr, &mut ctx)?;
197            if value.is_null() {
198                return Ok(None);
199            }
200            key.push(value);
201        }
202        self.ensure_group_table(runner, spec)?;
203        let table = runner.group_table.lock();
204        Ok(table.as_ref().and_then(|t| t.get(&key).cloned()))
205    }
206
207    /// Build the Group-Join table once: materialize the sub-plan
208    /// (`Aggregate`-rooted build side), drain its rows and index them by the
209    /// leading group-key columns.
210    fn ensure_group_table(
211        &self,
212        runner: &SubqueryRunner,
213        spec: &GroupJoinSpec,
214    ) -> Result<(), ExpressionError> {
215        if runner.group_table.lock().is_some() {
216            return Ok(());
217        }
218        let (mut exec, _) = PhysicalPlanMaterializer::materialize(&runner.plan, &self.bindings)
219            .map_err(|e| {
220                ExpressionError::type_error(format!(
221                    "Group-Join subquery plan materialization failed: {}",
222                    e
223                ))
224            })?;
225        exec.set_chunk_size(self.bindings.chunk_size);
226        exec.set_runtime(Some(self.runtime.clone()));
227        exec.open()
228            .map_err(|e| ExpressionError::type_error(format!("Subquery open failed: {}", e)))?;
229
230        let mut table: HashMap<Vec<Value>, Value> = HashMap::new();
231        while let Some(mut chunk) = exec
232            .advance()
233            .map_err(|e| ExpressionError::type_error(format!("Subquery execution failed: {}", e)))?
234        {
235            chunk.normalize_for_opaque("Subquery");
236            for row in chunk.rows {
237                if row.len() <= spec.key_columns {
238                    continue;
239                }
240                let key: Vec<Value> = row[..spec.key_columns].to_vec();
241                // NULL group keys can never be matched by an outer row.
242                if key.iter().any(Value::is_null) {
243                    continue;
244                }
245                table.insert(key, row[spec.key_columns].clone());
246            }
247        }
248        *runner.group_table.lock() = Some(table);
249        Ok(())
250    }
251
252    /// Run the subquery and short-circuit on the first produced row.
253    fn run_exists(
254        &self,
255        runner: &SubqueryRunner,
256        layout: Arc<SlotLayout>,
257        row: Vec<Value>,
258    ) -> Result<bool, ExpressionError> {
259        runner.with_executor(&self.runtime, &self.bindings, layout, row, |exec| loop {
260            match exec.advance()? {
261                // Visible rows only: an attached selection may leave
262                // non-visible physical rows behind.
263                Some(chunk) => {
264                    if chunk.visible_count() > 0 {
265                        return Ok(true);
266                    }
267                }
268                None => return Ok(false),
269            }
270        })
271    }
272
273    /// Run the subquery and probe the produced values against `value`,
274    /// short-circuiting on the first match (NULLs never match).
275    fn run_contains(
276        &self,
277        runner: &SubqueryRunner,
278        layout: Arc<SlotLayout>,
279        row: Vec<Value>,
280        value: &Value,
281    ) -> Result<bool, ExpressionError> {
282        runner.with_executor(&self.runtime, &self.bindings, layout, row, |exec| loop {
283            match exec.advance()? {
284                Some(chunk) => {
285                    for chunk_row in chunk.visible_rows() {
286                        if let Some(candidate) = chunk_row.first() {
287                            if !candidate.is_null() && candidate == value {
288                                return Ok(true);
289                            }
290                        }
291                    }
292                }
293                None => return Ok(false),
294            }
295        })
296    }
297
298    /// Run the subquery and collect the first column of every produced row.
299    fn run_values(
300        &self,
301        runner: &SubqueryRunner,
302        layout: Arc<SlotLayout>,
303        row: Vec<Value>,
304    ) -> Result<Vec<Value>, ExpressionError> {
305        runner.with_executor(&self.runtime, &self.bindings, layout, row, |exec| {
306            let mut values = Vec::new();
307            while let Some(mut chunk) = exec.advance()? {
308                chunk.normalize_for_opaque("Subquery");
309                for chunk_row in chunk.rows {
310                    if let Some(value) = chunk_row.first() {
311                        values.push(value.clone());
312                    }
313                }
314            }
315            Ok(values)
316        })
317    }
318}