1use arrow_array::RecordBatch;
5use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
6use datafusion::{logical_expr::Expr, physical_plan::projection::ProjectionExec};
7use datafusion_common::{Column, DFSchema};
8use datafusion_physical_expr::PhysicalExpr;
9use futures::TryStreamExt;
10use std::{
11 collections::{HashMap, HashSet},
12 sync::Arc,
13};
14use tracing::instrument;
15
16use lance_core::{
17 Error, ROW_ADDR, ROW_CREATED_AT_VERSION, ROW_ID, ROW_LAST_UPDATED_AT_VERSION, ROW_OFFSET,
18 Result, WILDCARD,
19 datatypes::{OnMissing, OnTypeMismatch, Projectable, Projection, Schema},
20};
21
22use crate::{
23 exec::{LanceExecutionOptions, OneShotExec, execute_plan},
24 planner::Planner,
25};
26
27const SCORING_COLUMNS: [&str; 2] = ["_distance", "_score"];
28
29fn canonical_scoring_column(name: &str) -> Option<&'static str> {
30 SCORING_COLUMNS
31 .into_iter()
32 .find(|scoring_column| name.eq_ignore_ascii_case(scoring_column))
33}
34
35struct ProjectionBuilder {
36 base: Arc<dyn Projectable>,
37 planner: Planner,
38 output: HashMap<String, Expr>,
39 output_cols: Vec<OutputColumn>,
40 scoring_exprs: HashMap<String, String>,
41 physical_cols_set: HashSet<String>,
42 physical_cols: Vec<String>,
43 needs_row_id: bool,
44 needs_row_addr: bool,
45 needs_row_last_updated_at: bool,
46 needs_row_created_at: bool,
47 must_add_row_offset: bool,
48 has_wildcard: bool,
49}
50
51impl ProjectionBuilder {
52 fn new(base: Arc<dyn Projectable>) -> Self {
53 let full_schema = Arc::new(Projection::full(base.clone()).to_arrow_schema());
54 let full_schema = Arc::new(ProjectionPlan::add_system_columns(&full_schema));
55 let planner = Planner::new(full_schema);
56
57 Self {
58 base,
59 planner,
60 output: HashMap::default(),
61 output_cols: Vec::default(),
62 scoring_exprs: HashMap::default(),
63 physical_cols_set: HashSet::default(),
64 physical_cols: Vec::default(),
65 needs_row_id: false,
66 needs_row_addr: false,
67 needs_row_created_at: false,
68 needs_row_last_updated_at: false,
69 must_add_row_offset: false,
70 has_wildcard: false,
71 }
72 }
73
74 fn check_duplicate_column(&self, name: &str) -> Result<()> {
75 if self.output.contains_key(name) {
76 return Err(Error::invalid_input(format!(
77 "Duplicate column name: {}",
78 name
79 )));
80 }
81 Ok(())
82 }
83
84 fn add_column(&mut self, output_name: &str, raw_expr: &str) -> Result<()> {
85 self.check_duplicate_column(output_name)?;
86
87 let expr = self.planner.parse_expr(raw_expr)?;
88 let expr = if Self::references_scoring_column(&expr) {
89 self.scoring_exprs
92 .insert(output_name.to_string(), raw_expr.to_string());
93 expr
94 } else {
95 self.planner.optimize_expr(expr)?
99 };
100
101 if let Expr::Column(Column {
103 name,
104 relation: None,
105 ..
106 }) = &expr
107 {
108 if name == ROW_ID {
109 self.needs_row_id = true;
110 } else if name == ROW_ADDR {
111 self.needs_row_addr = true;
112 } else if name == ROW_OFFSET {
113 self.must_add_row_offset = true;
114 } else if name == ROW_LAST_UPDATED_AT_VERSION {
115 self.needs_row_last_updated_at = true;
116 } else if name == ROW_CREATED_AT_VERSION {
117 self.needs_row_created_at = true;
118 }
119 }
120
121 for col in Planner::column_names_in_expr(&expr) {
122 let physical_col = if canonical_scoring_column(&col).is_some() {
126 self.base
127 .schema()
128 .field_case_insensitive(&col)
129 .map(|field| field.name.clone())
130 .unwrap_or(col)
131 } else {
132 col
133 };
134 if self.physical_cols_set.contains(&physical_col) {
135 continue;
136 }
137 self.physical_cols.push(physical_col.clone());
138 self.physical_cols_set.insert(physical_col);
139 }
140 self.output.insert(output_name.to_string(), expr.clone());
141
142 self.output_cols.push(OutputColumn {
143 expr,
144 name: output_name.to_string(),
145 });
146
147 Ok(())
148 }
149
150 fn references_scoring_column(expr: &Expr) -> bool {
151 Planner::column_names_in_expr(expr)
152 .iter()
153 .any(|name| canonical_scoring_column(name).is_some())
154 }
155
156 fn add_columns(&mut self, columns: &[(impl AsRef<str>, impl AsRef<str>)]) -> Result<()> {
157 for (output_name, raw_expr) in columns {
158 if raw_expr.as_ref() == WILDCARD {
159 self.has_wildcard = true;
160 for col in self.base.schema().fields.iter().map(|f| f.name.as_str()) {
161 self.check_duplicate_column(col)?;
162 self.output_cols.push(OutputColumn {
163 expr: Expr::Column(Column::from_name(col)),
164 name: col.to_string(),
165 });
166 self.output.insert(col.to_string(), Expr::default());
168 }
169 } else {
170 self.add_column(output_name.as_ref(), raw_expr.as_ref())?;
171 }
172 }
173 Ok(())
174 }
175
176 fn build(self) -> Result<ProjectionPlan> {
177 let mut physical_projection = if self.has_wildcard {
183 Projection::full(self.base.clone())
184 } else {
185 Projection::empty(self.base.clone())
186 .union_columns(&self.physical_cols, OnMissing::Ignore)?
187 };
188
189 physical_projection.with_row_id = self.needs_row_id;
190 physical_projection.with_row_addr = self.needs_row_addr || self.must_add_row_offset;
191 physical_projection.with_row_last_updated_at_version = self.needs_row_last_updated_at;
192 physical_projection.with_row_created_at_version = self.needs_row_created_at;
193
194 Ok(ProjectionPlan {
195 physical_projection,
196 must_add_row_offset: self.must_add_row_offset,
197 requested_output_expr: self.output_cols,
198 scoring_exprs: self.scoring_exprs,
199 })
200 }
201}
202
203#[derive(Clone, Debug)]
204pub struct OutputColumn {
205 pub expr: Expr,
207 pub name: String,
209}
210
211#[derive(Clone, Debug)]
212pub struct ProjectionPlan {
213 pub physical_projection: Projection,
215
216 pub must_add_row_offset: bool,
218
219 pub requested_output_expr: Vec<OutputColumn>,
221
222 scoring_exprs: HashMap<String, String>,
224}
225
226impl ProjectionPlan {
227 fn add_system_columns(schema: &ArrowSchema) -> ArrowSchema {
228 let mut fields = Vec::from_iter(schema.fields.iter().cloned());
229 fields.push(Arc::new(ArrowField::new(ROW_ID, DataType::UInt64, true)));
230 fields.push(Arc::new(ArrowField::new(ROW_ADDR, DataType::UInt64, true)));
231 fields.push(Arc::new(ArrowField::new(
232 ROW_OFFSET,
233 DataType::UInt64,
234 true,
235 )));
236 fields.push(Arc::new(
237 (*lance_core::ROW_LAST_UPDATED_AT_VERSION_FIELD).clone(),
238 ));
239 fields.push(Arc::new(
240 (*lance_core::ROW_CREATED_AT_VERSION_FIELD).clone(),
241 ));
242 for name in SCORING_COLUMNS {
246 if schema.field_with_name(name).is_err() {
247 fields.push(Arc::new(ArrowField::new(name, DataType::Float32, true)));
248 }
249 }
250 ArrowSchema::new(fields)
251 }
252
253 pub fn from_expressions(
255 base: Arc<dyn Projectable>,
256 columns: &[(impl AsRef<str>, impl AsRef<str>)],
257 ) -> Result<Self> {
258 let mut builder = ProjectionBuilder::new(base);
259 builder.add_columns(columns)?;
260 builder.build()
261 }
262
263 pub fn from_schema(base: Arc<dyn Projectable>, projection: &Schema) -> Result<Self> {
298 let mut data_fields = Vec::new();
302 let mut with_row_id = false;
303 let mut with_row_addr = false;
304 let mut must_add_row_offset = false;
305 let mut with_row_last_updated_at_version = false;
306 let mut with_row_created_at_version = false;
307
308 for field in projection.fields.iter() {
309 if lance_core::is_system_column(&field.name) {
310 if field.name == ROW_ID {
312 with_row_id = true;
316 } else if field.name == ROW_ADDR {
317 with_row_addr = true;
318 } else if field.name == ROW_OFFSET {
319 with_row_addr = true;
320 must_add_row_offset = true;
321 } else if field.name == ROW_LAST_UPDATED_AT_VERSION {
322 with_row_last_updated_at_version = true;
323 } else if field.name == ROW_CREATED_AT_VERSION {
324 with_row_created_at_version = true;
325 }
326 } else {
327 if base.schema().field(&field.name).is_none() {
329 return Err(Error::invalid_input(format!(
330 "Column '{}' not found in schema",
331 field.name
332 )));
333 }
334 data_fields.push(field.clone());
335 }
336 }
337
338 let data_schema = Schema {
340 fields: data_fields,
341 metadata: projection.metadata.clone(),
342 };
343
344 let data_schema = base.schema().project_by_schema(
352 &data_schema,
353 OnMissing::Error,
354 OnTypeMismatch::Error,
355 )?;
356
357 let mut physical_projection = Projection::empty(base).union_schema(&data_schema);
359 physical_projection.with_row_id = with_row_id;
360 physical_projection.with_row_addr = with_row_addr;
361 physical_projection.with_row_last_updated_at_version = with_row_last_updated_at_version;
362 physical_projection.with_row_created_at_version = with_row_created_at_version;
363
364 let exprs = projection
366 .fields
367 .iter()
368 .map(|f| OutputColumn {
369 expr: Expr::Column(Column::from_name(&f.name)),
370 name: f.name.clone(),
371 })
372 .collect::<Vec<_>>();
373
374 Ok(Self {
375 physical_projection,
376 requested_output_expr: exprs,
377 must_add_row_offset,
378 scoring_exprs: HashMap::default(),
379 })
380 }
381
382 pub fn full(base: Arc<dyn Projectable>) -> Result<Self> {
383 let physical_cols: Vec<&str> = base
384 .schema()
385 .fields
386 .iter()
387 .map(|f| f.name.as_ref())
388 .collect::<Vec<_>>();
389
390 let physical_projection =
391 Projection::empty(base.clone()).union_columns(&physical_cols, OnMissing::Ignore)?;
392
393 let requested_output_expr = physical_cols
394 .into_iter()
395 .map(|col_name| OutputColumn {
396 expr: Expr::Column(Column::from_name(col_name)),
397 name: col_name.to_string(),
398 })
399 .collect();
400
401 Ok(Self {
402 physical_projection,
403 must_add_row_offset: false,
404 requested_output_expr,
405 scoring_exprs: HashMap::default(),
406 })
407 }
408
409 pub fn to_physical_exprs(
413 &self,
414 current_schema: &ArrowSchema,
415 ) -> Result<Vec<(Arc<dyn PhysicalExpr>, String)>> {
416 let physical_df_schema = Arc::new(DFSchema::try_from(current_schema.clone())?);
417 self.requested_output_expr
418 .iter()
419 .map(|output_column| {
420 let expr = if let Some(raw_expr) = self.scoring_exprs.get(&output_column.name) {
421 let planner = Planner::new(Arc::new(current_schema.clone()));
422 let expr = planner.parse_expr(raw_expr)?;
423 planner.optimize_expr(expr)?
424 } else {
425 output_column.expr.clone()
426 };
427 Ok((
428 datafusion::physical_expr::create_physical_expr(
429 &expr,
430 physical_df_schema.as_ref(),
431 &Default::default(),
432 )?,
433 output_column.name.clone(),
434 ))
435 })
436 .collect::<Result<Vec<_>>>()
437 }
438
439 pub fn include_row_id(&mut self) {
441 self.physical_projection.with_row_id = true;
442 if !self
443 .requested_output_expr
444 .iter()
445 .any(|OutputColumn { name, .. }| name == ROW_ID)
446 {
447 self.requested_output_expr.push(OutputColumn {
448 expr: Expr::Column(Column::from_name(ROW_ID)),
449 name: ROW_ID.to_string(),
450 });
451 }
452 }
453
454 pub fn include_row_addr(&mut self) {
456 self.physical_projection.with_row_addr = true;
457 if !self
458 .requested_output_expr
459 .iter()
460 .any(|OutputColumn { name, .. }| name == ROW_ADDR)
461 {
462 self.requested_output_expr.push(OutputColumn {
463 expr: Expr::Column(Column::from_name(ROW_ADDR)),
464 name: ROW_ADDR.to_string(),
465 });
466 }
467 }
468
469 pub fn has_output_cols(&self) -> bool {
474 !self.requested_output_expr.is_empty()
475 }
476
477 pub fn output_schema(&self) -> Result<ArrowSchema> {
478 let physical_schema = self.physical_projection.to_arrow_schema();
479 let exprs = self.to_physical_exprs(&physical_schema)?;
480 let fields = exprs
481 .iter()
482 .map(|(expr, name)| {
483 let metadata = expr.return_field(&physical_schema)?.metadata().clone();
484 Ok(ArrowField::new(
485 name,
486 expr.data_type(&physical_schema)?,
487 expr.nullable(&physical_schema)?,
488 )
489 .with_metadata(metadata))
490 })
491 .collect::<Result<Vec<_>>>()?;
492 Ok(ArrowSchema::new_with_metadata(
493 fields,
494 physical_schema.metadata().clone(),
495 ))
496 }
497
498 #[instrument(skip_all, level = "debug")]
499 pub async fn project_batch(&self, batch: RecordBatch) -> Result<RecordBatch> {
500 let src = Arc::new(OneShotExec::from_batch(batch));
501
502 let extra_columns = vec![
504 ArrowField::new(ROW_ADDR, DataType::UInt64, true),
505 ArrowField::new(ROW_OFFSET, DataType::UInt64, true),
506 ];
507 let mut filterable_schema = self.physical_projection.to_schema();
508 filterable_schema = filterable_schema.merge(&ArrowSchema::new(extra_columns))?;
509
510 let physical_exprs = self.to_physical_exprs(&(&filterable_schema).into())?;
511 let projection = Arc::new(ProjectionExec::try_new(physical_exprs, src)?);
512
513 let stream = execute_plan(
515 projection,
516 LanceExecutionOptions {
517 skip_logging: true,
518 ..Default::default()
519 },
520 )?;
521 let batches = stream.try_collect::<Vec<_>>().await?;
522 if batches.len() != 1 {
523 Err(Error::internal("Expected exactly one batch".to_string()))
524 } else {
525 Ok(batches.into_iter().next().unwrap())
526 }
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 use arrow_array::{ArrayRef, Float32Array, Int64Array};
535 use arrow_schema::Fields;
536 use lance_arrow::json::{is_json_field, json_field};
537
538 #[test]
539 fn test_scoring_column_expression() {
540 for scoring_column in ["_distance", "_score"] {
541 for has_stored_column in [false, true] {
542 let base = if has_stored_column {
543 Arc::new(
544 Schema::try_from(&ArrowSchema::new(vec![ArrowField::new(
545 scoring_column,
546 DataType::Float64,
547 true,
548 )]))
549 .unwrap(),
550 )
551 } else {
552 Arc::new(Schema::default())
553 };
554 let expression = format!("1 - {scoring_column}");
555 let plan =
556 ProjectionPlan::from_expressions(base, &[("inverted", expression.as_str())])
557 .unwrap();
558
559 if has_stored_column {
560 let stored_output = plan.output_schema().unwrap();
561 assert_eq!(stored_output.field(0).data_type(), &DataType::Float64);
562 }
563
564 let batch = RecordBatch::try_from_iter([(
565 scoring_column,
566 Arc::new(Float32Array::from(vec![0.25, 0.75])) as ArrayRef,
567 )])
568 .unwrap();
569
570 let physical_exprs = plan.to_physical_exprs(batch.schema().as_ref()).unwrap();
571 let values = physical_exprs[0]
572 .0
573 .evaluate(&batch)
574 .unwrap()
575 .into_array(batch.num_rows())
576 .unwrap();
577
578 assert_eq!(
579 values.as_ref(),
580 &Float32Array::from(vec![0.75, 0.25]),
581 "unexpected result for {scoring_column}",
582 );
583 }
584 }
585 }
586
587 #[test]
588 fn test_stored_scoring_column_does_not_break_other_expressions() {
589 for scoring_column in ["_distance", "_score"] {
590 let base = Arc::new(
591 Schema::try_from(&ArrowSchema::new(vec![
592 ArrowField::new("id", DataType::Int64, false),
593 ArrowField::new(scoring_column, DataType::Float64, true),
594 ]))
595 .unwrap(),
596 );
597
598 ProjectionPlan::from_expressions(base, &[("incremented", "id + 1")]).unwrap();
599 }
600 }
601
602 #[test]
603 fn test_stored_scoring_column_is_case_insensitive() {
604 for (stored_name, requested_name) in [("_Distance", "_distance"), ("_Score", "_score")] {
605 let base = Arc::new(
606 Schema::try_from(&ArrowSchema::new(vec![ArrowField::new(
607 stored_name,
608 DataType::Float64,
609 true,
610 )]))
611 .unwrap(),
612 );
613 let plan =
614 ProjectionPlan::from_expressions(base, &[("stored", requested_name)]).unwrap();
615
616 assert_eq!(
617 plan.output_schema().unwrap().field(0).data_type(),
618 &DataType::Float64,
619 );
620
621 let batch = RecordBatch::try_from_iter([(
622 requested_name,
623 Arc::new(Float32Array::from(vec![0.25, 0.75])) as ArrayRef,
624 )])
625 .unwrap();
626 let physical_exprs = plan.to_physical_exprs(batch.schema().as_ref()).unwrap();
627 assert_eq!(
628 physical_exprs[0]
629 .0
630 .data_type(batch.schema().as_ref())
631 .unwrap(),
632 DataType::Float32,
633 );
634 }
635 }
636
637 #[test]
638 fn test_generated_scoring_function_with_mixed_case_stored_column() {
639 for (stored_name, generated_name) in [("_Distance", "_distance"), ("_Score", "_score")] {
640 let base = Arc::new(
641 Schema::try_from(&ArrowSchema::new(vec![ArrowField::new(
642 stored_name,
643 DataType::Float64,
644 true,
645 )]))
646 .unwrap(),
647 );
648 let expression = format!("coalesce(1 - {generated_name}, 0)");
649 let plan =
650 ProjectionPlan::from_expressions(base, &[("normalized", expression.as_str())])
651 .unwrap();
652 let batch = RecordBatch::try_from_iter([(
653 generated_name,
654 Arc::new(Float32Array::from(vec![Some(0.25), None])) as ArrayRef,
655 )])
656 .unwrap();
657
658 let physical_exprs = plan.to_physical_exprs(batch.schema().as_ref()).unwrap();
659 let values = physical_exprs[0]
660 .0
661 .evaluate(&batch)
662 .unwrap()
663 .into_array(batch.num_rows())
664 .unwrap();
665 assert_eq!(values.as_ref(), &Float32Array::from(vec![0.75, 0.0]));
666 }
667 }
668
669 #[test]
670 fn test_scoring_column_function_expression() {
671 for scoring_column in ["_distance", "_score"] {
672 let expression = format!("coalesce(1 - {scoring_column}, 0)");
673 let plan = ProjectionPlan::from_expressions(
674 Arc::new(Schema::default()),
675 &[("normalized", expression.as_str())],
676 )
677 .unwrap();
678 let batch = RecordBatch::try_from_iter([(
679 scoring_column,
680 Arc::new(Float32Array::from(vec![Some(0.25), None])) as ArrayRef,
681 )])
682 .unwrap();
683
684 let physical_exprs = plan.to_physical_exprs(batch.schema().as_ref()).unwrap();
685 let values = physical_exprs[0]
686 .0
687 .evaluate(&batch)
688 .unwrap()
689 .into_array(batch.num_rows())
690 .unwrap();
691 assert_eq!(values.as_ref(), &Float32Array::from(vec![0.75, 0.0]));
692 }
693 }
694
695 #[tokio::test]
696 async fn test_coalesce_in_column_map() {
697 let arrow_schema = Arc::new(ArrowSchema::new(vec![
701 ArrowField::new("col_a", DataType::Int64, true),
702 ArrowField::new("col_b", DataType::Int64, true),
703 ]));
704 let base_schema = Schema::try_from(arrow_schema.as_ref()).unwrap();
705 let base = Arc::new(base_schema);
706
707 let plan =
708 ProjectionPlan::from_expressions(base, &[("foo", "coalesce(col_a, col_b)")]).unwrap();
709
710 let batch = RecordBatch::try_new(
711 arrow_schema,
712 vec![
713 Arc::new(Int64Array::from(vec![Some(1), None, Some(3), None])),
714 Arc::new(Int64Array::from(vec![Some(10), Some(20), None, None])),
715 ],
716 )
717 .unwrap();
718
719 let projected = plan.project_batch(batch).await.unwrap();
720 let foo = projected
721 .column(0)
722 .as_any()
723 .downcast_ref::<Int64Array>()
724 .unwrap();
725 assert_eq!(
726 foo.iter().collect::<Vec<_>>(),
727 vec![Some(1), Some(20), Some(3), None],
728 );
729 }
730
731 #[test]
732 fn test_output_schema_preserves_json_extension_metadata() {
733 let arrow_schema = ArrowSchema::new(vec![
734 ArrowField::new("id", DataType::Int32, false),
735 json_field("meta", true),
736 ]);
737 let base_schema = Schema::try_from(&arrow_schema).unwrap();
738 let base = Arc::new(base_schema.clone());
739
740 let plan = ProjectionPlan::from_schema(base, &base_schema).unwrap();
741
742 let physical = plan.physical_projection.to_arrow_schema();
743 assert!(is_json_field(physical.field_with_name("meta").unwrap()));
744
745 let output = plan.output_schema().unwrap();
746 let output_field = output.field_with_name("meta").unwrap();
747 assert!(is_json_field(output_field));
748 }
749
750 fn nested_base_schema() -> Schema {
751 Schema::try_from(&ArrowSchema::new(vec![
752 ArrowField::new("id", DataType::Int32, false),
753 ArrowField::new(
754 "meta",
755 DataType::Struct(Fields::from(vec![
756 ArrowField::new("a", DataType::Int32, true),
757 ArrowField::new("b", DataType::Int32, true),
758 ])),
759 true,
760 ),
761 ]))
762 .unwrap()
763 }
764
765 #[test]
772 fn test_from_schema_rejects_a_nested_child_the_base_does_not_have() {
773 let requested = Schema::try_from(&ArrowSchema::new(vec![ArrowField::new(
774 "meta",
775 DataType::Struct(Fields::from(vec![
776 ArrowField::new("a", DataType::Int32, true),
777 ArrowField::new("c", DataType::Int32, true),
778 ])),
779 true,
780 )]))
781 .unwrap();
782
783 let error = ProjectionPlan::from_schema(Arc::new(nested_base_schema()), &requested)
784 .expect_err("a nested child outside the base schema must be rejected");
785 assert!(
786 error.to_string().contains('c'),
787 "the error should name the offending child: {error}"
788 );
789 }
790
791 #[test]
797 fn test_from_schema_resolves_independently_numbered_ids() {
798 let requested = Schema::try_from(&ArrowSchema::new(vec![ArrowField::new(
799 "meta",
800 DataType::Struct(Fields::from(vec![ArrowField::new(
801 "b",
802 DataType::Int32,
803 true,
804 )])),
805 true,
806 )]))
807 .unwrap();
808 let base_schema = nested_base_schema();
809 let requested_b = requested.fields[0].children[0].id;
811 let base_b = base_schema.fields[1].children[1].id;
812 assert_ne!(
813 requested_b, base_b,
814 "the requested id must differ from the base's for this to bite"
815 );
816
817 let plan = ProjectionPlan::from_schema(Arc::new(base_schema), &requested).unwrap();
818 let projected = plan.physical_projection.to_bare_schema();
819 assert_eq!(
820 projected.fields[0]
821 .children
822 .iter()
823 .map(|child| child.name.as_str())
824 .collect::<Vec<_>>(),
825 vec!["b"],
826 "the projection must follow the requested name, not the colliding id"
827 );
828 }
829}