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
36pub(crate) fn with_cleanup<T>(
41 primary: ExecResult<T>,
42 cleanup: ExecResult<()>,
43 cleanup_context: &str,
44) -> ExecResult<T> {
45 match (primary, cleanup) {
46 (Ok(value), Ok(())) => Ok(value),
47 (Ok(_), Err(cleanup_error)) => Err(cleanup_error),
48 (Err(primary_error), Ok(())) => Err(primary_error),
49 (Err(primary_error), Err(cleanup_error)) => Err(ExecError::Other(format!(
50 "{primary_error}; {cleanup_context}: {cleanup_error}"
51 ))),
52 }
53}
54
55pub trait PhysicalOperator: Send {
69 fn row_schema(&self) -> &RowSchema;
71
72 fn schema(&self) -> &[String] {
74 self.row_schema().columns()
75 }
76
77 fn estimated_cardinality(&self) -> Option<u64> {
81 None
82 }
83
84 fn output_ordering(&self) -> &[PhysicalOrder] {
86 &[]
87 }
88
89 fn consume_into_aggregate(
93 &mut self,
94 _executor: &mut dyn crate::relational::AggregateExecutor,
95 ) -> ExecResult<bool> {
96 Ok(false)
97 }
98
99 fn open(&mut self) -> ExecResult<()>;
100 fn next(&mut self) -> ExecResult<Option<Batch>>;
101 fn close(&mut self) -> ExecResult<()>;
102}
103
104pub fn ordering_satisfies(actual: &[PhysicalOrder], required: &[PhysicalOrder]) -> bool {
106 actual.len() >= required.len()
107 && actual.iter().zip(required).all(|(actual, required)| {
108 actual.position == required.position
109 && actual.descending == required.descending
110 && (!actual.nullable
111 || actual.nulls_first == required.nulls_first
112 || required.nulls_first.is_none())
113 })
114}
115
116pub fn order_expression_position(
118 schema: &RowSchema,
119 expression: &crate::ScalarExpr,
120) -> Option<usize> {
121 match expression {
122 crate::ScalarExpr::Column(column) => schema.unqualified_position(column),
123 crate::ScalarExpr::Position(position) => (*position < schema.len()).then_some(*position),
124 crate::ScalarExpr::QualifiedColumn { qualifier, column } => {
125 schema.qualified_position(qualifier, column)
126 }
127 _ => None,
128 }
129}
130
131pub struct OperatorBatchCursor<'operator> {
135 operator: &'operator mut dyn PhysicalOperator,
136 finished: bool,
137}
138
139impl<'operator> OperatorBatchCursor<'operator> {
140 pub fn open(operator: &'operator mut dyn PhysicalOperator) -> ExecResult<Self> {
141 if let Err(open_error) = operator.open() {
142 return match operator.close() {
143 Ok(()) => Err(open_error),
144 Err(close_error) => Err(ExecError::Other(format!(
145 "{open_error}; operator close after open failure also failed: {close_error}"
146 ))),
147 };
148 }
149 Ok(Self {
150 operator,
151 finished: false,
152 })
153 }
154
155 fn finish(&mut self) -> ExecResult<()> {
156 if self.finished {
157 return Ok(());
158 }
159 self.finished = true;
160 self.operator.close()
161 }
162}
163
164impl Iterator for OperatorBatchCursor<'_> {
165 type Item = ExecResult<Batch>;
166
167 fn next(&mut self) -> Option<Self::Item> {
168 if self.finished {
169 return None;
170 }
171 match self.operator.next() {
172 Ok(Some(batch)) => Some(Ok(batch)),
173 Ok(None) => match self.finish() {
174 Ok(()) => None,
175 Err(error) => Some(Err(error)),
176 },
177 Err(next_error) => {
178 let close = self.finish();
179 Some(with_cleanup(
180 Err(next_error),
181 close,
182 "operator close after execution failure also failed",
183 ))
184 }
185 }
186 }
187}
188
189impl Drop for OperatorBatchCursor<'_> {
190 fn drop(&mut self) {
191 let _ = self.finish();
192 }
193}
194
195pub fn run_to_batches(op: &mut dyn PhysicalOperator) -> ExecResult<Vec<Batch>> {
199 OperatorBatchCursor::open(op)?.collect()
200}
201
202pub fn run_to_rows(
205 op: &mut dyn PhysicalOperator,
206) -> ExecResult<(Vec<String>, Vec<uqa_sql::ResultRow>)> {
207 let schema = op.schema().to_vec();
208 let mut rows: Vec<uqa_sql::ResultRow> = Vec::new();
209 for batch in OperatorBatchCursor::open(op)? {
210 let batch = batch?;
211 rows.extend(batch.into_result_rows());
212 }
213 Ok((schema, rows))
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 struct FailingOperator {
221 fail_open: bool,
222 fail_close: bool,
223 closed: bool,
224 }
225
226 impl PhysicalOperator for FailingOperator {
227 fn row_schema(&self) -> &RowSchema {
228 static SCHEMA: std::sync::OnceLock<RowSchema> = std::sync::OnceLock::new();
229 SCHEMA.get_or_init(RowSchema::default)
230 }
231
232 fn open(&mut self) -> ExecResult<()> {
233 if self.fail_open {
234 Err(ExecError::Other("open failed".into()))
235 } else {
236 Ok(())
237 }
238 }
239
240 fn next(&mut self) -> ExecResult<Option<Batch>> {
241 Err(ExecError::Other("next failed".into()))
242 }
243
244 fn close(&mut self) -> ExecResult<()> {
245 self.closed = true;
246 if self.fail_close {
247 Err(ExecError::Other("close failed".into()))
248 } else {
249 Ok(())
250 }
251 }
252 }
253
254 #[test]
255 fn runner_closes_after_open_and_next_failures() {
256 let mut open = FailingOperator {
257 fail_open: true,
258 fail_close: false,
259 closed: false,
260 };
261 assert!(run_to_batches(&mut open)
262 .unwrap_err()
263 .to_string()
264 .contains("open failed"));
265 assert!(open.closed);
266
267 let mut next = FailingOperator {
268 fail_open: false,
269 fail_close: false,
270 closed: false,
271 };
272 assert!(run_to_batches(&mut next)
273 .unwrap_err()
274 .to_string()
275 .contains("next failed"));
276 assert!(next.closed);
277 }
278
279 #[test]
280 fn runner_reports_execution_and_cleanup_failures() {
281 let mut operator = FailingOperator {
282 fail_open: false,
283 fail_close: true,
284 closed: false,
285 };
286 let error = run_to_batches(&mut operator).unwrap_err().to_string();
287 assert!(error.contains("next failed"), "{error}");
288 assert!(error.contains("close failed"), "{error}");
289 assert!(operator.closed);
290 }
291
292 #[test]
293 fn cleanup_combiner_preserves_both_errors() {
294 let error = with_cleanup::<()>(
295 Err(ExecError::Other("primary".into())),
296 Err(ExecError::Other("cleanup".into())),
297 "cleanup failed",
298 )
299 .unwrap_err()
300 .to_string();
301 assert!(error.contains("primary"), "{error}");
302 assert!(error.contains("cleanup"), "{error}");
303 }
304
305 #[test]
306 fn dropping_cursor_closes_an_unfinished_pipeline() {
307 let mut operator = FailingOperator {
308 fail_open: false,
309 fail_close: false,
310 closed: false,
311 };
312 {
313 let _cursor = OperatorBatchCursor::open(&mut operator).unwrap();
314 }
315 assert!(operator.closed);
316 }
317
318 #[test]
319 fn ordering_positions_keep_duplicate_structured_identities_distinct() {
320 let schema = RowSchema::with_identities(
321 vec!["id".into(), "id".into()],
322 vec![
323 crate::ColumnIdentity::qualified("left", "id"),
324 crate::ColumnIdentity::qualified("right", "id"),
325 ],
326 vec![None, None],
327 );
328 assert_eq!(
329 order_expression_position(&schema, &crate::ScalarExpr::qualified_column("left", "id")),
330 Some(0)
331 );
332 assert_eq!(
333 order_expression_position(&schema, &crate::ScalarExpr::qualified_column("right", "id")),
334 Some(1)
335 );
336 assert_eq!(
337 order_expression_position(&schema, &crate::ScalarExpr::Column("id".into())),
338 None
339 );
340 let actual = [PhysicalOrder {
341 position: 0,
342 descending: false,
343 nulls_first: None,
344 nullable: false,
345 }];
346 let required = [PhysicalOrder {
347 position: 1,
348 descending: false,
349 nulls_first: Some(false),
350 nullable: true,
351 }];
352 assert!(!ordering_satisfies(&actual, &required));
353 }
354}