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, 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;
313 must_add_row_offset = true;
314 } else if field.name == ROW_ADDR {
315 with_row_addr = true;
316 } else if field.name == ROW_OFFSET {
317 with_row_addr = true;
318 must_add_row_offset = true;
319 } else if field.name == ROW_LAST_UPDATED_AT_VERSION {
320 with_row_last_updated_at_version = true;
321 } else if field.name == ROW_CREATED_AT_VERSION {
322 with_row_created_at_version = true;
323 }
324 } else {
325 if base.schema().field(&field.name).is_none() {
327 return Err(Error::invalid_input(format!(
328 "Column '{}' not found in schema",
329 field.name
330 )));
331 }
332 data_fields.push(field.clone());
333 }
334 }
335
336 let data_schema = Schema {
338 fields: data_fields,
339 metadata: projection.metadata.clone(),
340 };
341
342 let mut physical_projection = Projection::empty(base).union_schema(&data_schema);
344 physical_projection.with_row_id = with_row_id;
345 physical_projection.with_row_addr = with_row_addr;
346 physical_projection.with_row_last_updated_at_version = with_row_last_updated_at_version;
347 physical_projection.with_row_created_at_version = with_row_created_at_version;
348
349 let exprs = projection
351 .fields
352 .iter()
353 .map(|f| OutputColumn {
354 expr: Expr::Column(Column::from_name(&f.name)),
355 name: f.name.clone(),
356 })
357 .collect::<Vec<_>>();
358
359 Ok(Self {
360 physical_projection,
361 requested_output_expr: exprs,
362 must_add_row_offset,
363 scoring_exprs: HashMap::default(),
364 })
365 }
366
367 pub fn full(base: Arc<dyn Projectable>) -> Result<Self> {
368 let physical_cols: Vec<&str> = base
369 .schema()
370 .fields
371 .iter()
372 .map(|f| f.name.as_ref())
373 .collect::<Vec<_>>();
374
375 let physical_projection =
376 Projection::empty(base.clone()).union_columns(&physical_cols, OnMissing::Ignore)?;
377
378 let requested_output_expr = physical_cols
379 .into_iter()
380 .map(|col_name| OutputColumn {
381 expr: Expr::Column(Column::from_name(col_name)),
382 name: col_name.to_string(),
383 })
384 .collect();
385
386 Ok(Self {
387 physical_projection,
388 must_add_row_offset: false,
389 requested_output_expr,
390 scoring_exprs: HashMap::default(),
391 })
392 }
393
394 pub fn to_physical_exprs(
398 &self,
399 current_schema: &ArrowSchema,
400 ) -> Result<Vec<(Arc<dyn PhysicalExpr>, String)>> {
401 let physical_df_schema = Arc::new(DFSchema::try_from(current_schema.clone())?);
402 self.requested_output_expr
403 .iter()
404 .map(|output_column| {
405 let expr = if let Some(raw_expr) = self.scoring_exprs.get(&output_column.name) {
406 let planner = Planner::new(Arc::new(current_schema.clone()));
407 let expr = planner.parse_expr(raw_expr)?;
408 planner.optimize_expr(expr)?
409 } else {
410 output_column.expr.clone()
411 };
412 Ok((
413 datafusion::physical_expr::create_physical_expr(
414 &expr,
415 physical_df_schema.as_ref(),
416 &Default::default(),
417 )?,
418 output_column.name.clone(),
419 ))
420 })
421 .collect::<Result<Vec<_>>>()
422 }
423
424 pub fn include_row_id(&mut self) {
426 self.physical_projection.with_row_id = true;
427 if !self
428 .requested_output_expr
429 .iter()
430 .any(|OutputColumn { name, .. }| name == ROW_ID)
431 {
432 self.requested_output_expr.push(OutputColumn {
433 expr: Expr::Column(Column::from_name(ROW_ID)),
434 name: ROW_ID.to_string(),
435 });
436 }
437 }
438
439 pub fn include_row_addr(&mut self) {
441 self.physical_projection.with_row_addr = true;
442 if !self
443 .requested_output_expr
444 .iter()
445 .any(|OutputColumn { name, .. }| name == ROW_ADDR)
446 {
447 self.requested_output_expr.push(OutputColumn {
448 expr: Expr::Column(Column::from_name(ROW_ADDR)),
449 name: ROW_ADDR.to_string(),
450 });
451 }
452 }
453
454 pub fn has_output_cols(&self) -> bool {
459 !self.requested_output_expr.is_empty()
460 }
461
462 pub fn output_schema(&self) -> Result<ArrowSchema> {
463 let physical_schema = self.physical_projection.to_arrow_schema();
464 let exprs = self.to_physical_exprs(&physical_schema)?;
465 let fields = exprs
466 .iter()
467 .map(|(expr, name)| {
468 let metadata = expr.return_field(&physical_schema)?.metadata().clone();
469 Ok(ArrowField::new(
470 name,
471 expr.data_type(&physical_schema)?,
472 expr.nullable(&physical_schema)?,
473 )
474 .with_metadata(metadata))
475 })
476 .collect::<Result<Vec<_>>>()?;
477 Ok(ArrowSchema::new_with_metadata(
478 fields,
479 physical_schema.metadata().clone(),
480 ))
481 }
482
483 #[instrument(skip_all, level = "debug")]
484 pub async fn project_batch(&self, batch: RecordBatch) -> Result<RecordBatch> {
485 let src = Arc::new(OneShotExec::from_batch(batch));
486
487 let extra_columns = vec![
489 ArrowField::new(ROW_ADDR, DataType::UInt64, true),
490 ArrowField::new(ROW_OFFSET, DataType::UInt64, true),
491 ];
492 let mut filterable_schema = self.physical_projection.to_schema();
493 filterable_schema = filterable_schema.merge(&ArrowSchema::new(extra_columns))?;
494
495 let physical_exprs = self.to_physical_exprs(&(&filterable_schema).into())?;
496 let projection = Arc::new(ProjectionExec::try_new(physical_exprs, src)?);
497
498 let stream = execute_plan(
500 projection,
501 LanceExecutionOptions {
502 skip_logging: true,
503 ..Default::default()
504 },
505 )?;
506 let batches = stream.try_collect::<Vec<_>>().await?;
507 if batches.len() != 1 {
508 Err(Error::internal("Expected exactly one batch".to_string()))
509 } else {
510 Ok(batches.into_iter().next().unwrap())
511 }
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518
519 use arrow_array::{ArrayRef, Float32Array, Int64Array};
520 use lance_arrow::json::{is_json_field, json_field};
521
522 #[test]
523 fn test_scoring_column_expression() {
524 for scoring_column in ["_distance", "_score"] {
525 for has_stored_column in [false, true] {
526 let base = if has_stored_column {
527 Arc::new(
528 Schema::try_from(&ArrowSchema::new(vec![ArrowField::new(
529 scoring_column,
530 DataType::Float64,
531 true,
532 )]))
533 .unwrap(),
534 )
535 } else {
536 Arc::new(Schema::default())
537 };
538 let expression = format!("1 - {scoring_column}");
539 let plan =
540 ProjectionPlan::from_expressions(base, &[("inverted", expression.as_str())])
541 .unwrap();
542
543 if has_stored_column {
544 let stored_output = plan.output_schema().unwrap();
545 assert_eq!(stored_output.field(0).data_type(), &DataType::Float64);
546 }
547
548 let batch = RecordBatch::try_from_iter([(
549 scoring_column,
550 Arc::new(Float32Array::from(vec![0.25, 0.75])) as ArrayRef,
551 )])
552 .unwrap();
553
554 let physical_exprs = plan.to_physical_exprs(batch.schema().as_ref()).unwrap();
555 let values = physical_exprs[0]
556 .0
557 .evaluate(&batch)
558 .unwrap()
559 .into_array(batch.num_rows())
560 .unwrap();
561
562 assert_eq!(
563 values.as_ref(),
564 &Float32Array::from(vec![0.75, 0.25]),
565 "unexpected result for {scoring_column}",
566 );
567 }
568 }
569 }
570
571 #[test]
572 fn test_stored_scoring_column_does_not_break_other_expressions() {
573 for scoring_column in ["_distance", "_score"] {
574 let base = Arc::new(
575 Schema::try_from(&ArrowSchema::new(vec![
576 ArrowField::new("id", DataType::Int64, false),
577 ArrowField::new(scoring_column, DataType::Float64, true),
578 ]))
579 .unwrap(),
580 );
581
582 ProjectionPlan::from_expressions(base, &[("incremented", "id + 1")]).unwrap();
583 }
584 }
585
586 #[test]
587 fn test_stored_scoring_column_is_case_insensitive() {
588 for (stored_name, requested_name) in [("_Distance", "_distance"), ("_Score", "_score")] {
589 let base = Arc::new(
590 Schema::try_from(&ArrowSchema::new(vec![ArrowField::new(
591 stored_name,
592 DataType::Float64,
593 true,
594 )]))
595 .unwrap(),
596 );
597 let plan =
598 ProjectionPlan::from_expressions(base, &[("stored", requested_name)]).unwrap();
599
600 assert_eq!(
601 plan.output_schema().unwrap().field(0).data_type(),
602 &DataType::Float64,
603 );
604
605 let batch = RecordBatch::try_from_iter([(
606 requested_name,
607 Arc::new(Float32Array::from(vec![0.25, 0.75])) as ArrayRef,
608 )])
609 .unwrap();
610 let physical_exprs = plan.to_physical_exprs(batch.schema().as_ref()).unwrap();
611 assert_eq!(
612 physical_exprs[0]
613 .0
614 .data_type(batch.schema().as_ref())
615 .unwrap(),
616 DataType::Float32,
617 );
618 }
619 }
620
621 #[test]
622 fn test_generated_scoring_function_with_mixed_case_stored_column() {
623 for (stored_name, generated_name) in [("_Distance", "_distance"), ("_Score", "_score")] {
624 let base = Arc::new(
625 Schema::try_from(&ArrowSchema::new(vec![ArrowField::new(
626 stored_name,
627 DataType::Float64,
628 true,
629 )]))
630 .unwrap(),
631 );
632 let expression = format!("coalesce(1 - {generated_name}, 0)");
633 let plan =
634 ProjectionPlan::from_expressions(base, &[("normalized", expression.as_str())])
635 .unwrap();
636 let batch = RecordBatch::try_from_iter([(
637 generated_name,
638 Arc::new(Float32Array::from(vec![Some(0.25), None])) as ArrayRef,
639 )])
640 .unwrap();
641
642 let physical_exprs = plan.to_physical_exprs(batch.schema().as_ref()).unwrap();
643 let values = physical_exprs[0]
644 .0
645 .evaluate(&batch)
646 .unwrap()
647 .into_array(batch.num_rows())
648 .unwrap();
649 assert_eq!(values.as_ref(), &Float32Array::from(vec![0.75, 0.0]));
650 }
651 }
652
653 #[test]
654 fn test_scoring_column_function_expression() {
655 for scoring_column in ["_distance", "_score"] {
656 let expression = format!("coalesce(1 - {scoring_column}, 0)");
657 let plan = ProjectionPlan::from_expressions(
658 Arc::new(Schema::default()),
659 &[("normalized", expression.as_str())],
660 )
661 .unwrap();
662 let batch = RecordBatch::try_from_iter([(
663 scoring_column,
664 Arc::new(Float32Array::from(vec![Some(0.25), None])) as ArrayRef,
665 )])
666 .unwrap();
667
668 let physical_exprs = plan.to_physical_exprs(batch.schema().as_ref()).unwrap();
669 let values = physical_exprs[0]
670 .0
671 .evaluate(&batch)
672 .unwrap()
673 .into_array(batch.num_rows())
674 .unwrap();
675 assert_eq!(values.as_ref(), &Float32Array::from(vec![0.75, 0.0]));
676 }
677 }
678
679 #[tokio::test]
680 async fn test_coalesce_in_column_map() {
681 let arrow_schema = Arc::new(ArrowSchema::new(vec![
685 ArrowField::new("col_a", DataType::Int64, true),
686 ArrowField::new("col_b", DataType::Int64, true),
687 ]));
688 let base_schema = Schema::try_from(arrow_schema.as_ref()).unwrap();
689 let base = Arc::new(base_schema);
690
691 let plan =
692 ProjectionPlan::from_expressions(base, &[("foo", "coalesce(col_a, col_b)")]).unwrap();
693
694 let batch = RecordBatch::try_new(
695 arrow_schema,
696 vec![
697 Arc::new(Int64Array::from(vec![Some(1), None, Some(3), None])),
698 Arc::new(Int64Array::from(vec![Some(10), Some(20), None, None])),
699 ],
700 )
701 .unwrap();
702
703 let projected = plan.project_batch(batch).await.unwrap();
704 let foo = projected
705 .column(0)
706 .as_any()
707 .downcast_ref::<Int64Array>()
708 .unwrap();
709 assert_eq!(
710 foo.iter().collect::<Vec<_>>(),
711 vec![Some(1), Some(20), Some(3), None],
712 );
713 }
714
715 #[test]
716 fn test_output_schema_preserves_json_extension_metadata() {
717 let arrow_schema = ArrowSchema::new(vec![
718 ArrowField::new("id", DataType::Int32, false),
719 json_field("meta", true),
720 ]);
721 let base_schema = Schema::try_from(&arrow_schema).unwrap();
722 let base = Arc::new(base_schema.clone());
723
724 let plan = ProjectionPlan::from_schema(base, &base_schema).unwrap();
725
726 let physical = plan.physical_projection.to_arrow_schema();
727 assert!(is_json_field(physical.field_with_name("meta").unwrap()));
728
729 let output = plan.output_schema().unwrap();
730 let output_field = output.field_with_name("meta").unwrap();
731 assert!(is_json_field(output_field));
732 }
733}