1use std::sync::Arc;
17
18use arrow::array::{Array, ArrayRef, BooleanArray, RecordBatch};
19use arrow::compute::{concat_batches, filter_record_batch};
20use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
21use datafusion::common::DFSchema;
22use datafusion::execution::context::ExecutionProps;
23use datafusion::physical_expr::{create_physical_expr, PhysicalExpr};
24use fv_plan::inline::Step;
25use fv_value_datafusion::{data_type_for, Kind, Options, Translator};
26
27#[derive(Debug, Clone)]
29pub struct NotCompilable(pub String);
30
31pub use fv_streams_types::decode::META_PREFIX;
34
35enum Compiled {
36 Select {
38 columns: Vec<(String, Option<usize>)>,
39 },
40 Rename {
41 mapping: Vec<(String, String)>,
42 },
43 Drop {
44 columns: Vec<String>,
45 },
46 Filter {
47 expr: Arc<dyn PhysicalExpr>,
48 },
49 Apply {
50 column: String,
51 expr: Arc<dyn PhysicalExpr>,
52 data_type: DataType,
53 },
54}
55
56pub struct BatchSteps {
58 steps: Vec<Step>,
59 compiled: Option<(SchemaRef, Vec<Compiled>)>,
60 pub dropped: u64,
62 pub exact: bool,
64 reported: bool,
68}
69
70impl BatchSteps {
71 pub fn new(steps: Vec<Step>) -> Self {
72 BatchSteps {
73 steps,
74 compiled: None,
75 dropped: 0,
76 exact: true,
77 reported: false,
78 }
79 }
80
81 pub fn is_empty(&self) -> bool {
82 self.steps.is_empty()
83 }
84
85 pub fn apply(&mut self, batch: &RecordBatch) -> Result<RecordBatch, NotCompilable> {
88 if self.steps.is_empty() {
89 return Ok(batch.clone());
90 }
91 let schema = batch.schema();
92 let needs_compile = match &self.compiled {
93 Some((s, _)) => s.as_ref() != schema.as_ref(),
94 None => true,
95 };
96 if needs_compile {
97 let (plan, exact, per_row) = compile(&self.steps, &schema)?;
98 self.exact = exact;
99 if !per_row.is_empty() && !self.reported {
100 self.reported = true;
101 eprintln!(
102 "steps: {} expression(s) run per row, not vectorised (the translator has no exact form for them): {}",
103 per_row.len(),
104 per_row.join("; ")
105 );
106 }
107 self.compiled = Some((Arc::clone(&schema), plan));
108 }
109 let plan = &self.compiled.as_ref().expect("compiled above").1;
110 match apply_plan(plan, batch) {
111 Ok(b) => Ok(b),
112 Err(_) => {
113 let mut good: Vec<RecordBatch> = Vec::new();
115 let mut out_schema: Option<SchemaRef> = None;
116 for i in 0..batch.num_rows() {
117 match apply_plan(plan, &batch.slice(i, 1)) {
118 Ok(b) => {
119 out_schema.get_or_insert_with(|| b.schema());
120 good.push(b);
121 }
122 Err(_) => self.dropped += 1,
123 }
124 }
125 match out_schema {
126 Some(s) => concat_batches(&s, &good).map_err(|e| NotCompilable(e.to_string())),
127 None => apply_plan(plan, &batch.slice(0, 0)).map_err(|e| NotCompilable(e.to_string())),
129 }
130 }
131 }
132 }
133}
134
135fn compile(steps: &[Step], input: &SchemaRef) -> Result<(Vec<Compiled>, bool, Vec<String>), NotCompilable> {
136 let mut schema = Arc::clone(input);
137 let mut plan = Vec::with_capacity(steps.len());
138 let mut exact = true;
139 let mut per_row: Vec<String> = Vec::new();
140 for step in steps {
141 let c = match step {
142 Step::Select { columns } => {
143 let cols: Vec<(String, Option<usize>)> =
144 columns.iter().map(|c| (c.clone(), schema.index_of(c).ok())).collect();
145 let fields: Vec<Field> = cols
146 .iter()
147 .map(|(name, idx)| match idx {
148 Some(i) => schema.field(*i).clone().with_name(name),
149 None => Field::new(name, DataType::Null, true),
150 })
151 .collect();
152 schema = Arc::new(Schema::new(fields));
153 Compiled::Select { columns: cols }
154 }
155 Step::Rename { mapping } => {
156 let fields: Vec<Field> = schema
157 .fields()
158 .iter()
159 .map(|f| {
160 let name = mapping
161 .iter()
162 .find(|(from, _)| from == f.name())
163 .map(|(_, to)| to.as_str())
164 .unwrap_or(f.name());
165 f.as_ref().clone().with_name(name)
166 })
167 .collect();
168 schema = Arc::new(Schema::new(fields));
169 Compiled::Rename {
170 mapping: mapping.clone(),
171 }
172 }
173 Step::Drop { columns } => {
174 let fields: Vec<Field> = schema
175 .fields()
176 .iter()
177 .filter(|f| !columns.contains(f.name()))
178 .map(|f| f.as_ref().clone())
179 .collect();
180 schema = Arc::new(Schema::new(fields));
181 Compiled::Drop {
182 columns: columns.clone(),
183 }
184 }
185 Step::Filter { expression } => {
186 let ast = fv_value::compile(expression).map_err(|e| NotCompilable(format!("filter: {e}")))?;
187 let t = Translator::new(Arc::clone(&schema))
188 .with_options(Options { assume_no_nan: true })
189 .translate_predicate_or_fallback(&ast)
190 .map_err(|e| NotCompilable(format!("filter '{expression}': {e}")))?;
191 exact &= t.exact;
192 if !t.exact {
193 per_row.push(format!("filter `{expression}`"));
194 }
195 Compiled::Filter {
196 expr: physical(&t.expr, &schema)?,
197 }
198 }
199 Step::ApplyExpression { column, expression } => {
200 let ast = fv_value::compile(expression).map_err(|e| NotCompilable(format!("applyExpression: {e}")))?;
201 let t = Translator::new(Arc::clone(&schema))
202 .with_options(Options { assume_no_nan: true })
203 .translate_or_fallback(&ast)
204 .map_err(|e| NotCompilable(format!("applyExpression '{expression}': {e}")))?;
205 exact &= t.exact;
206 if !t.exact {
207 per_row.push(format!("{column} = `{expression}`"));
208 }
209 let data_type = data_type_for(t.kind);
210 let expr = physical(&t.expr, &schema)?;
211 schema = Arc::new(schema_with(&schema, column, &data_type));
212 Compiled::Apply {
213 column: column.clone(),
214 expr,
215 data_type,
216 }
217 }
218 };
219 plan.push(c);
220 }
221 Ok((plan, exact, per_row))
222}
223
224fn physical(expr: &datafusion::logical_expr::Expr, schema: &SchemaRef) -> Result<Arc<dyn PhysicalExpr>, NotCompilable> {
225 let df_schema = DFSchema::try_from(Arc::clone(schema)).map_err(|e| NotCompilable(e.to_string()))?;
226 create_physical_expr(expr, &df_schema, &ExecutionProps::new()).map_err(|e| NotCompilable(e.to_string()))
227}
228
229fn schema_with(schema: &Schema, column: &str, data_type: &DataType) -> Schema {
231 let mut fields: Vec<Field> = schema.fields().iter().map(|f| f.as_ref().clone()).collect();
232 match fields.iter_mut().find(|f| f.name() == column) {
233 Some(f) => *f = Field::new(column, data_type.clone(), true),
234 None => fields.push(Field::new(column, data_type.clone(), true)),
235 }
236 Schema::new(fields)
237}
238
239fn apply_plan(plan: &[Compiled], batch: &RecordBatch) -> Result<RecordBatch, arrow::error::ArrowError> {
240 let mut b = batch.clone();
241 for step in plan {
242 b = match step {
243 Compiled::Select { columns } => {
244 let n = b.num_rows();
245 let (mut fields, mut arrays): (Vec<Field>, Vec<ArrayRef>) = columns
246 .iter()
247 .map(|(name, idx)| match idx {
248 Some(i) => (b.schema().field(*i).clone().with_name(name), Arc::clone(b.column(*i))),
249 None => (
250 Field::new(name, DataType::Null, true),
251 arrow::array::new_null_array(&DataType::Null, n),
252 ),
253 })
254 .unzip();
255 for (i, f) in b.schema().fields().iter().enumerate() {
257 if f.name().starts_with(META_PREFIX) {
258 fields.push(f.as_ref().clone());
259 arrays.push(Arc::clone(b.column(i)));
260 }
261 }
262 RecordBatch::try_new(Arc::new(Schema::new(fields)), arrays)?
263 }
264 Compiled::Rename { mapping } => {
265 let fields: Vec<Field> = b
266 .schema()
267 .fields()
268 .iter()
269 .map(|f| {
270 let name = mapping
271 .iter()
272 .find(|(from, _)| from == f.name())
273 .map(|(_, to)| to.as_str())
274 .unwrap_or(f.name());
275 f.as_ref().clone().with_name(name)
276 })
277 .collect();
278 RecordBatch::try_new(Arc::new(Schema::new(fields)), b.columns().to_vec())?
279 }
280 Compiled::Drop { columns } => {
281 let keep: Vec<usize> = b
282 .schema()
283 .fields()
284 .iter()
285 .enumerate()
286 .filter(|(_, f)| !columns.contains(f.name()))
287 .map(|(i, _)| i)
288 .collect();
289 b.project(&keep)?
290 }
291 Compiled::Filter { expr } => {
292 let mask = expr
293 .evaluate(&b)
294 .and_then(|v| v.into_array(b.num_rows()))
295 .map_err(|e| arrow::error::ArrowError::ComputeError(e.to_string()))?;
296 let mask = mask
297 .as_any()
298 .downcast_ref::<BooleanArray>()
299 .ok_or_else(|| arrow::error::ArrowError::ComputeError("filter did not yield booleans".into()))?;
300 filter_record_batch(&b, mask)?
302 }
303 Compiled::Apply {
304 column,
305 expr,
306 data_type,
307 } => {
308 let value = expr
309 .evaluate(&b)
310 .and_then(|v| v.into_array(b.num_rows()))
311 .map_err(|e| arrow::error::ArrowError::ComputeError(e.to_string()))?;
312 let value = if value.data_type() == data_type {
313 value
314 } else {
315 arrow::compute::cast(&value, data_type)?
316 };
317 let schema = Arc::new(schema_with(&b.schema(), column, data_type));
318 let mut arrays: Vec<ArrayRef> = b.columns().to_vec();
319 match b.schema().index_of(column) {
320 Ok(i) => arrays[i] = value,
321 Err(_) => arrays.push(value),
322 }
323 RecordBatch::try_new(schema, arrays)?
324 }
325 };
326 }
327 Ok(b)
328}
329
330pub fn kind_type(kind: Kind) -> DataType {
332 data_type_for(kind)
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use arrow::array::{Float64Array, StringArray};
339
340 fn batch() -> RecordBatch {
341 let schema = Arc::new(Schema::new(vec![
342 Field::new("type", DataType::Utf8, true),
343 Field::new("price", DataType::Float64, true),
344 Field::new("auction", DataType::Float64, true),
345 ]));
346 let ty: StringArray = vec!["bid", "auction", "bid"].into();
347 let price: Float64Array = vec![Some(10.0), None, Some(0.0)].into();
348 let auction: Float64Array = vec![1.0, 2.0, 3.0].into();
349 RecordBatch::try_new(schema, vec![Arc::new(ty), Arc::new(price), Arc::new(auction)]).unwrap()
350 }
351
352 fn col_f64(b: &RecordBatch, name: &str) -> Vec<Option<f64>> {
353 let a = b.column_by_name(name).unwrap();
354 let a = a.as_any().downcast_ref::<Float64Array>().unwrap();
355 (0..a.len())
356 .map(|i| if a.is_null(i) { None } else { Some(a.value(i)) })
357 .collect()
358 }
359
360 #[test]
361 fn filter_keeps_only_true_rows_and_drops_null_comparisons() {
362 let mut s = BatchSteps::new(vec![Step::Filter {
363 expression: "price > 5".into(),
364 }]);
365 let out = s.apply(&batch()).unwrap();
366 assert_eq!(out.num_rows(), 1);
367 assert_eq!(col_f64(&out, "auction"), vec![Some(1.0)]);
368 assert!(s.exact, "a plain comparison translates exactly");
369 }
370
371 #[test]
372 fn apply_adds_or_overwrites_a_column_and_the_chain_sees_it() {
373 let mut s = BatchSteps::new(vec![
374 Step::ApplyExpression {
375 column: "eur".into(),
376 expression: "price * 2".into(),
377 },
378 Step::Filter {
379 expression: "eur >= 20".into(),
380 },
381 Step::ApplyExpression {
382 column: "auction".into(),
383 expression: "auction + 100".into(),
384 },
385 ]);
386 let out = s.apply(&batch()).unwrap();
387 assert_eq!(out.num_rows(), 1);
388 assert_eq!(col_f64(&out, "eur"), vec![Some(20.0)]);
389 assert_eq!(col_f64(&out, "auction"), vec![Some(101.0)]);
390 assert_eq!(out.schema().fields().len(), 4);
391 }
392
393 #[test]
394 fn select_rename_drop_shape_the_schema_in_order() {
395 let mut s = BatchSteps::new(vec![
396 Step::Rename {
397 mapping: vec![("auction".into(), "a".into())],
398 },
399 Step::Select {
400 columns: vec!["a".into(), "missing".into(), "price".into()],
401 },
402 Step::Drop {
403 columns: vec!["price".into()],
404 },
405 ]);
406 let out = s.apply(&batch()).unwrap();
407 let names: Vec<String> = out.schema().fields().iter().map(|f| f.name().clone()).collect();
408 assert_eq!(names, vec!["a", "missing"]);
409 assert_eq!(out.column(1).logical_null_count(), 3, "a missing column lands null");
410 assert_eq!(out.column(1).data_type(), &DataType::Null);
411 assert_eq!(col_f64(&out, "a"), vec![Some(1.0), Some(2.0), Some(3.0)]);
412 }
413
414 #[test]
415 fn select_keeps_the_engines_meta_columns() {
416 let b = batch();
417 let off: arrow::array::Int64Array = vec![7, 8, 9].into();
418 let with_meta = RecordBatch::try_new(
419 Arc::new(Schema::new(
420 b.schema()
421 .fields()
422 .iter()
423 .map(|f| f.as_ref().clone())
424 .chain([Field::new("__fv_offset", DataType::Int64, true)])
425 .collect::<Vec<_>>(),
426 )),
427 b.columns().iter().cloned().chain([Arc::new(off) as ArrayRef]).collect(),
428 )
429 .unwrap();
430 let mut s = BatchSteps::new(vec![Step::Select {
431 columns: vec!["price".into()],
432 }]);
433 let out = s.apply(&with_meta).unwrap();
434 let names: Vec<String> = out.schema().fields().iter().map(|f| f.name().clone()).collect();
435 assert_eq!(names, vec!["price", "__fv_offset"]);
436 }
437
438 #[test]
439 fn a_poison_row_is_isolated_and_counted() {
440 let mut s = BatchSteps::new(vec![Step::ApplyExpression {
442 column: "r".into(),
443 expression: "10 / price".into(),
444 }]);
445 let out = s.apply(&batch()).unwrap();
446 assert_eq!(out.num_rows(), 2, "the price=0 row is dropped");
447 assert_eq!(s.dropped, 1);
448 assert_eq!(col_f64(&out, "r"), vec![Some(1.0), None]);
449 }
450
451 #[test]
452 fn recompiles_when_the_schema_changes() {
453 let mut s = BatchSteps::new(vec![Step::Filter {
454 expression: "price > 5".into(),
455 }]);
456 assert_eq!(s.apply(&batch()).unwrap().num_rows(), 1);
457 let other = batch().project(&[1]).unwrap(); assert_eq!(s.apply(&other).unwrap().num_rows(), 1);
459 }
460
461 #[test]
462 fn a_column_the_dialect_has_no_kind_for_runs_through_the_row_fallback() {
463 let schema = Arc::new(Schema::new(vec![Field::new(
464 "s",
465 DataType::Struct(vec![Field::new("x", DataType::Int64, true)].into()),
466 true,
467 )]));
468 let inner = arrow::array::StructArray::from(vec![(
469 Arc::new(Field::new("x", DataType::Int64, true)),
470 Arc::new(arrow::array::Int64Array::from(vec![1])) as ArrayRef,
471 )]);
472 let b = RecordBatch::try_new(schema, vec![Arc::new(inner)]).unwrap();
473 let mut s = BatchSteps::new(vec![Step::Filter {
474 expression: "s == 1".into(),
475 }]);
476 let out = s.apply(&b).unwrap();
479 assert_eq!(out.num_rows(), 0, "a struct never equals 1");
480 assert!(!s.exact);
481 }
482}