1use thiserror::Error;
10
11use crate::batch::{Batch, RowSchema};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct PhysicalOrder {
18 pub position: usize,
19 pub descending: bool,
20 pub nulls_first: Option<bool>,
21 pub nullable: bool,
22}
23
24#[derive(Debug, Error)]
27pub enum ExecError {
28 #[error("execution error: {0}")]
29 Other(String),
30 #[error("SQL error: {0}")]
31 SQL(#[from] uqa_sql::SQLError),
32}
33
34pub type ExecResult<T> = std::result::Result<T, ExecError>;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum PhysicalScanDirection {
39 Forward,
40 Backward,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum BackwardScanSupport {
46 Unsupported,
48 Materialize,
50 Native,
52}
53
54pub(crate) fn with_cleanup<T>(
59 primary: ExecResult<T>,
60 cleanup: ExecResult<()>,
61 cleanup_context: &str,
62) -> ExecResult<T> {
63 match (primary, cleanup) {
64 (Ok(value), Ok(())) => Ok(value),
65 (Ok(_), Err(cleanup_error)) => Err(cleanup_error),
66 (Err(primary_error), Ok(())) => Err(primary_error),
67 (Err(primary_error), Err(cleanup_error)) => Err(ExecError::Other(format!(
68 "{primary_error}; {cleanup_context}: {cleanup_error}"
69 ))),
70 }
71}
72
73pub trait PhysicalOperator: Send {
87 fn row_schema(&self) -> &RowSchema;
89
90 fn schema(&self) -> &[String] {
92 self.row_schema().columns()
93 }
94
95 fn estimated_cardinality(&self) -> Option<u64> {
99 None
100 }
101
102 fn output_ordering(&self) -> &[PhysicalOrder] {
104 &[]
105 }
106
107 fn consume_into_aggregate(
111 &mut self,
112 _executor: &mut dyn crate::relational::AggregateExecutor,
113 ) -> ExecResult<bool> {
114 Ok(false)
115 }
116
117 fn backward_scan_support(&self) -> BackwardScanSupport {
119 BackwardScanSupport::Unsupported
120 }
121
122 fn open(&mut self) -> ExecResult<()>;
123 fn next(&mut self) -> ExecResult<Option<Batch>>;
124 fn next_direction(&mut self, direction: PhysicalScanDirection) -> ExecResult<Option<Batch>> {
126 match direction {
127 PhysicalScanDirection::Forward => self.next(),
128 PhysicalScanDirection::Backward => Err(ExecError::Other(
129 "physical operator does not support backwards scanning".into(),
130 )),
131 }
132 }
133 fn rewind(&mut self) -> ExecResult<()> {
135 Err(ExecError::Other(
136 "physical operator does not support rewind".into(),
137 ))
138 }
139 fn close(&mut self) -> ExecResult<()>;
140}
141
142pub fn ordering_satisfies(actual: &[PhysicalOrder], required: &[PhysicalOrder]) -> bool {
144 actual.len() >= required.len()
145 && actual.iter().zip(required).all(|(actual, required)| {
146 actual.position == required.position
147 && actual.descending == required.descending
148 && (!actual.nullable
149 || actual.nulls_first == required.nulls_first
150 || required.nulls_first.is_none())
151 })
152}
153
154pub fn order_expression_position(
156 schema: &RowSchema,
157 expression: &crate::ScalarExpr,
158) -> Option<usize> {
159 match expression {
160 crate::ScalarExpr::Column(column) => schema.unqualified_position(column),
161 crate::ScalarExpr::Position(position) => (*position < schema.len()).then_some(*position),
162 crate::ScalarExpr::QualifiedColumn { qualifier, column } => {
163 schema.qualified_position(qualifier, column)
164 }
165 _ => None,
166 }
167}
168
169pub struct OperatorBatchCursor<'operator> {
173 operator: &'operator mut dyn PhysicalOperator,
174 finished: bool,
175}
176
177impl<'operator> OperatorBatchCursor<'operator> {
178 pub fn open(operator: &'operator mut dyn PhysicalOperator) -> ExecResult<Self> {
179 if let Err(open_error) = operator.open() {
180 return match operator.close() {
181 Ok(()) => Err(open_error),
182 Err(close_error) => Err(ExecError::Other(format!(
183 "{open_error}; operator close after open failure also failed: {close_error}"
184 ))),
185 };
186 }
187 Ok(Self {
188 operator,
189 finished: false,
190 })
191 }
192
193 fn finish(&mut self) -> ExecResult<()> {
194 if self.finished {
195 return Ok(());
196 }
197 self.finished = true;
198 self.operator.close()
199 }
200}
201
202impl Iterator for OperatorBatchCursor<'_> {
203 type Item = ExecResult<Batch>;
204
205 fn next(&mut self) -> Option<Self::Item> {
206 if self.finished {
207 return None;
208 }
209 match self.operator.next() {
210 Ok(Some(batch)) => Some(Ok(batch)),
211 Ok(None) => match self.finish() {
212 Ok(()) => None,
213 Err(error) => Some(Err(error)),
214 },
215 Err(next_error) => {
216 let close = self.finish();
217 Some(with_cleanup(
218 Err(next_error),
219 close,
220 "operator close after execution failure also failed",
221 ))
222 }
223 }
224 }
225}
226
227impl Drop for OperatorBatchCursor<'_> {
228 fn drop(&mut self) {
229 let _ = self.finish();
230 }
231}
232
233pub fn run_to_batches(op: &mut dyn PhysicalOperator) -> ExecResult<Vec<Batch>> {
237 OperatorBatchCursor::open(op)?.collect()
238}
239
240pub fn run_to_rows(
243 op: &mut dyn PhysicalOperator,
244) -> ExecResult<(Vec<String>, Vec<uqa_sql::ResultRow>)> {
245 let schema = op.schema().to_vec();
246 let mut rows: Vec<uqa_sql::ResultRow> = Vec::new();
247 for batch in OperatorBatchCursor::open(op)? {
248 let batch = batch?;
249 rows.extend(batch.into_result_rows());
250 }
251 Ok((schema, rows))
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 struct FailingOperator {
259 fail_open: bool,
260 fail_close: bool,
261 closed: bool,
262 }
263
264 impl PhysicalOperator for FailingOperator {
265 fn row_schema(&self) -> &RowSchema {
266 static SCHEMA: std::sync::OnceLock<RowSchema> = std::sync::OnceLock::new();
267 SCHEMA.get_or_init(RowSchema::default)
268 }
269
270 fn open(&mut self) -> ExecResult<()> {
271 if self.fail_open {
272 Err(ExecError::Other("open failed".into()))
273 } else {
274 Ok(())
275 }
276 }
277
278 fn next(&mut self) -> ExecResult<Option<Batch>> {
279 Err(ExecError::Other("next failed".into()))
280 }
281
282 fn close(&mut self) -> ExecResult<()> {
283 self.closed = true;
284 if self.fail_close {
285 Err(ExecError::Other("close failed".into()))
286 } else {
287 Ok(())
288 }
289 }
290 }
291
292 #[test]
293 fn runner_closes_after_open_and_next_failures() {
294 let mut open = FailingOperator {
295 fail_open: true,
296 fail_close: false,
297 closed: false,
298 };
299 assert!(run_to_batches(&mut open)
300 .unwrap_err()
301 .to_string()
302 .contains("open failed"));
303 assert!(open.closed);
304
305 let mut next = FailingOperator {
306 fail_open: false,
307 fail_close: false,
308 closed: false,
309 };
310 assert!(run_to_batches(&mut next)
311 .unwrap_err()
312 .to_string()
313 .contains("next failed"));
314 assert!(next.closed);
315 }
316
317 #[test]
318 fn runner_reports_execution_and_cleanup_failures() {
319 let mut operator = FailingOperator {
320 fail_open: false,
321 fail_close: true,
322 closed: false,
323 };
324 let error = run_to_batches(&mut operator).unwrap_err().to_string();
325 assert!(error.contains("next failed"), "{error}");
326 assert!(error.contains("close failed"), "{error}");
327 assert!(operator.closed);
328 }
329
330 #[test]
331 fn cleanup_combiner_preserves_both_errors() {
332 let error = with_cleanup::<()>(
333 Err(ExecError::Other("primary".into())),
334 Err(ExecError::Other("cleanup".into())),
335 "cleanup failed",
336 )
337 .unwrap_err()
338 .to_string();
339 assert!(error.contains("primary"), "{error}");
340 assert!(error.contains("cleanup"), "{error}");
341 }
342
343 #[test]
344 fn dropping_cursor_closes_an_unfinished_pipeline() {
345 let mut operator = FailingOperator {
346 fail_open: false,
347 fail_close: false,
348 closed: false,
349 };
350 {
351 let _cursor = OperatorBatchCursor::open(&mut operator).unwrap();
352 }
353 assert!(operator.closed);
354 }
355
356 #[test]
357 fn ordering_positions_keep_duplicate_structured_identities_distinct() {
358 let schema = RowSchema::with_identities(
359 vec!["id".into(), "id".into()],
360 vec![
361 crate::ColumnIdentity::qualified("left", "id"),
362 crate::ColumnIdentity::qualified("right", "id"),
363 ],
364 vec![None, None],
365 );
366 assert_eq!(
367 order_expression_position(&schema, &crate::ScalarExpr::qualified_column("left", "id")),
368 Some(0)
369 );
370 assert_eq!(
371 order_expression_position(&schema, &crate::ScalarExpr::qualified_column("right", "id")),
372 Some(1)
373 );
374 assert_eq!(
375 order_expression_position(&schema, &crate::ScalarExpr::Column("id".into())),
376 None
377 );
378 let actual = [PhysicalOrder {
379 position: 0,
380 descending: false,
381 nulls_first: None,
382 nullable: false,
383 }];
384 let required = [PhysicalOrder {
385 position: 1,
386 descending: false,
387 nulls_first: Some(false),
388 nullable: true,
389 }];
390 assert!(!ordering_satisfies(&actual, &required));
391 }
392}