Skip to main content

lance/dataset/
scanner.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::ops::Range;
5use std::pin::Pin;
6use std::sync::{Arc, LazyLock};
7use std::task::{Context, Poll};
8
9use arrow::array::AsArray;
10use arrow_array::{Array, Float32Array, Int64Array, RecordBatch};
11use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema, SchemaRef, SortOptions};
12use arrow_select::concat::concat_batches;
13use async_recursion::async_recursion;
14use chrono::Utc;
15use datafusion::common::{DFSchema, JoinType, NullEquality, SchemaExt, exec_datafusion_err};
16use datafusion::functions_aggregate;
17use datafusion::logical_expr::{Expr, ScalarUDF, col, lit};
18use datafusion::physical_expr::PhysicalSortExpr;
19use datafusion::physical_plan::coalesce_batches::CoalesceBatchesExec;
20use datafusion::physical_plan::expressions;
21use datafusion::physical_plan::projection::ProjectionExec as DFProjectionExec;
22use datafusion::physical_plan::sorts::sort::SortExec;
23use datafusion::physical_plan::{
24    ExecutionPlan, SendableRecordBatchStream,
25    aggregates::{AggregateExec, AggregateMode, PhysicalGroupBy},
26    display::DisplayableExecutionPlan,
27    limit::GlobalLimitExec,
28    repartition::RepartitionExec,
29    union::UnionExec,
30};
31use datafusion::scalar::ScalarValue;
32use datafusion_expr::ExprSchemable;
33use datafusion_expr::execution_props::ExecutionProps;
34use datafusion_functions::core::getfield::GetFieldFunc;
35use datafusion_physical_expr::expressions::Column;
36use datafusion_physical_expr::{LexOrdering, Partitioning, PhysicalExpr, create_physical_expr};
37use datafusion_physical_plan::joins::PartitionMode;
38use datafusion_physical_plan::projection::ProjectionExec;
39use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
40use datafusion_physical_plan::{empty::EmptyExec, joins::HashJoinExec};
41use futures::future::BoxFuture;
42use futures::stream::{Stream, StreamExt};
43use futures::{FutureExt, TryStreamExt};
44use lance_arrow::floats::{FloatType, coerce_float_vector};
45use lance_arrow::{DataTypeExt, SchemaExt as ArrowSchemaExt};
46use lance_core::datatypes::{
47    BlobHandling, Field, OnMissing, Projection, escape_field_path_for_project, format_field_path,
48};
49use lance_core::error::LanceOptionExt;
50use lance_core::utils::address::RowAddress;
51use lance_core::utils::mask::{RowAddrMask, RowAddrTreeMap};
52use lance_core::utils::tokio::get_num_compute_intensive_cpus;
53use lance_core::{ROW_ADDR, ROW_ID, ROW_OFFSET};
54use lance_datafusion::aggregate::Aggregate;
55use lance_datafusion::exec::{
56    LanceExecutionOptions, OneShotExec, StrictBatchSizeExec, analyze_plan, execute_plan,
57};
58use lance_datafusion::expr::safe_coerce_scalar;
59use lance_datafusion::projection::ProjectionPlan;
60use lance_file::reader::FileReaderOptions;
61use lance_index::IndexCriteria;
62use lance_index::scalar::FullTextSearchQuery;
63use lance_index::scalar::expression::{INDEX_EXPR_RESULT_SCHEMA, IndexExprResult, PlannerIndexExt};
64use lance_index::scalar::inverted::query::{
65    FtsQuery, FtsQueryNode, FtsSearchParams, MatchQuery, PhraseQuery, fill_fts_query_column,
66};
67use lance_index::scalar::inverted::{SCORE_COL, SCORE_FIELD};
68use lance_index::vector::{DIST_COL, Query};
69use lance_index::{DatasetIndexExt, scalar::expression::ScalarIndexExpr};
70use lance_index::{metrics::NoOpMetricsCollector, scalar::inverted::FTS_SCHEMA};
71use lance_io::stream::RecordBatchStream;
72use lance_linalg::distance::MetricType;
73use lance_table::format::{Fragment, IndexMetadata};
74use roaring::RoaringBitmap;
75use tracing::{Span, info_span, instrument};
76
77use super::Dataset;
78use crate::dataset::row_offsets_to_row_addresses;
79use crate::dataset::utils::SchemaAdapter;
80use crate::index::DatasetIndexInternalExt;
81use crate::index::vector::utils::{
82    default_distance_type_for, get_vector_dim, get_vector_type, validate_distance_type_for,
83};
84use crate::io::exec::filtered_read::{FilteredReadExec, FilteredReadOptions};
85use crate::io::exec::fts::{
86    BoostQueryExec, FlatMatchFilterExec, FlatMatchQueryExec, MatchQueryExec, PhraseQueryExec,
87};
88use crate::io::exec::knn::MultivectorScoringExec;
89use crate::io::exec::scalar_index::{MaterializeIndexExec, ScalarIndexExec};
90use crate::io::exec::{
91    AddRowAddrExec, FilterPlan as ExprFilterPlan, KNNVectorDistanceExec, LancePushdownScanExec,
92    LanceScanExec, Planner, PreFilterSource, ScanConfig, TakeExec,
93    knn::{KNN_INDEX_SCHEMA, new_knn_exec},
94    project,
95};
96use crate::io::exec::{AddRowOffsetExec, LanceFilterExec, LanceScanConfig, get_physical_optimizer};
97use crate::{Error, Result};
98use crate::{datatypes::Schema, io::exec::fts::BooleanQueryExec};
99
100pub use lance_datafusion::exec::{ExecutionStatsCallback, ExecutionSummaryCounts};
101#[cfg(feature = "substrait")]
102use lance_datafusion::substrait::parse_substrait;
103
104pub(crate) const BATCH_SIZE_FALLBACK: usize = 8192;
105
106/// Parse an environment variable as a specific type, logging a warning on parse failure.
107fn parse_env_var<T: std::str::FromStr>(env_var_name: &str, default_val: &str) -> Option<T>
108where
109    T::Err: std::fmt::Display,
110{
111    std::env::var(env_var_name)
112        .ok()
113        .and_then(|val| match val.parse() {
114            Ok(value) => Some(value),
115            Err(e) => {
116                log::warn!(
117                    "Failed to parse the environment variable {}='{}': {}, the default value is: {}.",
118                    env_var_name,
119                    val,
120                    e,
121                    default_val
122                );
123                None
124            }
125        })
126}
127
128// For backwards compatibility / historical reasons we re-calculate the default batch size
129// on each call
130pub fn get_default_batch_size() -> Option<usize> {
131    parse_env_var("LANCE_DEFAULT_BATCH_SIZE", &BATCH_SIZE_FALLBACK.to_string())
132}
133
134pub const LEGACY_DEFAULT_FRAGMENT_READAHEAD: usize = 4;
135
136pub static DEFAULT_FRAGMENT_READAHEAD: LazyLock<Option<usize>> = LazyLock::new(|| {
137    parse_env_var(
138        "LANCE_DEFAULT_FRAGMENT_READAHEAD",
139        &LEGACY_DEFAULT_FRAGMENT_READAHEAD.to_string(),
140    )
141});
142
143const DEFAULT_XTR_OVERFETCH_VALUE: u32 = 10;
144
145pub static DEFAULT_XTR_OVERFETCH: LazyLock<u32> = LazyLock::new(|| {
146    parse_env_var(
147        "LANCE_XTR_OVERFETCH",
148        &DEFAULT_XTR_OVERFETCH_VALUE.to_string(),
149    )
150    .unwrap_or(DEFAULT_XTR_OVERFETCH_VALUE)
151});
152
153// We want to support ~256 concurrent reads to maximize throughput on cloud storage systems
154// Our typical page size is 8MiB (though not all reads are this large yet due to offset buffers, validity buffers, etc.)
155// So we want to support 256 * 8MiB ~= 2GiB of queued reads
156const DEFAULT_IO_BUFFER_SIZE_VALUE: u64 = 2 * 1024 * 1024 * 1024;
157
158pub static DEFAULT_IO_BUFFER_SIZE: LazyLock<u64> = LazyLock::new(|| {
159    parse_env_var(
160        "LANCE_DEFAULT_IO_BUFFER_SIZE",
161        &DEFAULT_IO_BUFFER_SIZE_VALUE.to_string(),
162    )
163    .unwrap_or(DEFAULT_IO_BUFFER_SIZE_VALUE)
164});
165
166/// Defines an ordering for a single column
167///
168/// Floats are sorted using the IEEE 754 total ordering
169/// Strings are sorted using UTF-8 lexicographic order (i.e. we sort the binary)
170#[derive(Debug, Clone)]
171pub struct ColumnOrdering {
172    pub ascending: bool,
173    pub nulls_first: bool,
174    pub column_name: String,
175}
176
177impl ColumnOrdering {
178    pub fn asc_nulls_first(column_name: String) -> Self {
179        Self {
180            ascending: true,
181            nulls_first: true,
182            column_name,
183        }
184    }
185
186    pub fn asc_nulls_last(column_name: String) -> Self {
187        Self {
188            ascending: true,
189            nulls_first: false,
190            column_name,
191        }
192    }
193
194    pub fn desc_nulls_first(column_name: String) -> Self {
195        Self {
196            ascending: false,
197            nulls_first: true,
198            column_name,
199        }
200    }
201
202    pub fn desc_nulls_last(column_name: String) -> Self {
203        Self {
204            ascending: false,
205            nulls_first: false,
206            column_name,
207        }
208    }
209}
210
211/// Materialization style for the scanner
212///
213/// This only affects columns that are not used in a filter
214///
215/// Early materialization will fetch the entire column and throw
216/// away the rows that are not needed.  This fetches more data but
217/// uses fewer I/O requests.
218///
219/// Late materialization will only fetch the rows that are needed.
220/// This fetches less data but uses more I/O requests.
221///
222/// This parameter only affects scans.  Vector search and full text search
223/// always use late materialization.
224#[derive(Clone)]
225pub enum MaterializationStyle {
226    /// Heuristic-based materialization style
227    ///
228    /// The default approach depends on the type of object storage.  For
229    /// cloud storage (e.g. S3, GCS, etc.) we only use late materialization
230    /// for columns that are more than 1000 bytes in size.
231    ///
232    /// For local storage we use late materialization for columns that are
233    /// more than 10 bytes in size.
234    ///
235    /// These values are based on experimentation and the assumption that a
236    /// filter will be selecting ~0.1% of the rows in a column.
237    Heuristic,
238    /// All columns will be fetched with late materialization where possible
239    AllLate,
240    /// All columns will be fetched with early materialization where possible
241    AllEarly,
242    /// All columns will be fetched with late materialization except for the specified columns
243    AllEarlyExcept(Vec<u32>),
244}
245
246impl MaterializationStyle {
247    pub fn all_early_except(columns: &[impl AsRef<str>], schema: &Schema) -> Result<Self> {
248        let field_ids = schema
249            .project(columns)?
250            .field_ids()
251            .into_iter()
252            .map(|id| id as u32)
253            .collect();
254        Ok(Self::AllEarlyExcept(field_ids))
255    }
256}
257
258#[derive(Debug)]
259struct PlannedFilteredScan {
260    plan: Arc<dyn ExecutionPlan>,
261    limit_pushed_down: bool,
262    filter_pushed_down: bool,
263}
264
265pub struct FilterPlan {
266    // Query filter plan
267    query_filter: Option<QueryFilter>,
268    refine_query_filter: bool,
269    // Expr filter plan
270    expr_filter_plan: ExprFilterPlan,
271}
272
273impl FilterPlan {
274    pub fn new(query_filter: Option<QueryFilter>, expr_filter_plan: ExprFilterPlan) -> Self {
275        Self {
276            query_filter,
277            refine_query_filter: false,
278            expr_filter_plan,
279        }
280    }
281
282    pub fn disable_refine(&mut self) {
283        self.expr_filter_plan = ExprFilterPlan::default();
284        self.refine_query_filter = false;
285    }
286
287    pub fn make_refine_only(&mut self) {
288        self.expr_filter_plan.make_refine_only();
289        self.refine_query_filter = true;
290    }
291
292    pub fn fts_filter(&self) -> Option<FullTextSearchQuery> {
293        match &self.query_filter {
294            Some(QueryFilter::Fts(query)) => Some(query.clone()),
295            _ => None,
296        }
297    }
298
299    pub fn vector_filter(&self) -> Option<Query> {
300        match &self.query_filter {
301            Some(QueryFilter::Vector(query)) => Some(query.clone()),
302            _ => None,
303        }
304    }
305
306    pub fn has_refine(&self) -> bool {
307        self.expr_filter_plan.has_refine() || self.refine_query_filter
308    }
309
310    pub async fn refine_columns(&self, dataset: &Arc<Dataset>) -> Result<Vec<String>> {
311        let mut columns = vec![];
312
313        if self.expr_filter_plan.has_refine() {
314            columns.extend(self.expr_filter_plan.refine_columns());
315        }
316
317        if self.refine_query_filter {
318            match &self.query_filter {
319                Some(QueryFilter::Fts(fts_query)) => {
320                    let cols = if fts_query.columns().is_empty() {
321                        let indexed_columns = fts_indexed_columns(dataset.clone()).await?;
322                        let q = fill_fts_query_column(&fts_query.query, &indexed_columns, false)?;
323                        q.columns()
324                    } else {
325                        fts_query.columns()
326                    };
327
328                    // Add refine column for match query since it supports `FlatMatchQueryExec`.
329                    // Other fts query use join so we don't need to add refine column.
330                    if let FtsQuery::Match(_) = &fts_query.query {
331                        columns.extend(cols.iter().cloned().collect::<Vec<_>>());
332                    }
333                }
334                Some(QueryFilter::Vector(vector_query)) => {
335                    columns.push(vector_query.column.clone());
336                }
337                None => {}
338            }
339        }
340
341        Ok(columns)
342    }
343
344    pub async fn refine_filter(
345        &self,
346        input: Arc<dyn ExecutionPlan>,
347        scanner: &Scanner,
348    ) -> Result<Arc<dyn ExecutionPlan>> {
349        let mut plan = input;
350
351        if self.refine_query_filter {
352            match &self.query_filter {
353                Some(QueryFilter::Fts(fts_query)) => {
354                    plan = scanner.flat_fts_filter(plan, fts_query).await?;
355                }
356                Some(QueryFilter::Vector(vector_query)) => {
357                    plan = scanner.flat_knn(plan, vector_query)?;
358                }
359                None => {}
360            }
361        }
362
363        if let Some(refine_expr) = &self.expr_filter_plan.refine_expr {
364            // We create a new planner specific to the node's schema, since
365            // physical expressions reference column by index rather than by name.
366            plan = Arc::new(LanceFilterExec::try_new(refine_expr.clone(), plan)?);
367        }
368
369        Ok(plan)
370    }
371}
372
373#[derive(Debug, Clone, Default)]
374pub struct LanceFilter {
375    query_filter: Option<QueryFilter>,
376    expr_filter: Option<ExprFilter>,
377}
378
379impl LanceFilter {
380    pub fn is_none(&self) -> bool {
381        self.query_filter.is_none() && self.expr_filter.is_none()
382    }
383}
384
385/// Query filter for filtering rows
386#[derive(Debug, Clone)]
387pub enum QueryFilter {
388    Fts(FullTextSearchQuery),
389    Vector(Query),
390}
391
392/// Expr filter for filtering rows
393#[derive(Debug, Clone)]
394pub enum ExprFilter {
395    /// The filter is an SQL string
396    Sql(String),
397    /// The filter is a Substrait expression
398    Substrait(Vec<u8>),
399    /// The filter is a Datafusion expression
400    Datafusion(Expr),
401}
402
403impl ExprFilter {
404    /// Converts the filter to a Datafusion expression
405    ///
406    /// The schema for this conversion should be the full schema available to
407    /// the filter (`full_schema`).  However, due to a limitation in the way
408    /// we do Substrait conversion today we can only do Substrait conversion with
409    /// the dataset schema (`dataset_schema`).  This means that Substrait will
410    /// not be able to access columns that are not in the dataset schema (e.g.
411    /// _rowid, _rowaddr, etc.)
412    #[allow(unused)]
413    #[instrument(level = "trace", name = "filter_to_df", skip_all)]
414    pub fn to_datafusion(&self, dataset_schema: &Schema, full_schema: &Schema) -> Result<Expr> {
415        match self {
416            Self::Sql(sql) => {
417                let schema = Arc::new(ArrowSchema::from(full_schema));
418                let planner = Planner::new(schema.clone());
419                let filter = planner.parse_filter(sql)?;
420
421                let df_schema = DFSchema::try_from(schema)?;
422                let ret_field = filter.to_field(&df_schema)?.1;
423                let ret_type = ret_field.data_type();
424                if ret_type != &DataType::Boolean {
425                    return Err(Error::invalid_input_source(
426                        format!("The filter {} does not return a boolean", filter).into(),
427                    ));
428                }
429
430                let optimized = planner.optimize_expr(filter).map_err(|e| {
431                    Error::invalid_input(format!("Error optimizing sql filter: {sql} ({e})"))
432                })?;
433                Ok(optimized)
434            }
435            #[cfg(feature = "substrait")]
436            Self::Substrait(expr) => {
437                use lance_datafusion::exec::{LanceExecutionOptions, get_session_context};
438
439                let ctx = get_session_context(&LanceExecutionOptions::default());
440                let state = ctx.state();
441                let schema = Arc::new(ArrowSchema::from(dataset_schema));
442                let expr = parse_substrait(expr, schema.clone(), &ctx.state())
443                    .now_or_never()
444                    .expect("could not parse the Substrait filter in a synchronous fashion")?;
445                let planner = Planner::new(schema);
446                planner.optimize_expr(expr.clone()).map_err(|e| {
447                    Error::invalid_input(format!(
448                        "Error optimizing substrait filter: {expr:?} ({e})"
449                    ))
450                })
451            }
452            #[cfg(not(feature = "substrait"))]
453            Self::Substrait(_) => Err(Error::not_supported_source(
454                "Substrait filter is not supported in this build".into(),
455            )),
456            Self::Datafusion(expr) => Ok(expr.clone()),
457        }
458    }
459}
460
461/// Aggregate expression from Substrait or DataFusion.
462#[derive(Debug, Clone)]
463pub enum AggregateExpr {
464    #[cfg(feature = "substrait")]
465    Substrait(Vec<u8>),
466    Datafusion {
467        group_by: Vec<Expr>,
468        aggregates: Vec<Expr>,
469    },
470}
471
472impl AggregateExpr {
473    /// Create a new builder for aggregate expressions.
474    ///
475    /// # Example
476    /// ```ignore
477    /// let agg = AggregateExpr::builder()
478    ///     .group_by("category")
479    ///     .count_star().alias("total_count")
480    ///     .sum("amount").alias("total_amount")
481    ///     .avg("price")
482    ///     .build();
483    /// scanner.aggregate(agg);
484    /// ```
485    pub fn builder() -> AggregateExprBuilder<false> {
486        AggregateExprBuilder::new()
487    }
488
489    /// Create from Substrait Plan bytes.
490    #[cfg(feature = "substrait")]
491    pub fn substrait(bytes: impl Into<Vec<u8>>) -> Self {
492        Self::Substrait(bytes.into())
493    }
494
495    /// Create from DataFusion expressions.
496    /// Use `.alias()` on expressions to set output column names.
497    pub fn datafusion(group_by: Vec<Expr>, aggregates: Vec<Expr>) -> Self {
498        Self::Datafusion {
499            group_by,
500            aggregates,
501        }
502    }
503
504    /// Parse into a unified Aggregate structure.
505    ///
506    /// For Substrait, this parses the bytes into DataFusion expressions.
507    /// For DataFusion, this just wraps the expressions.
508    ///
509    /// The schema is used to resolve field references in Substrait expressions.
510    fn parse(self, #[allow(unused_variables)] schema: Arc<ArrowSchema>) -> Result<Aggregate> {
511        match self {
512            #[cfg(feature = "substrait")]
513            Self::Substrait(bytes) => {
514                use lance_datafusion::exec::{LanceExecutionOptions, get_session_context};
515                use lance_datafusion::substrait::parse_substrait_aggregate;
516
517                let ctx = get_session_context(&LanceExecutionOptions::default());
518                parse_substrait_aggregate(&bytes, schema, &ctx.state())
519                    .now_or_never()
520                    .expect("could not parse the Substrait aggregate in a synchronous fashion")
521            }
522            Self::Datafusion {
523                group_by,
524                aggregates,
525            } => Ok(Aggregate::new(group_by, aggregates)),
526        }
527    }
528}
529
530/// Builder for creating aggregate expressions without using DataFusion or Substrait directly.
531///
532/// The const generic `HAS_PENDING` tracks whether there's a pending aggregate that can be aliased.
533/// When `HAS_PENDING` is `true`, the last item in `aggregates` is the pending aggregate.
534#[derive(Debug, Clone)]
535pub struct AggregateExprBuilder<const HAS_PENDING: bool> {
536    group_by: Vec<Expr>,
537    aggregates: Vec<Expr>,
538}
539
540impl Default for AggregateExprBuilder<false> {
541    fn default() -> Self {
542        Self {
543            group_by: Vec::new(),
544            aggregates: Vec::new(),
545        }
546    }
547}
548
549impl AggregateExprBuilder<false> {
550    /// Create a new builder.
551    pub fn new() -> Self {
552        Self::default()
553    }
554
555    /// Build the aggregate expression.
556    pub fn build(self) -> AggregateExpr {
557        AggregateExpr::Datafusion {
558            group_by: self.group_by,
559            aggregates: self.aggregates,
560        }
561    }
562}
563
564impl<const HAS_PENDING: bool> AggregateExprBuilder<HAS_PENDING> {
565    /// Add a column to group by.
566    ///
567    /// Multiple invocations will add to the list (not replace it).
568    /// E.g. `.group_by("x").group_by("y")` will group by both `x` and `y`.
569    pub fn group_by(mut self, column: impl Into<String>) -> AggregateExprBuilder<false> {
570        self.group_by.push(col(column.into()));
571        AggregateExprBuilder {
572            group_by: self.group_by,
573            aggregates: self.aggregates,
574        }
575    }
576
577    /// Add multiple columns to group by.
578    ///
579    /// Multiple invocations will add to the list (not replace it).
580    /// E.g. `.group_by("x").group_by_columns(["y", "z"])` will group by `x`, `y`, and `z`.
581    pub fn group_by_columns(
582        mut self,
583        columns: impl IntoIterator<Item = impl Into<String>>,
584    ) -> AggregateExprBuilder<false> {
585        for column in columns {
586            self.group_by.push(col(column.into()));
587        }
588        AggregateExprBuilder {
589            group_by: self.group_by,
590            aggregates: self.aggregates,
591        }
592    }
593
594    /// Add COUNT(*) aggregate that counts all rows.
595    pub fn count_star(mut self) -> AggregateExprBuilder<true> {
596        self.aggregates
597            .push(functions_aggregate::count::count(lit(1)));
598        AggregateExprBuilder {
599            group_by: self.group_by,
600            aggregates: self.aggregates,
601        }
602    }
603
604    /// Add COUNT(column) aggregate.
605    ///
606    /// Unlike `count_star`, this will only count the number of rows where `column`
607    /// is not NULL.
608    pub fn count(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
609        self.aggregates
610            .push(functions_aggregate::count::count(col(column.into())));
611        AggregateExprBuilder {
612            group_by: self.group_by,
613            aggregates: self.aggregates,
614        }
615    }
616
617    /// Add SUM(column) aggregate.
618    pub fn sum(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
619        self.aggregates
620            .push(functions_aggregate::sum::sum(col(column.into())));
621        AggregateExprBuilder {
622            group_by: self.group_by,
623            aggregates: self.aggregates,
624        }
625    }
626
627    /// Add AVG(column) aggregate.
628    pub fn avg(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
629        self.aggregates
630            .push(functions_aggregate::average::avg(col(column.into())));
631        AggregateExprBuilder {
632            group_by: self.group_by,
633            aggregates: self.aggregates,
634        }
635    }
636
637    /// Add MIN(column) aggregate.
638    pub fn min(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
639        self.aggregates
640            .push(functions_aggregate::min_max::min(col(column.into())));
641        AggregateExprBuilder {
642            group_by: self.group_by,
643            aggregates: self.aggregates,
644        }
645    }
646
647    /// Add MAX(column) aggregate.
648    pub fn max(mut self, column: impl Into<String>) -> AggregateExprBuilder<true> {
649        self.aggregates
650            .push(functions_aggregate::min_max::max(col(column.into())));
651        AggregateExprBuilder {
652            group_by: self.group_by,
653            aggregates: self.aggregates,
654        }
655    }
656}
657
658impl AggregateExprBuilder<true> {
659    /// Set an alias for the pending aggregate (the last added aggregate).
660    pub fn alias(mut self, name: impl Into<String>) -> AggregateExprBuilder<false> {
661        let pending = self.aggregates.pop().expect("pending aggregate must exist");
662        self.aggregates.push(pending.alias(name.into()));
663        AggregateExprBuilder {
664            group_by: self.group_by,
665            aggregates: self.aggregates,
666        }
667    }
668
669    /// Build the aggregate expression.
670    pub fn build(self) -> AggregateExpr {
671        AggregateExpr::Datafusion {
672            group_by: self.group_by,
673            aggregates: self.aggregates,
674        }
675    }
676}
677
678/// Dataset Scanner
679///
680/// ```rust,ignore
681/// let dataset = Dataset::open(uri).await.unwrap();
682/// let stream = dataset.scan()
683///     .project(&["col", "col2.subfield"]).unwrap()
684///     .limit(10)
685///     .into_stream();
686/// stream
687///   .map(|batch| batch.num_rows())
688///   .buffered(16)
689///   .sum()
690/// ```
691#[derive(Clone)]
692pub struct Scanner {
693    dataset: Arc<Dataset>,
694
695    /// The projection plan for the scanner
696    ///
697    /// This includes
698    /// - The physical projection that must be read from the dataset
699    /// - Dynamic expressions that are evaluated after the physical projection
700    /// - The names of the output columns
701    projection_plan: ProjectionPlan,
702    blob_handling: BlobHandling,
703
704    /// If true then the filter will be applied before an index scan
705    prefilter: bool,
706
707    /// Materialization style controls when columns are fetched
708    materialization_style: MaterializationStyle,
709
710    /// Filter.
711    filter: LanceFilter,
712
713    /// Optional full text search query
714    full_text_query: Option<FullTextSearchQuery>,
715
716    /// The batch size controls the maximum size of rows to return for each read.
717    batch_size: Option<usize>,
718
719    /// Number of batches to prefetch
720    batch_readahead: usize,
721
722    /// Number of fragments to read concurrently
723    fragment_readahead: Option<usize>,
724
725    /// Number of bytes to allow to queue up in the I/O buffer
726    io_buffer_size: Option<u64>,
727
728    limit: Option<i64>,
729    offset: Option<i64>,
730
731    /// If Some then results will be ordered by the provided ordering
732    ///
733    /// If there are multiple columns the results will first be ordered
734    /// by the first column.  Then, any values whose first column is equal
735    /// will be sorted by the next column, and so on.
736    ///
737    /// If this is Some then the value of `ordered` is ignored.  The scan
738    /// will always be unordered since we are just going to reorder it anyways.
739    ordering: Option<Vec<ColumnOrdering>>,
740
741    nearest: Option<Query>,
742
743    /// If false, do not use any scalar indices for the scan
744    ///
745    /// This can be used to pick a more efficient plan for certain queries where
746    /// scalar indices do not work well (though we should also improve our planning
747    /// to handle this better in the future as well)
748    use_scalar_index: bool,
749
750    /// Whether to use statistics to optimize the scan (default: true)
751    ///
752    /// This is used for debugging or benchmarking purposes.
753    use_stats: bool,
754
755    /// Whether to scan in deterministic order (default: true)
756    ///
757    /// This field is ignored if `ordering` is defined
758    ordered: bool,
759
760    /// If set, this scanner serves only these fragments.
761    fragments: Option<Vec<Fragment>>,
762
763    /// Only search the data being indexed (weak consistency search).
764    ///
765    /// Default value is false.
766    ///
767    /// This is essentially a weak consistency search. Users can run index or optimize index
768    /// to make the index catch up with the latest data.
769    fast_search: bool,
770
771    /// If true, the scanner will emit deleted rows
772    include_deleted_rows: bool,
773
774    /// If set, this callback will be called after the scan with summary statistics
775    scan_stats_callback: Option<ExecutionStatsCallback>,
776
777    /// Whether the result returned by the scanner must be of the size of the batch_size.
778    /// By default, it is false.
779    /// Mainly, if the result is returned strictly according to the batch_size,
780    /// batching and waiting are required, and the performance will decrease.
781    strict_batch_size: bool,
782
783    /// File reader options to use when reading data files.
784    file_reader_options: Option<FileReaderOptions>,
785
786    aggregate: Option<Aggregate>,
787
788    // Legacy fields to help migrate some old projection behavior to new behavior
789    //
790    // There are two behaviors we are moving away from:
791    //
792    // First, the old behavior used methods like with_row_id and with_row_addr to add
793    // "system" columns.  The new behavior is to specify them in the projection like any
794    // other column.  The only difference between a system column and a regular column is
795    // that system columns are not returned in the schema and are not returned by default
796    // (i.e. "SELECT *")
797    //
798    // Second, the old behavior would _always_ add the _score or _distance columns to the
799    // output and there was no way for the user to opt out.  The new behavior treats the
800    // _score and _distance as regular output columns of the "search table function".  If
801    // the user does not specify a projection (i.e. "SELECT *") then we will add the _score
802    // and _distance columns to the end.  If the user does specify a projection then they
803    // must request those columns for them to show up.
804    //
805    // --------------------------------------------------------------------------
806    /// Whether the user wants the row id on top of the projection, will always come last
807    /// except possibly before _rowaddr
808    legacy_with_row_id: bool,
809    /// Whether the user wants the row address on top of the projection, will always come last
810    legacy_with_row_addr: bool,
811    /// Whether the user explicitly requested a projection.  If they did then we will warn them
812    /// if they do not specify _score / _distance unless legacy_projection_behavior is set to false
813    explicit_projection: bool,
814    /// Whether the user wants to use the legacy projection behavior.
815    autoproject_scoring_columns: bool,
816}
817
818/// Represents a user-requested take operation
819#[derive(Debug, Clone)]
820pub enum TakeOperation {
821    /// Take rows by row id
822    RowIds(Vec<u64>),
823    /// Take rows by row address
824    RowAddrs(Vec<u64>),
825    /// Take rows by row offset
826    ///
827    /// The row offset is the offset of the row in the dataset.  This can
828    /// be converted to row addresses using the fragment sizes.
829    RowOffsets(Vec<u64>),
830}
831
832impl TakeOperation {
833    fn extract_u64_list(list: &[Expr]) -> Option<Vec<u64>> {
834        let mut u64s = Vec::with_capacity(list.len());
835        for expr in list {
836            if let Expr::Literal(lit, _) = expr {
837                if let Some(ScalarValue::UInt64(Some(val))) =
838                    safe_coerce_scalar(lit, &DataType::UInt64)
839                {
840                    u64s.push(val);
841                } else {
842                    return None;
843                }
844            } else {
845                return None;
846            }
847        }
848        Some(u64s)
849    }
850
851    fn merge(self, other: Self) -> Option<Self> {
852        match (self, other) {
853            (Self::RowIds(mut left), Self::RowIds(right)) => {
854                left.extend(right);
855                Some(Self::RowIds(left))
856            }
857            (Self::RowAddrs(mut left), Self::RowAddrs(right)) => {
858                left.extend(right);
859                Some(Self::RowAddrs(left))
860            }
861            (Self::RowOffsets(mut left), Self::RowOffsets(right)) => {
862                left.extend(right);
863                Some(Self::RowOffsets(left))
864            }
865            _ => None,
866        }
867    }
868
869    /// Attempts to create a take operation from an expression.  This will succeed if the expression
870    /// has one of the following forms:
871    ///  - `_rowid = 10`
872    ///  - `_rowid = 10 OR _rowid = 20 OR _rowid = 30`
873    ///  - `_rowid IN (10, 20, 30)`
874    ///  - `_rowaddr = 10`
875    ///  - `_rowaddr = 10 OR _rowaddr = 20 OR _rowaddr = 30`
876    ///  - `_rowaddr IN (10, 20, 30)`
877    ///  - `_rowoffset = 10`
878    ///  - `_rowoffset = 10 OR _rowoffset = 20 OR _rowoffset = 30`
879    ///  - `_rowoffset IN (10, 20, 30)`
880    ///
881    /// The _rowid / _rowaddr / _rowoffset determine if we are taking by row id, address, or offset.
882    ///
883    /// If a take expression is combined with some other filter via an AND then the remainder will be
884    /// returned as well.  For example, `_rowid = 10` will return (take_op, None) and
885    /// `_rowid = 10 AND x > 70` will return (take_op, Some(x > 70)).
886    fn try_from_expr(expr: &Expr) -> Option<(Self, Option<Expr>)> {
887        if let Expr::BinaryExpr(binary) = expr {
888            match binary.op {
889                datafusion_expr::Operator::And => {
890                    let left_take = Self::try_from_expr(&binary.left);
891                    let right_take = Self::try_from_expr(&binary.right);
892                    match (left_take, right_take) {
893                        (Some(_), Some(_)) => {
894                            // This is something like...
895                            //
896                            // _rowid = 10 AND _rowid = 20
897                            //
898                            // ...which is kind of nonsensical.  Better to just return None.
899                            return None;
900                        }
901                        (Some((left_op, left_rem)), None) => {
902                            let remainder = match left_rem {
903                                // If there is a remainder on the left side we combine it.  This _should_
904                                // be something like converting (_rowid = 10 AND x > 70) AND y > 80
905                                // to (_rowid = 10) AND (x > 70 AND y > 80) which should be valid
906                                Some(expr) => Expr::and(expr, binary.right.as_ref().clone()),
907                                None => binary.right.as_ref().clone(),
908                            };
909                            return Some((left_op, Some(remainder)));
910                        }
911                        (None, Some((right_op, right_rem))) => {
912                            let remainder = match right_rem {
913                                Some(expr) => Expr::and(expr, binary.left.as_ref().clone()),
914                                None => binary.left.as_ref().clone(),
915                            };
916                            return Some((right_op, Some(remainder)));
917                        }
918                        (None, None) => {
919                            return None;
920                        }
921                    }
922                }
923                datafusion_expr::Operator::Eq => {
924                    // Check for _rowid = literal
925                    if let (Expr::Column(col), Expr::Literal(lit, _)) =
926                        (binary.left.as_ref(), binary.right.as_ref())
927                        && let Some(ScalarValue::UInt64(Some(val))) =
928                            safe_coerce_scalar(lit, &DataType::UInt64)
929                    {
930                        if col.name == ROW_ID {
931                            return Some((Self::RowIds(vec![val]), None));
932                        } else if col.name == ROW_ADDR {
933                            return Some((Self::RowAddrs(vec![val]), None));
934                        } else if col.name == ROW_OFFSET {
935                            return Some((Self::RowOffsets(vec![val]), None));
936                        }
937                    }
938                }
939                datafusion_expr::Operator::Or => {
940                    let left_take = Self::try_from_expr(&binary.left);
941                    let right_take = Self::try_from_expr(&binary.right);
942                    if let (Some(left), Some(right)) = (left_take, right_take) {
943                        if left.1.is_some() || right.1.is_some() {
944                            // This would be something like...
945                            //
946                            // (_rowid = 10 AND x > 70) OR _rowid = 20
947                            //
948                            // I don't think it's correct to convert this into a take operation
949                            // which would give us (_rowid = 10 OR _rowid = 20) AND x > 70
950                            return None;
951                        }
952                        return left.0.merge(right.0).map(|op| (op, None));
953                    }
954                }
955                _ => {}
956            }
957        } else if let Expr::InList(in_expr) = expr
958            && let Expr::Column(col) = in_expr.expr.as_ref()
959            && let Some(u64s) = Self::extract_u64_list(&in_expr.list)
960        {
961            if col.name == ROW_ID {
962                return Some((Self::RowIds(u64s), None));
963            } else if col.name == ROW_ADDR {
964                return Some((Self::RowAddrs(u64s), None));
965            } else if col.name == ROW_OFFSET {
966                return Some((Self::RowOffsets(u64s), None));
967            }
968        }
969        None
970    }
971}
972
973impl Scanner {
974    pub fn new(dataset: Arc<Dataset>) -> Self {
975        let projection_plan = ProjectionPlan::full(dataset.clone()).unwrap();
976        let file_reader_options = dataset.file_reader_options.clone();
977        let mut scanner = Self {
978            dataset,
979            projection_plan,
980            blob_handling: BlobHandling::default(),
981            prefilter: false,
982            materialization_style: MaterializationStyle::Heuristic,
983            filter: LanceFilter::default(),
984            full_text_query: None,
985            batch_size: None,
986            batch_readahead: get_num_compute_intensive_cpus(),
987            fragment_readahead: None,
988            io_buffer_size: None,
989            limit: None,
990            offset: None,
991            ordering: None,
992            nearest: None,
993            use_stats: true,
994            ordered: true,
995            fragments: None,
996            fast_search: false,
997            use_scalar_index: true,
998            include_deleted_rows: false,
999            scan_stats_callback: None,
1000            strict_batch_size: false,
1001            file_reader_options,
1002            aggregate: None,
1003            legacy_with_row_addr: false,
1004            legacy_with_row_id: false,
1005            explicit_projection: false,
1006            autoproject_scoring_columns: true,
1007        };
1008        scanner.apply_blob_handling();
1009        scanner
1010    }
1011
1012    fn apply_blob_handling(&mut self) {
1013        let projection = self
1014            .projection_plan
1015            .physical_projection
1016            .clone()
1017            .with_blob_handling(self.blob_handling.clone());
1018        self.projection_plan.physical_projection = projection;
1019    }
1020
1021    pub fn blob_handling(&mut self, blob_handling: BlobHandling) -> &mut Self {
1022        self.blob_handling = blob_handling;
1023        self.apply_blob_handling();
1024        self
1025    }
1026
1027    pub fn from_fragment(dataset: Arc<Dataset>, fragment: Fragment) -> Self {
1028        Self {
1029            fragments: Some(vec![fragment]),
1030            ..Self::new(dataset)
1031        }
1032    }
1033
1034    /// Set which fragments should be scanned.
1035    ///
1036    /// If scan_in_order is set to true, the fragments will be scanned in the order of the vector.
1037    pub fn with_fragments(&mut self, fragments: Vec<Fragment>) -> &mut Self {
1038        self.fragments = Some(fragments);
1039        self
1040    }
1041
1042    fn get_batch_size(&self) -> usize {
1043        // Default batch size to be large enough so that a i32 column can be
1044        // read in a single range request. For the object store default of
1045        // 64KB, this is 16K rows. For local file systems, the default block size
1046        // is just 4K, which would mean only 1K rows, which might be a little small.
1047        // So we use a default minimum of 8K rows.
1048        get_default_batch_size().unwrap_or_else(|| {
1049            self.batch_size.unwrap_or_else(|| {
1050                std::cmp::max(
1051                    self.dataset.object_store().block_size() / 4,
1052                    BATCH_SIZE_FALLBACK,
1053                )
1054            })
1055        })
1056    }
1057
1058    fn ensure_not_fragment_scan(&self) -> Result<()> {
1059        if self.is_fragment_scan() {
1060            Err(Error::not_supported(
1061                "This operation is not supported for fragment scan".to_string(),
1062            ))
1063        } else {
1064            Ok(())
1065        }
1066    }
1067
1068    fn is_fragment_scan(&self) -> bool {
1069        self.fragments.is_some()
1070    }
1071
1072    /// Empty Projection (useful for count queries)
1073    ///
1074    /// The row_address will be scanned (no I/O required) but not included in the output
1075    pub fn empty_project(&mut self) -> Result<&mut Self> {
1076        self.project(&[] as &[&str])
1077    }
1078
1079    /// Projection.
1080    ///
1081    /// Only select the specified columns. If not specified, all columns will be scanned.
1082    pub fn project<T: AsRef<str>>(&mut self, columns: &[T]) -> Result<&mut Self> {
1083        let transformed_columns: Vec<(&str, String)> = columns
1084            .iter()
1085            .map(|c| (c.as_ref(), escape_field_path_for_project(c.as_ref())))
1086            .collect();
1087
1088        self.project_with_transform(&transformed_columns)
1089    }
1090
1091    /// Projection with transform
1092    ///
1093    /// Only select the specified columns with the given transform.
1094    pub fn project_with_transform(
1095        &mut self,
1096        columns: &[(impl AsRef<str>, impl AsRef<str>)],
1097    ) -> Result<&mut Self> {
1098        self.explicit_projection = true;
1099        self.projection_plan = ProjectionPlan::from_expressions(self.dataset.clone(), columns)?;
1100        if self.legacy_with_row_id {
1101            self.projection_plan.include_row_id();
1102        }
1103        if self.legacy_with_row_addr {
1104            self.projection_plan.include_row_addr();
1105        }
1106        self.apply_blob_handling();
1107        Ok(self)
1108    }
1109
1110    /// Should the filter run before the vector index is applied
1111    ///
1112    /// If true then the filter will be applied before the vector index.  This
1113    /// means the results will be accurate but the overall query may be more expensive.
1114    ///
1115    /// If false then the filter will be applied to the nearest results.  This means
1116    /// you may get back fewer results than you ask for (or none at all) if the closest
1117    /// results do not match the filter.
1118    pub fn prefilter(&mut self, should_prefilter: bool) -> &mut Self {
1119        self.prefilter = should_prefilter;
1120        self
1121    }
1122
1123    /// Set the callback to be called after the scan with summary statistics
1124    pub fn scan_stats_callback(&mut self, callback: ExecutionStatsCallback) -> &mut Self {
1125        self.scan_stats_callback = Some(callback);
1126        self
1127    }
1128
1129    /// Set the materialization style for the scan
1130    ///
1131    /// This controls when columns are fetched from storage.  The default should work
1132    /// well for most cases.
1133    ///
1134    /// If you know (in advance) a query will return relatively few results (less than
1135    /// 0.1% of the rows) then you may want to experiment with applying late materialization
1136    /// to more (or all) columns.
1137    ///
1138    /// If you know a query is going to return many rows then you may want to experiment
1139    /// with applying early materialization to more (or all) columns.
1140    pub fn materialization_style(&mut self, style: MaterializationStyle) -> &mut Self {
1141        self.materialization_style = style;
1142        self
1143    }
1144
1145    /// Apply filters
1146    ///
1147    /// The filters can be presented as the string, as in WHERE clause in SQL.
1148    ///
1149    /// ```rust,ignore
1150    /// let dataset = Dataset::open(uri).await.unwrap();
1151    /// let stream = dataset.scan()
1152    ///     .project(&["col", "col2.subfield"]).unwrap()
1153    ///     .filter("a > 10 AND b < 200").unwrap()
1154    ///     .limit(10)
1155    ///     .into_stream();
1156    /// ```
1157    ///
1158    /// Once the filter is applied, Lance will create an optimized I/O plan for filtering.
1159    ///
1160    pub fn filter(&mut self, filter: &str) -> Result<&mut Self> {
1161        self.filter.expr_filter = Some(ExprFilter::Sql(filter.to_string()));
1162        Ok(self)
1163    }
1164
1165    /// Apply fts/vector query as filter.
1166    ///
1167    /// * Vector query filter can only be applied to full text search.
1168    /// * Fts query filter can only be applied to vector search.
1169    /// * Query filter couldn't be applied to normal query.
1170    ///
1171    /// ```rust,ignore
1172    /// let dataset = Dataset::open(uri).await.unwrap();
1173    /// let query_vector = Float32Array::from(vec![300f32, 300f32, 300f32, 300f32]);
1174    /// let stream = dataset.scan()
1175    ///     .nearest("vector", &query_vector, 5)
1176    ///     .project(&["col", "col2.subfield"]).unwrap()
1177    ///     .query_filter(QueryFilter::Fts(FullTextSearchQuery::new(
1178    ///       "hello".to_string(),
1179    ///     ))).unwrap()
1180    ///     .limit(10)
1181    ///     .into_stream();
1182    /// ```
1183    pub fn filter_query(&mut self, filter: QueryFilter) -> Result<&mut Self> {
1184        self.filter.query_filter = Some(filter);
1185        Ok(self)
1186    }
1187
1188    /// Filter by full text search
1189    /// The column must be a string column.
1190    /// The query is a string to search for.
1191    /// The search is case-insensitive, BM25 scoring is used.
1192    ///
1193    /// ```rust,ignore
1194    /// let dataset = Dataset::open(uri).await.unwrap();
1195    /// let stream = dataset.scan()
1196    ///    .project(&["col", "col2.subfield"]).unwrap()
1197    ///    .full_text_search("col", "query").unwrap()
1198    ///    .limit(10)
1199    ///    .into_stream();
1200    /// ```
1201    pub fn full_text_search(&mut self, query: FullTextSearchQuery) -> Result<&mut Self> {
1202        let fields = query.columns();
1203        if !fields.is_empty() {
1204            for field in fields.iter() {
1205                if self.dataset.schema().field(field).is_none() {
1206                    return Err(Error::invalid_input(format!("Column {} not found", field)));
1207                }
1208            }
1209        }
1210
1211        self.full_text_query = Some(query);
1212        Ok(self)
1213    }
1214
1215    /// Set a filter using a Substrait ExtendedExpression message
1216    ///
1217    /// The message must contain exactly one expression and that expression
1218    /// must be a scalar expression whose return type is boolean.
1219    pub fn filter_substrait(&mut self, filter: &[u8]) -> Result<&mut Self> {
1220        self.filter.expr_filter = Some(ExprFilter::Substrait(filter.to_vec()));
1221        Ok(self)
1222    }
1223
1224    pub fn filter_expr(&mut self, filter: Expr) -> &mut Self {
1225        self.filter.expr_filter = Some(ExprFilter::Datafusion(filter));
1226        self
1227    }
1228
1229    /// Set aggregation.
1230    ///
1231    /// The aggregate expression is parsed immediately using the dataset schema.
1232    /// For Substrait aggregates, this converts them to DataFusion expressions.
1233    pub fn aggregate(&mut self, aggregate: AggregateExpr) -> Result<&mut Self> {
1234        let schema: Arc<ArrowSchema> = Arc::new(self.dataset.schema().into());
1235        let parsed = aggregate.parse(schema)?;
1236        self.aggregate = Some(parsed);
1237        Ok(self)
1238    }
1239
1240    /// Set the batch size.
1241    pub fn batch_size(&mut self, batch_size: usize) -> &mut Self {
1242        self.batch_size = Some(batch_size);
1243        self
1244    }
1245
1246    /// Include deleted rows
1247    ///
1248    /// These are rows that have been deleted from the dataset but are still present in the
1249    /// underlying storage.  These rows will have the `_rowid` column set to NULL.  The other columns
1250    /// (include _rowaddr) will be set to their deleted values.
1251    ///
1252    /// This can be useful for generating aligned fragments or debugging
1253    ///
1254    /// Note: when entire fragments are deleted, the scanner will not emit any rows for that fragment
1255    /// since the fragment is no longer present in the dataset.
1256    pub fn include_deleted_rows(&mut self) -> &mut Self {
1257        self.include_deleted_rows = true;
1258        self
1259    }
1260
1261    /// Set the I/O buffer size
1262    ///
1263    /// This is the amount of RAM that will be reserved for holding I/O received from
1264    /// storage before it is processed.  This is used to control the amount of memory
1265    /// used by the scanner.  If the buffer is full then the scanner will block until
1266    /// the buffer is processed.
1267    ///
1268    /// Generally this should scale with the number of concurrent I/O threads.  The
1269    /// default is 2GiB which comfortably provides enough space for somewhere between
1270    /// 32 and 256 concurrent I/O threads.
1271    ///
1272    /// This value is not a hard cap on the amount of RAM the scanner will use.  Some
1273    /// space is used for the compute (which can be controlled by the batch size) and
1274    /// Lance does not keep track of memory after it is returned to the user.
1275    ///
1276    /// Currently, if there is a single batch of data which is larger than the io buffer
1277    /// size then the scanner will deadlock.  This is a known issue and will be fixed in
1278    /// a future release.
1279    pub fn io_buffer_size(&mut self, size: u64) -> &mut Self {
1280        self.io_buffer_size = Some(size);
1281        self
1282    }
1283
1284    /// Set the prefetch size.
1285    /// Ignored in v2 and newer format
1286    pub fn batch_readahead(&mut self, nbatches: usize) -> &mut Self {
1287        self.batch_readahead = nbatches;
1288        self
1289    }
1290
1291    /// Set the fragment readahead.
1292    ///
1293    /// This is only used if ``scan_in_order`` is set to false.
1294    pub fn fragment_readahead(&mut self, nfragments: usize) -> &mut Self {
1295        self.fragment_readahead = Some(nfragments);
1296        self
1297    }
1298
1299    /// Set whether to read data in order (default: true)
1300    ///
1301    /// A scan will always read from the disk concurrently.  If this property
1302    /// is true then a ready batch (a batch that has been read from disk) will
1303    /// only be returned if it is the next batch in the sequence.  Otherwise,
1304    /// the batch will be held until the stream catches up.  This means the
1305    /// sequence is returned in order but there may be slightly less parallelism.
1306    ///
1307    /// If this is false, then batches will be returned as soon as they are
1308    /// available, potentially increasing throughput slightly
1309    ///
1310    /// If an ordering is defined (using [Self::order_by]) then the scan will
1311    /// always scan in parallel and any value set here will be ignored.
1312    pub fn scan_in_order(&mut self, ordered: bool) -> &mut Self {
1313        self.ordered = ordered;
1314        self
1315    }
1316
1317    /// Set whether to use scalar index.
1318    ///
1319    /// By default, scalar indices will be used to optimize a query if available.
1320    /// However, in some corner cases, scalar indices may not be the best choice.
1321    /// This option allows users to disable scalar indices for a query.
1322    pub fn use_scalar_index(&mut self, use_scalar_index: bool) -> &mut Self {
1323        self.use_scalar_index = use_scalar_index;
1324        self
1325    }
1326
1327    /// Set whether to use strict batch size.
1328    ///
1329    /// If this is true then output batches (except the last batch) will have exactly `batch_size` rows.
1330    /// By default, this is False and output batches are allowed to have fewer than `batch_size` rows
1331    /// Setting this to True will require us to merge batches, incurring a data copy, for a minor performance
1332    /// penalty.
1333    pub fn strict_batch_size(&mut self, strict_batch_size: bool) -> &mut Self {
1334        self.strict_batch_size = strict_batch_size;
1335        self
1336    }
1337
1338    /// Set limit and offset.
1339    ///
1340    /// If offset is set, the first offset rows will be skipped. If limit is set,
1341    /// only the provided number of rows will be returned. These can be set
1342    /// independently. For example, setting offset to 10 and limit to None will
1343    /// skip the first 10 rows and return the rest of the rows in the dataset.
1344    pub fn limit(&mut self, limit: Option<i64>, offset: Option<i64>) -> Result<&mut Self> {
1345        if limit.unwrap_or_default() < 0 {
1346            return Err(Error::invalid_input(
1347                "Limit must be non-negative".to_string(),
1348            ));
1349        }
1350        if let Some(off) = offset
1351            && off < 0
1352        {
1353            return Err(Error::invalid_input(
1354                "Offset must be non-negative".to_string(),
1355            ));
1356        }
1357        self.limit = limit;
1358        self.offset = offset;
1359        Ok(self)
1360    }
1361
1362    /// Find k-nearest neighbor within the vector column.
1363    /// the query can be a Float16Array, Float32Array, Float64Array, UInt8Array,
1364    /// or a ListArray/FixedSizeListArray of the above types.
1365    pub fn nearest(&mut self, column: &str, q: &dyn Array, k: usize) -> Result<&mut Self> {
1366        if !self.prefilter {
1367            // We can allow fragment scan if the input to nearest is a prefilter.
1368            // The fragment scan will be performed by the prefilter.
1369            self.ensure_not_fragment_scan()?;
1370        }
1371
1372        if k == 0 {
1373            return Err(Error::invalid_input("k must be positive".to_string()));
1374        }
1375        if q.is_empty() {
1376            return Err(Error::invalid_input(
1377                "Query vector must have non-zero length".to_string(),
1378            ));
1379        }
1380        // make sure the field exists
1381        let (vector_type, element_type) = get_vector_type(self.dataset.schema(), column)?;
1382        let dim = get_vector_dim(self.dataset.schema(), column)?;
1383
1384        let q = match q.data_type() {
1385            DataType::List(_) | DataType::FixedSizeList(_, _) => {
1386                if !matches!(vector_type, DataType::List(_)) {
1387                    return Err(Error::invalid_input(format!(
1388                        "Query is multivector but column {}({})is not multivector",
1389                        column, vector_type,
1390                    )));
1391                }
1392
1393                if let Some(list_array) = q.as_list_opt::<i32>() {
1394                    for i in 0..list_array.len() {
1395                        let vec = list_array.value(i);
1396                        if vec.len() != dim {
1397                            return Err(Error::invalid_input(format!(
1398                                "query dim({}) doesn't match the column {} vector dim({})",
1399                                vec.len(),
1400                                column,
1401                                dim,
1402                            )));
1403                        }
1404                    }
1405                    list_array.values().clone()
1406                } else {
1407                    let fsl = q.as_fixed_size_list();
1408                    if fsl.value_length() as usize != dim {
1409                        return Err(Error::invalid_input(format!(
1410                            "query dim({}) doesn't match the column {} vector dim({})",
1411                            fsl.value_length(),
1412                            column,
1413                            dim,
1414                        )));
1415                    }
1416                    fsl.values().clone()
1417                }
1418            }
1419            _ => {
1420                if q.len() != dim {
1421                    return Err(Error::invalid_input(format!(
1422                        "query dim({}) doesn't match the column {} vector dim({})",
1423                        q.len(),
1424                        column,
1425                        dim,
1426                    )));
1427                }
1428                q.slice(0, q.len())
1429            }
1430        };
1431
1432        let key = match &element_type {
1433            dt if dt == q.data_type() => q,
1434            dt if dt.is_floating() => coerce_float_vector(
1435                q.as_any().downcast_ref::<Float32Array>().unwrap(),
1436                FloatType::try_from(dt)?,
1437            )?,
1438            _ => {
1439                return Err(Error::invalid_input(format!(
1440                    "Column {} has element type {} and the query vector is {}",
1441                    column,
1442                    element_type,
1443                    q.data_type(),
1444                )));
1445            }
1446        };
1447
1448        self.nearest = Some(Query {
1449            column: column.to_string(),
1450            key,
1451            k,
1452            lower_bound: None,
1453            upper_bound: None,
1454            minimum_nprobes: 1,
1455            maximum_nprobes: None,
1456            ef: None,
1457            refine_factor: None,
1458            metric_type: None,
1459            use_index: true,
1460            dist_q_c: 0.0,
1461        });
1462        Ok(self)
1463    }
1464
1465    #[cfg(test)]
1466    fn nearest_mut(&mut self) -> Option<&mut Query> {
1467        self.nearest.as_mut()
1468    }
1469
1470    /// Set the distance thresholds for the nearest neighbor search.
1471    pub fn distance_range(
1472        &mut self,
1473        lower_bound: Option<f32>,
1474        upper_bound: Option<f32>,
1475    ) -> &mut Self {
1476        if let Some(q) = self.nearest.as_mut() {
1477            q.lower_bound = lower_bound;
1478            q.upper_bound = upper_bound;
1479        }
1480        self
1481    }
1482
1483    /// Configures how many partititions will be searched in the vector index.
1484    ///
1485    /// This method is a convenience method that sets both [Self::minimum_nprobes] and
1486    /// [Self::maximum_nprobes] to the same value.
1487    pub fn nprobes(&mut self, n: usize) -> &mut Self {
1488        if let Some(q) = self.nearest.as_mut() {
1489            q.minimum_nprobes = n;
1490            q.maximum_nprobes = Some(n);
1491        } else {
1492            log::warn!("nprobes is not set because nearest has not been called yet");
1493        }
1494        self
1495    }
1496
1497    /// Configures how many partititions will be searched in the vector index.
1498    ///
1499    /// This method is a convenience method that sets both [Self::minimum_nprobes] and
1500    /// [Self::maximum_nprobes] to the same value.
1501    #[deprecated(note = "Use nprobes instead")]
1502    pub fn nprobs(&mut self, n: usize) -> &mut Self {
1503        if let Some(q) = self.nearest.as_mut() {
1504            q.minimum_nprobes = n;
1505            q.maximum_nprobes = Some(n);
1506        } else {
1507            log::warn!("nprobes is not set because nearest has not been called yet");
1508        }
1509        self
1510    }
1511
1512    /// Configures the minimum number of partitions to search in the vector index.
1513    ///
1514    /// If we have found k matching results after searching this many partitions then
1515    /// the search will stop.  Increasing this number can increase recall but will increase
1516    /// latency on all queries.
1517    ///
1518    /// The default value is 1.
1519    pub fn minimum_nprobes(&mut self, n: usize) -> &mut Self {
1520        if let Some(q) = self.nearest.as_mut() {
1521            q.minimum_nprobes = n;
1522        } else {
1523            log::warn!("minimum_nprobes is not set because nearest has not been called yet");
1524        }
1525        self
1526    }
1527
1528    /// Configures the maximum number of partitions to search in the vector index.
1529    ///
1530    /// These partitions will only be searched if we have not found `k` results after
1531    /// searching the minimum number of partitions.  Setting this to None (the default)
1532    /// will search all partitions if needed.
1533    ///
1534    /// This setting only takes effect when a prefilter is in place.  In that case we
1535    /// can spend more effort to try and find results when the filter is highly selective.
1536    ///
1537    /// If there is no prefilter, or the results are not highly selective, this value will
1538    /// have no effect.
1539    pub fn maximum_nprobes(&mut self, n: usize) -> &mut Self {
1540        if let Some(q) = self.nearest.as_mut() {
1541            q.maximum_nprobes = Some(n);
1542        } else {
1543            log::warn!("maximum_nprobes is not set because nearest has not been called yet");
1544        }
1545        self
1546    }
1547
1548    pub fn ef(&mut self, ef: usize) -> &mut Self {
1549        if let Some(q) = self.nearest.as_mut() {
1550            q.ef = Some(ef);
1551        }
1552        self
1553    }
1554
1555    /// Only search the data being indexed.
1556    ///
1557    /// Default value is false.
1558    ///
1559    /// This is essentially a weak consistency search, only on the indexed data.
1560    pub fn fast_search(&mut self) -> &mut Self {
1561        if let Some(q) = self.nearest.as_mut() {
1562            q.use_index = true;
1563        }
1564        self.fast_search = true;
1565        self.projection_plan.include_row_id(); // fast search requires _rowid
1566        self
1567    }
1568
1569    /// Apply a refine step to the vector search.
1570    ///
1571    /// A refine improves query accuracy but also makes search slower, by reading extra elements
1572    /// and using the original vector values to re-rank the distances.
1573    ///
1574    /// * `factor` - the factor of extra elements to read.  For example, if factor is 2, then
1575    ///   the search will read 2x more elements than the requested k before performing
1576    ///   the re-ranking. Note: even if the factor is 1, the  results will still be
1577    ///   re-ranked without fetching additional elements.
1578    pub fn refine(&mut self, factor: u32) -> &mut Self {
1579        if let Some(q) = self.nearest.as_mut() {
1580            q.refine_factor = Some(factor)
1581        };
1582        self
1583    }
1584
1585    /// Change the distance [MetricType], i.e, L2 or Cosine distance.
1586    pub fn distance_metric(&mut self, metric_type: MetricType) -> &mut Self {
1587        if let Some(q) = self.nearest.as_mut() {
1588            q.metric_type = Some(metric_type)
1589        }
1590        self
1591    }
1592
1593    /// Sort the results of the scan by one or more columns
1594    ///
1595    /// If Some, then the resulting stream will be sorted according to the given ordering.
1596    /// This may increase the latency of the first result since all data must be read before
1597    /// the first batch can be returned.
1598    pub fn order_by(&mut self, ordering: Option<Vec<ColumnOrdering>>) -> Result<&mut Self> {
1599        if let Some(ordering) = &ordering {
1600            if ordering.is_empty() {
1601                self.ordering = None;
1602                return Ok(self);
1603            }
1604            // Verify early that the fields exist
1605            for column in ordering {
1606                self.dataset
1607                    .schema()
1608                    .field(&column.column_name)
1609                    .ok_or(Error::invalid_input(format!(
1610                        "Column {} not found",
1611                        &column.column_name
1612                    )))?;
1613            }
1614        }
1615        self.ordering = ordering;
1616        Ok(self)
1617    }
1618
1619    /// Set whether to use the index if available
1620    pub fn use_index(&mut self, use_index: bool) -> &mut Self {
1621        if let Some(q) = self.nearest.as_mut() {
1622            q.use_index = use_index
1623        }
1624        self
1625    }
1626
1627    /// Instruct the scanner to return the `_rowid` meta column from the dataset.
1628    pub fn with_row_id(&mut self) -> &mut Self {
1629        self.legacy_with_row_id = true;
1630        self.projection_plan.include_row_id();
1631        self
1632    }
1633
1634    /// Instruct the scanner to return the `_rowaddr` meta column from the dataset.
1635    pub fn with_row_address(&mut self) -> &mut Self {
1636        self.legacy_with_row_addr = true;
1637        self.projection_plan.include_row_addr();
1638        self
1639    }
1640
1641    /// Instruct the scanner to disable automatic projection of scoring columns
1642    ///
1643    /// In the future, this will be the default behavior.  This method is useful for
1644    /// opting in to the new behavior early to avoid breaking changes (and a warning
1645    /// message)
1646    ///
1647    /// Once the default switches, the old autoprojection behavior will be removed.
1648    ///
1649    /// The autoprojection behavior (current default) includes the _score or _distance
1650    /// column even if a projection is manually specified with `[project]` or
1651    /// `[project_with_transform]`.
1652    ///
1653    /// The new behavior will only include the _score or _distance column if no projection
1654    /// is specified or if the user explicitly includes the _score or _distance column
1655    /// in the projection.
1656    pub fn disable_scoring_autoprojection(&mut self) -> &mut Self {
1657        self.autoproject_scoring_columns = false;
1658        self
1659    }
1660
1661    /// Set the file reader options to use when reading data files.
1662    pub fn with_file_reader_options(&mut self, options: FileReaderOptions) -> &mut Self {
1663        self.file_reader_options = Some(options);
1664        self
1665    }
1666
1667    /// Create a physical expression for a column that may be nested
1668    fn create_column_expr(
1669        column_name: &str,
1670        dataset: &Dataset,
1671        arrow_schema: &ArrowSchema,
1672    ) -> Result<Arc<dyn PhysicalExpr>> {
1673        let lance_schema = dataset.schema();
1674        let field_path = lance_schema
1675            .resolve_case_insensitive(column_name)
1676            .ok_or_else(|| {
1677                Error::invalid_input(format!("Field '{}' not found in schema", column_name))
1678            })?;
1679
1680        if field_path.len() == 1 {
1681            // Simple top-level column
1682            expressions::col(&field_path[0].name, arrow_schema).map_err(|e| {
1683                Error::internal(format!(
1684                    "Failed to create column expression for '{}': {}",
1685                    column_name, e
1686                ))
1687            })
1688        } else {
1689            // Nested field - build a chain of GetFieldFunc calls
1690            let get_field_func = ScalarUDF::from(GetFieldFunc::default());
1691
1692            // Use Expr::Column with Column::new_unqualified to preserve exact case
1693            // (col() normalizes identifiers to lowercase)
1694            let mut expr = Expr::Column(datafusion::common::Column::new_unqualified(
1695                &field_path[0].name,
1696            ));
1697            for nested_field in &field_path[1..] {
1698                expr = get_field_func.call(vec![expr, lit(&nested_field.name)]);
1699            }
1700
1701            // Convert logical to physical expression
1702            let df_schema = Arc::new(DFSchema::try_from(arrow_schema.clone())?);
1703            let execution_props = ExecutionProps::new().with_query_execution_start_time(Utc::now());
1704            create_physical_expr(&expr, &df_schema, &execution_props).map_err(|e| {
1705                Error::internal(format!(
1706                    "Failed to create physical expression for nested field '{}': {}",
1707                    column_name, e
1708                ))
1709            })
1710        }
1711    }
1712
1713    /// Set whether to use statistics to optimize the scan (default: true)
1714    ///
1715    /// This is used for debugging or benchmarking purposes.
1716    pub fn use_stats(&mut self, use_stats: bool) -> &mut Self {
1717        self.use_stats = use_stats;
1718        self
1719    }
1720
1721    /// The Arrow schema of the output, including projections and vector / _distance
1722    pub async fn schema(&self) -> Result<SchemaRef> {
1723        let plan = self.create_plan().await?;
1724        Ok(plan.schema())
1725    }
1726
1727    /// Fetches the currently set expr filter
1728    ///
1729    /// Note that this forces the filter to be evaluated and the result will depend on
1730    /// the current state of the scanner (e.g. if with_row_id has been called then _rowid
1731    /// will be available for filtering but not otherwise) and so you may want to call this
1732    /// after setting all other options.
1733    pub fn get_expr_filter(&self) -> Result<Option<Expr>> {
1734        if let Some(filter) = &self.filter.expr_filter {
1735            let filter_schema = self.filterable_schema()?;
1736            Ok(Some(filter.to_datafusion(
1737                self.dataset.schema(),
1738                filter_schema.as_ref(),
1739            )?))
1740        } else {
1741            Ok(None)
1742        }
1743    }
1744
1745    fn add_extra_columns(&self, schema: Schema) -> Result<Schema> {
1746        let mut extra_columns = vec![ArrowField::new(ROW_OFFSET, DataType::UInt64, true)];
1747
1748        if self.nearest.as_ref().is_some() {
1749            extra_columns.push(ArrowField::new(DIST_COL, DataType::Float32, true));
1750        };
1751
1752        if self.full_text_query.is_some() {
1753            extra_columns.push(ArrowField::new(SCORE_COL, DataType::Float32, true));
1754        }
1755
1756        schema.merge(&ArrowSchema::new(extra_columns))
1757    }
1758
1759    /// The full schema available to filters
1760    ///
1761    /// This is the schema of the dataset, any metadata columns like _rowid or _rowaddr
1762    /// and any extra columns like _distance or _score
1763    fn filterable_schema(&self) -> Result<Arc<Schema>> {
1764        let base_schema = Projection::full(self.dataset.clone())
1765            .with_row_id()
1766            .with_row_addr()
1767            .with_row_last_updated_at_version()
1768            .with_row_created_at_version()
1769            .to_schema();
1770
1771        Ok(Arc::new(self.add_extra_columns(base_schema)?))
1772    }
1773
1774    /// This takes the current output, and the user's requested projection, and calculates the
1775    /// final projection expression.
1776    ///
1777    /// This final expression may reorder columns, drop columns, or calculate new columns
1778    pub(crate) fn calculate_final_projection(
1779        &self,
1780        current_schema: &ArrowSchema,
1781    ) -> Result<Vec<(Arc<dyn PhysicalExpr>, String)>> {
1782        // Select the columns from the output schema based on the user's projection (or the list
1783        // of all available columns if the user did not specify a projection)
1784        let mut output_expr = self.projection_plan.to_physical_exprs(current_schema)?;
1785
1786        // Make sure _distance and _score are _always_ in the output unless user has opted out of the legacy
1787        // projection behavior
1788        if self.autoproject_scoring_columns {
1789            if self.nearest.is_some() && output_expr.iter().all(|(_, name)| name != DIST_COL) {
1790                if self.explicit_projection {
1791                    log::warn!(
1792                        "Deprecation warning, this behavior will change in the future. This search specified output columns but did not include `_distance`.  Currently the `_distance` column will be included.  In the future it will not.  Call `disable_scoring_autoprojection` to adopt the future behavior and avoid this warning"
1793                    );
1794                }
1795                let vector_expr = expressions::col(DIST_COL, current_schema)?;
1796                output_expr.push((vector_expr, DIST_COL.to_string()));
1797            }
1798            if self.full_text_query.is_some()
1799                && output_expr.iter().all(|(_, name)| name != SCORE_COL)
1800            {
1801                if self.explicit_projection {
1802                    log::warn!(
1803                        "Deprecation warning, this behavior will change in the future. This search specified output columns but did not include `_score`.  Currently the `_score` column will be included.  In the future it will not.  Call `disable_scoring_autoprojection` to adopt the future behavior and avoid this warning"
1804                    );
1805                }
1806                let score_expr = expressions::col(SCORE_COL, current_schema)?;
1807                output_expr.push((score_expr, SCORE_COL.to_string()));
1808            }
1809        }
1810
1811        if self.legacy_with_row_id {
1812            let row_id_pos = output_expr
1813                .iter()
1814                .position(|(_, name)| name == ROW_ID)
1815                .ok_or_else(|| {
1816                    Error::internal(
1817                        "user specified with_row_id but the _rowid column was not in the output"
1818                            .to_string(),
1819                    )
1820                })?;
1821            if row_id_pos != output_expr.len() - 1 {
1822                // Row id is not last column.  Need to rotate it to the last spot.
1823                let row_id_expr = output_expr.remove(row_id_pos);
1824                output_expr.push(row_id_expr);
1825            }
1826        }
1827
1828        if self.legacy_with_row_addr {
1829            let row_addr_pos = output_expr.iter().position(|(_, name)| name == ROW_ADDR).ok_or_else(|| {
1830                Error::internal("user specified with_row_address but the _rowaddr column was not in the output".to_string())
1831            })?;
1832            if row_addr_pos != output_expr.len() - 1 {
1833                // Row addr is not last column.  Need to rotate it to the last spot.
1834                let row_addr_expr = output_expr.remove(row_addr_pos);
1835                output_expr.push(row_addr_expr);
1836            }
1837        }
1838
1839        Ok(output_expr)
1840    }
1841
1842    /// Create a stream from the Scanner.
1843    #[instrument(skip_all)]
1844    pub fn try_into_stream(&self) -> BoxFuture<'_, Result<DatasetRecordBatchStream>> {
1845        // Future intentionally boxed here to avoid large futures on the stack
1846        async move {
1847            let plan = self.create_plan().await?;
1848
1849            Ok(DatasetRecordBatchStream::new(execute_plan(
1850                plan,
1851                LanceExecutionOptions {
1852                    batch_size: self.batch_size,
1853                    execution_stats_callback: self.scan_stats_callback.clone(),
1854                    ..Default::default()
1855                },
1856            )?))
1857        }
1858        .boxed()
1859    }
1860
1861    pub(crate) async fn try_into_dfstream(
1862        &self,
1863        mut options: LanceExecutionOptions,
1864    ) -> Result<SendableRecordBatchStream> {
1865        let plan = self.create_plan().await?;
1866
1867        // Use the scan stats callback if the user didn't set an execution stats callback
1868        if options.execution_stats_callback.is_none() {
1869            options.execution_stats_callback = self.scan_stats_callback.clone();
1870        }
1871
1872        execute_plan(plan, options)
1873    }
1874
1875    pub async fn try_into_batch(&self) -> Result<RecordBatch> {
1876        let stream = self.try_into_stream().await?;
1877        let schema = stream.schema();
1878        let batches = stream.try_collect::<Vec<_>>().await?;
1879        Ok(concat_batches(&schema, &batches)?)
1880    }
1881
1882    /// Scan and return the number of matching rows
1883    ///
1884    /// Note: calling [`Dataset::count_rows`] can be more efficient than calling this method
1885    /// especially if there is no filter.
1886    #[instrument(skip_all)]
1887    pub fn count_rows(&self) -> BoxFuture<'_, Result<u64>> {
1888        // Future intentionally boxed here to avoid large futures on the stack
1889        async move {
1890            let mut scanner = self.clone();
1891            scanner.aggregate(AggregateExpr::builder().count_star().build())?;
1892
1893            let plan = scanner.create_plan().await?;
1894            let mut stream = execute_plan(plan, LanceExecutionOptions::default())?;
1895
1896            // A count plan will always return a single batch with a single row.
1897            if let Some(first_batch) = stream.next().await {
1898                let batch = first_batch?;
1899                let array = batch
1900                    .column(0)
1901                    .as_any()
1902                    .downcast_ref::<Int64Array>()
1903                    .ok_or(Error::invalid_input(
1904                        "Count plan did not return an Int64Array".to_string(),
1905                    ))?;
1906                Ok(array.value(0) as u64)
1907            } else {
1908                Ok(0)
1909            }
1910        }
1911        .boxed()
1912    }
1913
1914    /// Create an execution plan with aggregation.
1915    ///
1916    /// Requires `aggregate()` to be called first.
1917    #[deprecated(note = "Use create_plan() instead, which now applies aggregate automatically")]
1918    pub fn create_aggregate_plan(&self) -> BoxFuture<'_, Result<Arc<dyn ExecutionPlan>>> {
1919        async move {
1920            if self.aggregate.is_none() {
1921                return Err(Error::invalid_input(
1922                    "create_aggregate_plan called but no aggregate was set",
1923                ));
1924            }
1925            // create_plan() now applies aggregate automatically when set
1926            self.create_plan().await
1927        }
1928        .boxed()
1929    }
1930
1931    async fn apply_aggregate(
1932        &self,
1933        plan: Arc<dyn ExecutionPlan>,
1934        agg: &Aggregate,
1935    ) -> Result<Arc<dyn ExecutionPlan>> {
1936        use datafusion_physical_expr::aggregate::AggregateFunctionExpr;
1937
1938        let schema = plan.schema();
1939        let df_schema = DFSchema::try_from(schema.as_ref().clone())?;
1940
1941        let group_exprs: Vec<(Arc<dyn PhysicalExpr>, String)> = agg
1942            .group_by
1943            .iter()
1944            .map(|expr| {
1945                let name = expr.schema_name().to_string();
1946                let physical_expr =
1947                    create_physical_expr(expr, &df_schema, &ExecutionProps::default())?;
1948                Ok((physical_expr, name))
1949            })
1950            .collect::<Result<_>>()?;
1951
1952        #[allow(clippy::type_complexity)]
1953        let aggr_results: Vec<(Arc<AggregateFunctionExpr>, Option<Arc<dyn PhysicalExpr>>)> = agg
1954            .aggregates
1955            .iter()
1956            .map(|expr| self.build_physical_aggregate_expr(expr, &df_schema, &schema))
1957            .collect::<Result<_>>()?;
1958
1959        let (aggr_exprs, filters): (Vec<_>, Vec<_>) = aggr_results.into_iter().unzip();
1960
1961        Ok(Arc::new(AggregateExec::try_new(
1962            AggregateMode::Single,
1963            PhysicalGroupBy::new_single(group_exprs),
1964            aggr_exprs,
1965            filters,
1966            plan,
1967            schema,
1968        )?) as Arc<dyn ExecutionPlan>)
1969    }
1970
1971    #[allow(clippy::type_complexity)]
1972    fn build_physical_aggregate_expr(
1973        &self,
1974        expr: &Expr,
1975        df_schema: &DFSchema,
1976        input_schema: &SchemaRef,
1977    ) -> Result<(
1978        Arc<datafusion_physical_expr::aggregate::AggregateFunctionExpr>,
1979        Option<Arc<dyn PhysicalExpr>>,
1980    )> {
1981        use datafusion::physical_planner::create_aggregate_expr_and_maybe_filter;
1982
1983        let coerced_expr = self.coerce_aggregate_expr(expr, df_schema)?;
1984
1985        // Note: order_by is already embedded in the AggregateFunctionExpr for ordered aggregates
1986        let (agg_expr, filter, _order_by) = create_aggregate_expr_and_maybe_filter(
1987            &coerced_expr,
1988            df_schema,
1989            input_schema.as_ref(),
1990            &ExecutionProps::default(),
1991        )?;
1992
1993        Ok((agg_expr, filter))
1994    }
1995
1996    /// Apply type coercion to aggregate arguments for UserDefined signature functions.
1997    ///
1998    /// Most aggregate functions (SUM, COUNT, MIN, MAX) have explicit type signatures that
1999    /// DataFusion handles automatically. However, some functions like AVG use UserDefined
2000    /// type signatures in the Substrait consumer, which means DataFusion doesn't know the
2001    /// expected input types and won't perform automatic coercion. We must explicitly coerce
2002    /// arguments to the types returned by `func.coerce_types()`.
2003    fn coerce_aggregate_expr(&self, expr: &Expr, schema: &DFSchema) -> Result<Expr> {
2004        Self::coerce_aggregate_expr_impl(expr, schema)
2005    }
2006
2007    fn coerce_aggregate_expr_impl(expr: &Expr, schema: &DFSchema) -> Result<Expr> {
2008        use datafusion::logical_expr::Expr;
2009        use datafusion::logical_expr::expr::AggregateFunction;
2010        use datafusion::logical_expr::type_coercion::functions::fields_with_udf;
2011
2012        match expr {
2013            Expr::AggregateFunction(agg_func) => {
2014                let func = &agg_func.func;
2015                let args = &agg_func.params.args;
2016
2017                if args.is_empty() {
2018                    return Ok(expr.clone());
2019                }
2020
2021                let current_fields: Vec<arrow_schema::FieldRef> = args
2022                    .iter()
2023                    .enumerate()
2024                    .map(|(i, e)| {
2025                        let dt = e.get_type(schema)?;
2026                        Ok(Arc::new(arrow_schema::Field::new(
2027                            format!("arg_{i}"),
2028                            dt,
2029                            true,
2030                        )))
2031                    })
2032                    .collect::<std::result::Result<_, datafusion::common::DataFusionError>>()?;
2033
2034                let coerced_fields = fields_with_udf(&current_fields, func.as_ref())?;
2035                let coerced_args: Vec<Expr> = args
2036                    .iter()
2037                    .zip(coerced_fields.iter())
2038                    .map(|(arg, target_field)| {
2039                        let arg_type = arg.get_type(schema)?;
2040                        let target_type = target_field.data_type();
2041                        if arg_type == *target_type {
2042                            Ok(arg.clone())
2043                        } else {
2044                            arg.clone().cast_to(target_type, schema)
2045                        }
2046                    })
2047                    .collect::<std::result::Result<_, _>>()?;
2048
2049                Ok(Expr::AggregateFunction(AggregateFunction::new_udf(
2050                    func.clone(),
2051                    coerced_args,
2052                    agg_func.params.distinct,
2053                    agg_func.params.filter.clone(),
2054                    agg_func.params.order_by.clone(),
2055                    agg_func.params.null_treatment,
2056                )))
2057            }
2058            Expr::Alias(alias) => {
2059                // Recursively coerce the inner expression and preserve the alias
2060                let coerced_inner = Self::coerce_aggregate_expr_impl(&alias.expr, schema)?;
2061                Ok(coerced_inner.alias(&alias.name))
2062            }
2063            other => Err(Error::invalid_input(format!(
2064                "Expected aggregate function expression, got {:?}",
2065                other.variant_name()
2066            ))),
2067        }
2068    }
2069
2070    // A "narrow" field is a field that is so small that we are better off reading the
2071    // entire column and filtering in memory rather than "take"ing the column.
2072    //
2073    // The exact threshold depends on a two factors:
2074    // 1. The number of rows returned by the filter
2075    // 2. The number of rows in the dataset
2076    // 3. The IOPS/bandwidth ratio of the storage system
2077    // 4. The size of each value in the column
2078    //
2079    // We don't (today) have a good way of knowing #1 or #4.  #2 is easy to know.  We can
2080    // combine 1 & 2 into "percentage of rows returned" but since we don't know #1 it
2081    // doesn't really help.  #3 is complex but as a rule of thumb we can use:
2082    //
2083    //   Local storage: 1 IOP for ever ten thousand bytes
2084    //   Cloud storage: 1 IOP for every million bytes
2085    //
2086    // Our current heuristic today is to assume a filter will return 0.1% of the rows in the dataset.
2087    //
2088    // This means, for cloud storage, a field is "narrow" if there are 1KB of data per row and
2089    // for local disk a field is "narrow" if there are 10 bytes of data per row.
2090    fn is_early_field(&self, field: &Field) -> bool {
2091        match self.materialization_style {
2092            MaterializationStyle::AllEarly => true,
2093            MaterializationStyle::AllLate => false,
2094            MaterializationStyle::AllEarlyExcept(ref cols) => !cols.contains(&(field.id as u32)),
2095            MaterializationStyle::Heuristic => {
2096                if field.is_blob() {
2097                    // By default, blobs are loaded as descriptions, and so should be early
2098                    //
2099                    // TODO: Once we make blob handling configurable, we should use the blob
2100                    // handling setting here.
2101                    return true;
2102                }
2103
2104                let byte_width = field.data_type().byte_width_opt();
2105                let is_cloud = self.dataset.object_store().is_cloud();
2106                if is_cloud {
2107                    byte_width.is_some_and(|bw| bw < 1000)
2108                } else {
2109                    byte_width.is_some_and(|bw| bw < 10)
2110                }
2111            }
2112        }
2113    }
2114
2115    // If we are going to filter on `filter_plan`, then which columns are so small it is
2116    // cheaper to read the entire column and filter in memory.
2117    //
2118    // Note: only add columns that we actually need to read
2119    fn calc_eager_projection(
2120        &self,
2121        filter_plan: &ExprFilterPlan,
2122        desired_projection: &Projection,
2123    ) -> Result<Projection> {
2124        // Note: We use all_columns and not refine_columns here.  If a column is covered by an index but
2125        // the user has requested it, then we do not use it for late materialization.
2126        //
2127        // Either that column is covered by an exact filter (e.g. string with bitmap/btree) and there is no
2128        // need for late materialization or that column is covered by an inexact filter (e.g. ngram) in which
2129        // case we are going to load the column anyways for the recheck.
2130        let filter_columns = filter_plan.all_columns();
2131
2132        let filter_schema = self
2133            .dataset
2134            .empty_projection()
2135            .union_columns(filter_columns, OnMissing::Error)?
2136            .into_schema();
2137
2138        // Start with the desired fields
2139        Ok(desired_projection
2140            .clone()
2141            // Subtract columns that are expensive
2142            .subtract_predicate(|f| !self.is_early_field(f))
2143            // Add back columns that we need for filtering
2144            .union_schema(&filter_schema))
2145    }
2146
2147    fn validate_options(&self) -> Result<()> {
2148        if self.include_deleted_rows && !self.projection_plan.physical_projection.with_row_id {
2149            return Err(Error::invalid_input_source(
2150                "include_deleted_rows is set but with_row_id is false".into(),
2151            ));
2152        }
2153
2154        if self.aggregate.is_some() {
2155            if self.limit.is_some() || self.offset.is_some() {
2156                return Err(Error::invalid_input_source(
2157                    "Cannot use limit/offset with aggregate. Apply limit to the result instead."
2158                        .into(),
2159                ));
2160            }
2161            if self.ordering.is_some() {
2162                return Err(Error::invalid_input_source(
2163                    "Cannot use order_by with aggregate. Apply ordering to the result instead."
2164                        .into(),
2165                ));
2166            }
2167        }
2168
2169        Ok(())
2170    }
2171
2172    async fn create_filter_plan(&self, use_scalar_index: bool) -> Result<FilterPlan> {
2173        let filter_schema = self.filterable_schema()?;
2174        let planner = Planner::new(Arc::new(filter_schema.as_ref().into()));
2175
2176        // Check expr filter
2177        let filter_plan = if let Some(filter) = self.filter.expr_filter.as_ref() {
2178            let expr = filter.to_datafusion(self.dataset.schema(), filter_schema.as_ref())?;
2179            let index_info = self.dataset.scalar_index_info().await?;
2180            let filter_plan =
2181                planner.create_filter_plan(expr.clone(), &index_info, use_scalar_index)?;
2182
2183            // This tests if any of the fragments are missing the physical_rows property (old style)
2184            // If they are then we cannot use scalar indices
2185            if filter_plan.index_query.is_some() {
2186                let fragments = if let Some(fragments) = self.fragments.as_ref() {
2187                    fragments
2188                } else {
2189                    self.dataset.fragments()
2190                };
2191                let mut has_missing_row_count = false;
2192                for frag in fragments {
2193                    if frag.physical_rows.is_none() {
2194                        has_missing_row_count = true;
2195                        break;
2196                    }
2197                }
2198                if has_missing_row_count {
2199                    // We need row counts to use scalar indices.  If we don't have them then
2200                    // fallback to a non-indexed filter
2201                    let filter_plan =
2202                        planner.create_filter_plan(expr.clone(), &index_info, false)?;
2203                    FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2204                } else {
2205                    FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2206                }
2207            } else {
2208                FilterPlan::new(self.filter.query_filter.clone(), filter_plan)
2209            }
2210        } else {
2211            FilterPlan::new(self.filter.query_filter.clone(), ExprFilterPlan::default())
2212        };
2213
2214        // Check query filter
2215        if filter_plan.query_filter.is_some()
2216            && self.nearest.is_none()
2217            && self.full_text_query.is_none()
2218        {
2219            return Err(Error::invalid_input_source(
2220                "Query filter can only be used with full text search or vector search".into(),
2221            ));
2222        }
2223        if self.nearest.is_some() && filter_plan.vector_filter().is_some() {
2224            return Err(Error::invalid_input_source(
2225                "Query filter can't be used with vector search".into(),
2226            ));
2227        }
2228        if self.full_text_query.is_some() && filter_plan.fts_filter().is_some() {
2229            return Err(Error::invalid_input_source(
2230                "Fts filter can't be used with fts search".into(),
2231            ));
2232        }
2233
2234        Ok(filter_plan)
2235    }
2236
2237    async fn get_scan_range(&self, filter_plan: &ExprFilterPlan) -> Result<Option<Range<u64>>> {
2238        if filter_plan.has_any_filter() {
2239            // If there is a filter we can't pushdown limit / offset
2240            Ok(None)
2241        } else if self.ordering.is_some() {
2242            // If there is ordering, we can't pushdown limit / offset
2243            // because we need to sort all data first before applying the limit
2244            Ok(None)
2245        } else {
2246            match (self.limit, self.offset) {
2247                (None, None) => Ok(None),
2248                (Some(limit), None) => {
2249                    let num_rows = self.dataset.count_all_rows().await? as i64;
2250                    Ok(Some(0..limit.min(num_rows) as u64))
2251                }
2252                (None, Some(offset)) => {
2253                    let num_rows = self.dataset.count_all_rows().await? as i64;
2254                    Ok(Some(offset.min(num_rows) as u64..num_rows as u64))
2255                }
2256                (Some(limit), Some(offset)) => {
2257                    let num_rows = self.dataset.count_all_rows().await? as i64;
2258                    Ok(Some(
2259                        offset.min(num_rows) as u64..(offset + limit).min(num_rows) as u64,
2260                    ))
2261                }
2262            }
2263        }
2264    }
2265
2266    /// Create [`ExecutionPlan`] for Scan.
2267    ///
2268    /// An ExecutionPlan is a graph of operators that can be executed.
2269    ///
2270    /// The following plans are supported:
2271    ///
2272    ///  - **Plain scan without filter or limits.**
2273    ///
2274    ///  ```ignore
2275    ///  Scan(projections)
2276    ///  ```
2277    ///
2278    ///  - **Scan with filter and/or limits.**
2279    ///
2280    ///  ```ignore
2281    ///  Scan(filtered_cols) -> Filter(expr)
2282    ///     -> (*LimitExec(limit, offset))
2283    ///     -> Take(remaining_cols) -> Projection()
2284    ///  ```
2285    ///
2286    ///  - **Use KNN Index (with filter and/or limits)**
2287    ///
2288    /// ```ignore
2289    /// KNNIndex() -> Take(vector) -> FlatRefine()
2290    ///     -> Take(filtered_cols) -> Filter(expr)
2291    ///     -> (*LimitExec(limit, offset))
2292    ///     -> Take(remaining_cols) -> Projection()
2293    /// ```
2294    ///
2295    /// - **Use KNN flat (brute force) with filter and/or limits**
2296    ///
2297    /// ```ignore
2298    /// Scan(vector) -> FlatKNN()
2299    ///     -> Take(filtered_cols) -> Filter(expr)
2300    ///     -> (*LimitExec(limit, offset))
2301    ///     -> Take(remaining_cols) -> Projection()
2302    /// ```
2303    ///
2304    /// In general, a plan has 5 stages:
2305    ///
2306    /// 1. Source (from dataset Scan or from index, may include prefilter)
2307    /// 2. Filter
2308    /// 3. Sort
2309    /// 4. Limit / Offset
2310    /// 5. Take remaining columns / Projection
2311    #[instrument(level = "debug", skip_all)]
2312    pub async fn create_plan(&self) -> Result<Arc<dyn ExecutionPlan>> {
2313        log::trace!("creating scanner plan");
2314        self.validate_options()?;
2315
2316        // Scalar indices are only used when prefiltering
2317        let use_scalar_index = self.use_scalar_index && (self.prefilter || self.nearest.is_none());
2318        let mut filter_plan = self.create_filter_plan(use_scalar_index).await?;
2319
2320        let mut use_limit_node = true;
2321        // Source: either a (K|A)NN search, full text search, or a (full|indexed) scan
2322        let mut plan: Arc<dyn ExecutionPlan> = match (&self.nearest, &self.full_text_query) {
2323            (Some(_), None) => self.vector_search_source(&mut filter_plan).await?,
2324            (None, Some(query)) => self.fts_search_source(&mut filter_plan, query).await?,
2325            (None, None) => {
2326                if self.projection_plan.has_output_cols()
2327                    && self.projection_plan.physical_projection.is_empty()
2328                {
2329                    // This means the user is doing something like `SELECT 1 AS foo`.  We don't support this and
2330                    // I'm not sure we should.  Users should use a full SQL API to do something like this.
2331                    //
2332                    // It's also possible we get here from `SELECT does_not_exist`
2333
2334                    // Note: even though we are just going to return an error we still want to calculate the
2335                    // final projection here.  This lets us distinguish between a user doing something like:
2336                    //
2337                    // SELECT 1 FROM t (not supported error)
2338                    // SELECT non_existent_column FROM t (column not found error)
2339                    let output_expr = self.calculate_final_projection(&ArrowSchema::empty())?;
2340                    return Err(Error::not_supported_source(format!("Scans must request at least one column.  Received only dynamic expressions: {:?}", output_expr).into()));
2341                }
2342
2343                let take_op = filter_plan
2344                    .expr_filter_plan
2345                    .full_expr
2346                    .as_ref()
2347                    .and_then(TakeOperation::try_from_expr);
2348                if let Some((take_op, remainder)) = take_op {
2349                    // If there is any remainder use it as the filter (we don't even try and combine an indexed
2350                    // search on the filter with a take as that seems excessive)
2351                    filter_plan.expr_filter_plan = remainder
2352                        .map(ExprFilterPlan::new_refine_only)
2353                        .unwrap_or(ExprFilterPlan::default());
2354                    self.take_source(take_op).await?
2355                } else {
2356                    let planned_read = self
2357                        .filtered_read_source(&mut filter_plan.expr_filter_plan)
2358                        .await?;
2359                    if planned_read.limit_pushed_down {
2360                        use_limit_node = false;
2361                    }
2362                    if planned_read.filter_pushed_down {
2363                        filter_plan.disable_refine();
2364                    }
2365                    planned_read.plan
2366                }
2367            }
2368            _ => {
2369                return Err(Error::invalid_input_source(
2370                    "Cannot have both nearest and full text search".into(),
2371                ));
2372            }
2373        };
2374
2375        // Load columns needed for filter and ordering
2376        let mut pre_filter_projection = self.dataset.empty_projection();
2377
2378        // We may need to take filter columns if we are going to refine
2379        // an indexed scan.
2380        if filter_plan.has_refine() {
2381            // It's ok for some filter columns to be missing (e.g. _rowid)
2382            pre_filter_projection = pre_filter_projection.union_columns(
2383                filter_plan.refine_columns(&self.dataset).await?,
2384                OnMissing::Ignore,
2385            )?;
2386        }
2387
2388        // TODO: Does it always make sense to take the ordering columns here?  If there is a filter then
2389        // maybe we wait until after the filter to take the ordering columns?  Maybe it would be better to
2390        // grab the ordering column in the initial scan (if it is eager) and if it isn't then we should
2391        // take it after the filtering phase, if any (we already have a take there).
2392        if let Some(ordering) = &self.ordering {
2393            pre_filter_projection = pre_filter_projection.union_columns(
2394                ordering.iter().map(|col| &col.column_name),
2395                OnMissing::Error,
2396            )?;
2397        }
2398
2399        plan = self.take(plan, pre_filter_projection)?;
2400
2401        // Filter
2402        plan = filter_plan.refine_filter(plan, self).await?;
2403
2404        // Aggregate (if set, applies aggregate and returns early)
2405        if let Some(agg) = &self.aggregate {
2406            // Take only columns needed by the aggregate, not the full projection.
2407            // For COUNT(*), this is empty. For SUM(x), this is just [x].
2408            let required_columns = agg.required_columns();
2409            let agg_projection = if required_columns.is_empty() {
2410                self.dataset.empty_projection()
2411            } else {
2412                self.dataset
2413                    .empty_projection()
2414                    .union_columns(&required_columns, OnMissing::Error)?
2415            };
2416            plan = self.take(plan, agg_projection)?;
2417            plan = self.apply_aggregate(plan, agg).await?;
2418
2419            let optimizer = get_physical_optimizer();
2420            let options = Default::default();
2421            for rule in optimizer.rules {
2422                plan = rule.optimize(plan, &options)?;
2423            }
2424
2425            return Ok(plan);
2426        }
2427
2428        // Sort
2429        if let Some(ordering) = &self.ordering {
2430            let ordering_columns = ordering.iter().map(|col| &col.column_name);
2431            let projection_with_ordering = self
2432                .dataset
2433                .empty_projection()
2434                .union_columns(ordering_columns, OnMissing::Error)?;
2435            // We haven't loaded the sort column yet so take it now
2436            plan = self.take(plan, projection_with_ordering)?;
2437            let col_exprs = ordering
2438                .iter()
2439                .map(|col| {
2440                    Ok(PhysicalSortExpr {
2441                        expr: Self::create_column_expr(
2442                            &col.column_name,
2443                            &self.dataset,
2444                            plan.schema().as_ref(),
2445                        )?,
2446                        options: SortOptions {
2447                            descending: !col.ascending,
2448                            nulls_first: col.nulls_first,
2449                        },
2450                    })
2451                })
2452                .collect::<Result<Vec<_>>>()?;
2453            plan = Arc::new(SortExec::new(
2454                LexOrdering::new(col_exprs)
2455                    .ok_or(exec_datafusion_err!("Unexpected empty sort expressions"))?,
2456                plan,
2457            ));
2458        }
2459
2460        // Limit / offset
2461        if use_limit_node && (self.limit.unwrap_or(0) > 0 || self.offset.is_some()) {
2462            plan = self.limit_node(plan);
2463        }
2464
2465        // Take remaining columns required for projection
2466        plan = self.take(plan, self.projection_plan.physical_projection.clone())?;
2467
2468        // Add system columns, if requested
2469        if self.projection_plan.must_add_row_offset {
2470            plan = Arc::new(AddRowOffsetExec::try_new(plan, self.dataset.clone()).await?);
2471        }
2472
2473        // Final projection
2474        let final_projection = self.calculate_final_projection(plan.schema().as_ref())?;
2475
2476        plan = Arc::new(DFProjectionExec::try_new(final_projection, plan)?);
2477
2478        // If requested, apply a strict batch size to the final output
2479        if self.strict_batch_size {
2480            plan = Arc::new(StrictBatchSizeExec::new(plan, self.get_batch_size()));
2481        }
2482
2483        let optimizer = get_physical_optimizer();
2484        let options = Default::default();
2485        for rule in optimizer.rules {
2486            plan = rule.optimize(plan, &options)?;
2487        }
2488
2489        Ok(plan)
2490    }
2491
2492    // Check if a filter plan references version columns
2493    fn filter_references_version_columns(&self, filter_plan: &ExprFilterPlan) -> bool {
2494        use lance_core::{ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION};
2495
2496        if let Some(refine_expr) = &filter_plan.refine_expr {
2497            let column_names = Planner::column_names_in_expr(refine_expr);
2498            for col_name in column_names {
2499                if col_name == ROW_CREATED_AT_VERSION || col_name == ROW_LAST_UPDATED_AT_VERSION {
2500                    return true;
2501                }
2502            }
2503        }
2504        false
2505    }
2506
2507    // Helper function for filtered_read
2508    //
2509    // Do not call this directly, use filtered_read instead
2510    //
2511    // First return value is the plan, second is whether the limit was pushed down
2512    async fn legacy_filtered_read(
2513        &self,
2514        filter_plan: &ExprFilterPlan,
2515        projection: Projection,
2516        make_deletions_null: bool,
2517        fragments: Option<Arc<Vec<Fragment>>>,
2518        scan_range: Option<Range<u64>>,
2519        is_prefilter: bool,
2520    ) -> Result<PlannedFilteredScan> {
2521        let fragments = fragments.unwrap_or(self.dataset.fragments().clone());
2522        let mut filter_pushed_down = false;
2523
2524        let plan: Arc<dyn ExecutionPlan> = if filter_plan.has_index_query() {
2525            if self.include_deleted_rows {
2526                return Err(Error::invalid_input_source(
2527                    "Cannot include deleted rows in a scalar indexed scan".into(),
2528                ));
2529            }
2530            self.scalar_indexed_scan(projection, filter_plan, fragments)
2531                .await
2532        } else if !is_prefilter
2533            && filter_plan.has_refine()
2534            && self.batch_size.is_none()
2535            && self.use_stats
2536            && !self.filter_references_version_columns(filter_plan)
2537        {
2538            filter_pushed_down = true;
2539            self.pushdown_scan(false, filter_plan)
2540        } else {
2541            let ordered = if self.ordering.is_some() || self.nearest.is_some() {
2542                // If we are sorting the results there is no need to scan in order
2543                false
2544            } else if projection.with_row_last_updated_at_version
2545                || projection.with_row_created_at_version
2546            {
2547                // Version columns require ordered scanning because version metadata
2548                // is indexed by position within each fragment
2549                true
2550            } else {
2551                self.ordered
2552            };
2553
2554            let projection = if let Some(refine_expr) = filter_plan.refine_expr.as_ref() {
2555                if is_prefilter {
2556                    let refine_cols = Planner::column_names_in_expr(refine_expr);
2557                    projection.union_columns(refine_cols, OnMissing::Error)?
2558                } else {
2559                    projection
2560                }
2561            } else {
2562                projection
2563            };
2564
2565            // Can't push down limit for legacy scan if there is a refine step
2566            let scan_range = if filter_plan.has_refine() {
2567                None
2568            } else {
2569                scan_range
2570            };
2571
2572            let scan = self.scan_fragments(
2573                projection.with_row_id,
2574                self.projection_plan.physical_projection.with_row_addr,
2575                self.projection_plan
2576                    .physical_projection
2577                    .with_row_last_updated_at_version,
2578                self.projection_plan
2579                    .physical_projection
2580                    .with_row_created_at_version,
2581                make_deletions_null,
2582                Arc::new(projection.to_bare_schema()),
2583                fragments,
2584                scan_range,
2585                ordered,
2586            );
2587
2588            if filter_plan.has_refine() && is_prefilter {
2589                Ok(Arc::new(LanceFilterExec::try_new(
2590                    filter_plan.refine_expr.clone().unwrap(),
2591                    scan,
2592                )?) as Arc<dyn ExecutionPlan>)
2593            } else {
2594                Ok(scan)
2595            }
2596        }?;
2597        Ok(PlannedFilteredScan {
2598            plan,
2599            limit_pushed_down: false,
2600            filter_pushed_down,
2601        })
2602    }
2603
2604    // Helper function for filtered_read
2605    //
2606    // Do not call this directly, use filtered_read instead
2607    async fn new_filtered_read(
2608        &self,
2609        filter_plan: &ExprFilterPlan,
2610        projection: Projection,
2611        make_deletions_null: bool,
2612        fragments: Option<Arc<Vec<Fragment>>>,
2613        scan_range: Option<Range<u64>>,
2614    ) -> Result<Arc<dyn ExecutionPlan>> {
2615        let mut read_options = FilteredReadOptions::basic_full_read(&self.dataset)
2616            .with_filter_plan(filter_plan.clone())
2617            .with_projection(projection);
2618
2619        if let Some(fragments) = fragments {
2620            read_options = read_options.with_fragments(fragments);
2621        }
2622
2623        if let Some(scan_range) = scan_range {
2624            read_options = read_options.with_scan_range_before_filter(scan_range)?;
2625        }
2626
2627        if let Some(batch_size) = self.batch_size {
2628            read_options = read_options.with_batch_size(batch_size as u32);
2629        }
2630
2631        if let Some(fragment_readahead) = self.fragment_readahead {
2632            read_options = read_options.with_fragment_readahead(fragment_readahead);
2633        }
2634
2635        if make_deletions_null {
2636            read_options = read_options.with_deleted_rows()?;
2637        }
2638
2639        if let Some(io_buffer_size_bytes) = self.io_buffer_size {
2640            read_options = read_options.with_io_buffer_size(io_buffer_size_bytes);
2641        }
2642
2643        let index_input = filter_plan.index_query.clone().map(|index_query| {
2644            Arc::new(ScalarIndexExec::new(self.dataset.clone(), index_query))
2645                as Arc<dyn ExecutionPlan>
2646        });
2647
2648        Ok(Arc::new(FilteredReadExec::try_new(
2649            self.dataset.clone(),
2650            read_options,
2651            index_input,
2652        )?))
2653    }
2654
2655    // Helper function for filtered read
2656    //
2657    // Delegates to legacy or new filtered read based on dataset storage version
2658    async fn filtered_read(
2659        &self,
2660        filter_plan: &ExprFilterPlan,
2661        projection: Projection,
2662        make_deletions_null: bool,
2663        fragments: Option<Arc<Vec<Fragment>>>,
2664        scan_range: Option<Range<u64>>,
2665        is_prefilter: bool,
2666    ) -> Result<PlannedFilteredScan> {
2667        // Use legacy path if dataset uses legacy storage format
2668        if self.dataset.is_legacy_storage() {
2669            self.legacy_filtered_read(
2670                filter_plan,
2671                projection,
2672                make_deletions_null,
2673                fragments,
2674                scan_range,
2675                is_prefilter,
2676            )
2677            .await
2678        } else {
2679            let limit_pushed_down = scan_range.is_some();
2680            let plan = self
2681                .new_filtered_read(
2682                    filter_plan,
2683                    projection,
2684                    make_deletions_null,
2685                    fragments,
2686                    scan_range,
2687                )
2688                .await?;
2689            Ok(PlannedFilteredScan {
2690                filter_pushed_down: true,
2691                limit_pushed_down,
2692                plan,
2693            })
2694        }
2695    }
2696
2697    fn u64s_as_take_input(&self, u64s: Vec<u64>) -> Result<Arc<dyn ExecutionPlan>> {
2698        let row_addrs = RowAddrTreeMap::from_iter(u64s);
2699        let row_addr_mask = RowAddrMask::from_allowed(row_addrs);
2700        let index_result = IndexExprResult::Exact(row_addr_mask);
2701        let fragments_covered = self.dataset.fragment_bitmap.as_ref().clone();
2702        let batch = index_result.serialize_to_arrow(&fragments_covered)?;
2703        let stream = futures::stream::once(async move { Ok(batch) });
2704        let stream = Box::pin(RecordBatchStreamAdapter::new(
2705            INDEX_EXPR_RESULT_SCHEMA.clone(),
2706            stream,
2707        ));
2708        Ok(Arc::new(OneShotExec::new(stream)))
2709    }
2710
2711    async fn take_source(&self, take_op: TakeOperation) -> Result<Arc<dyn ExecutionPlan>> {
2712        // We generally assume that late materialization does not make sense for take operations
2713        // so we can just use the physical projection
2714        let projection = self.projection_plan.physical_projection.clone();
2715
2716        let input = match take_op {
2717            TakeOperation::RowIds(ids) => self.u64s_as_take_input(ids),
2718            TakeOperation::RowAddrs(addrs) => self.u64s_as_take_input(addrs),
2719            TakeOperation::RowOffsets(offsets) => {
2720                let mut addrs =
2721                    row_offsets_to_row_addresses(self.dataset.as_ref(), &offsets).await?;
2722                addrs.retain(|addr| *addr != RowAddress::TOMBSTONE_ROW);
2723                self.u64s_as_take_input(addrs)
2724            }
2725        }?;
2726
2727        let mut filtered_read_options = FilteredReadOptions::new(projection);
2728        if let Some(fragment) = self.fragments.as_ref() {
2729            filtered_read_options =
2730                filtered_read_options.with_fragments(Arc::new(fragment.clone()));
2731        }
2732
2733        Ok(Arc::new(FilteredReadExec::try_new(
2734            self.dataset.clone(),
2735            filtered_read_options,
2736            Some(input),
2737        )?))
2738    }
2739
2740    async fn filtered_read_source(
2741        &self,
2742        filter_plan: &mut ExprFilterPlan,
2743    ) -> Result<PlannedFilteredScan> {
2744        log::trace!("source is a filtered read");
2745
2746        // Compute the effective projection based on what's actually needed.
2747        // If we have an aggregate, we only need the columns referenced by the aggregate,
2748        // not all the columns from the projection plan.
2749        let effective_projection = if let Some(agg) = &self.aggregate {
2750            let required_columns = agg.required_columns();
2751            if required_columns.is_empty() {
2752                // COUNT(*) or similar - no columns needed
2753                self.dataset.empty_projection()
2754            } else {
2755                // Aggregate needs specific columns
2756                self.dataset
2757                    .empty_projection()
2758                    .union_columns(&required_columns, OnMissing::Error)?
2759            }
2760        } else {
2761            self.projection_plan.physical_projection.clone()
2762        };
2763
2764        let mut projection = if filter_plan.has_refine() {
2765            // If the filter plan has two steps (a scalar indexed portion and a refine portion) then
2766            // it makes sense to grab cheap columns during the first step to avoid taking them for
2767            // the second step.
2768            self.calc_eager_projection(filter_plan, &effective_projection)?
2769                .with_row_id()
2770        } else {
2771            // If the filter plan only has one step then we just do a filtered read of all the
2772            // columns that the user asked for.
2773            effective_projection
2774        };
2775
2776        if projection.is_empty() {
2777            // If the user is not requesting any columns then we will scan the row address which
2778            // is cheap
2779            projection.with_row_addr = true;
2780        }
2781
2782        let scan_range = if filter_plan.is_empty() {
2783            log::trace!("pushing scan_range into filtered_read");
2784            self.get_scan_range(filter_plan).await?
2785        } else {
2786            None
2787        };
2788
2789        self.filtered_read(
2790            filter_plan,
2791            projection,
2792            self.include_deleted_rows,
2793            self.fragments.clone().map(Arc::new),
2794            scan_range,
2795            /*is_prefilter= */ false,
2796        )
2797        .await
2798    }
2799
2800    async fn fts_search_source(
2801        &self,
2802        filter_plan: &mut FilterPlan,
2803        query: &FullTextSearchQuery,
2804    ) -> Result<Arc<dyn ExecutionPlan>> {
2805        log::trace!("source is an fts search");
2806        if self.include_deleted_rows {
2807            return Err(Error::invalid_input_source(
2808                "Cannot include deleted rows in an FTS search".into(),
2809            ));
2810        }
2811
2812        // The source is an FTS search
2813        if self.prefilter {
2814            let source: Arc<dyn ExecutionPlan> = match &filter_plan.vector_filter() {
2815                Some(vector_query) => {
2816                    // Perform vector search first then rerank according to BM25 scores
2817                    let vector_plan = self
2818                        .vector_search(&filter_plan.expr_filter_plan, vector_query)
2819                        .await?;
2820                    self.fts_rerank(vector_plan, query).await?
2821                }
2822                None => self.fts(&filter_plan.expr_filter_plan, query).await?,
2823            };
2824            // If we are prefiltering then the fts node will take care of the filter
2825            filter_plan.disable_refine();
2826            Ok(source)
2827        } else {
2828            // If we are postfiltering then we can't use scalar indices for the filter
2829            // and will need to run the postfilter in memory
2830            filter_plan.make_refine_only();
2831            self.fts(&ExprFilterPlan::default(), query).await
2832        }
2833    }
2834
2835    async fn vector_search_source(
2836        &self,
2837        filter_plan: &mut FilterPlan,
2838    ) -> Result<Arc<dyn ExecutionPlan>> {
2839        if self.include_deleted_rows {
2840            return Err(Error::invalid_input_source(
2841                "Cannot include deleted rows in a nearest neighbor search".into(),
2842            ));
2843        }
2844        let Some(query) = self.nearest.as_ref() else {
2845            return Err(Error::invalid_input("No nearest query".to_string()));
2846        };
2847
2848        if self.prefilter {
2849            log::trace!("source is a vector search (prefilter)");
2850            // If we are prefiltering then the ann / knn node will take care of the filter
2851            let source: Arc<dyn ExecutionPlan> = match &filter_plan.fts_filter() {
2852                Some(fts_query) => {
2853                    let fts_plan = self.fts(&filter_plan.expr_filter_plan, fts_query).await?;
2854                    let projection = self
2855                        .dataset
2856                        .empty_projection()
2857                        .union_column(&query.column, OnMissing::Error)?;
2858                    let plan = self.take(fts_plan, projection)?;
2859
2860                    self.flat_knn(plan, query)?
2861                }
2862                None => {
2863                    self.vector_search(&filter_plan.expr_filter_plan, query)
2864                        .await?
2865                }
2866            };
2867
2868            filter_plan.disable_refine();
2869            Ok(source)
2870        } else {
2871            log::trace!("source is a vector search (postfilter)");
2872            // If we are postfiltering then we can't use scalar indices for the filter
2873            // and will need to run the postfilter in memory
2874            filter_plan.make_refine_only();
2875            self.vector_search(&ExprFilterPlan::default(), query).await
2876        }
2877    }
2878
2879    async fn fragments_covered_by_fts_leaf(
2880        &self,
2881        column: &str,
2882        accum: &mut RoaringBitmap,
2883    ) -> Result<bool> {
2884        let index = self
2885            .dataset
2886            .load_scalar_index(IndexCriteria::default().for_column(column).supports_fts())
2887            .await?;
2888        match index {
2889            Some(index) => match &index.fragment_bitmap {
2890                Some(fragmap) => {
2891                    *accum |= fragmap;
2892                    Ok(true)
2893                }
2894                None => Ok(false),
2895            },
2896            None => Ok(false),
2897        }
2898    }
2899
2900    #[async_recursion]
2901    async fn fragments_covered_by_fts_query_helper(
2902        &self,
2903        query: &FtsQuery,
2904        accum: &mut RoaringBitmap,
2905    ) -> Result<bool> {
2906        match query {
2907            FtsQuery::Match(match_query) => {
2908                self.fragments_covered_by_fts_leaf(
2909                    match_query.column.as_ref().ok_or(Error::invalid_input(
2910                        "the column must be specified in the query".to_string(),
2911                    ))?,
2912                    accum,
2913                )
2914                .await
2915            }
2916            FtsQuery::Boost(boost) => Ok(self
2917                .fragments_covered_by_fts_query_helper(&boost.negative, accum)
2918                .await?
2919                & self
2920                    .fragments_covered_by_fts_query_helper(&boost.positive, accum)
2921                    .await?),
2922            FtsQuery::MultiMatch(multi_match) => {
2923                for mq in &multi_match.match_queries {
2924                    if !self
2925                        .fragments_covered_by_fts_leaf(
2926                            mq.column.as_ref().ok_or(Error::invalid_input(
2927                                "the column must be specified in the query".to_string(),
2928                            ))?,
2929                            accum,
2930                        )
2931                        .await?
2932                    {
2933                        return Ok(false);
2934                    }
2935                }
2936                Ok(true)
2937            }
2938            FtsQuery::Phrase(phrase_query) => {
2939                self.fragments_covered_by_fts_leaf(
2940                    phrase_query.column.as_ref().ok_or(Error::invalid_input(
2941                        "the column must be specified in the query".to_string(),
2942                    ))?,
2943                    accum,
2944                )
2945                .await
2946            }
2947            FtsQuery::Boolean(bool_query) => {
2948                for query in bool_query.must.iter() {
2949                    if !self
2950                        .fragments_covered_by_fts_query_helper(query, accum)
2951                        .await?
2952                    {
2953                        return Ok(false);
2954                    }
2955                }
2956                for query in &bool_query.should {
2957                    if !self
2958                        .fragments_covered_by_fts_query_helper(query, accum)
2959                        .await?
2960                    {
2961                        return Ok(false);
2962                    }
2963                }
2964                Ok(true)
2965            }
2966        }
2967    }
2968
2969    async fn fragments_covered_by_fts_query(&self, query: &FtsQuery) -> Result<RoaringBitmap> {
2970        let all_fragments = self.get_fragments_as_bitmap();
2971
2972        let mut referenced_fragments = RoaringBitmap::new();
2973        if !self
2974            .fragments_covered_by_fts_query_helper(query, &mut referenced_fragments)
2975            .await?
2976        {
2977            // One or more indices is missing the fragment bitmap, require all fragments in prefilter
2978            Ok(all_fragments)
2979        } else {
2980            // Fragments required for prefilter is intersection of index fragments and query fragments
2981            Ok(all_fragments & referenced_fragments)
2982        }
2983    }
2984
2985    // Create an execution plan to do full text search
2986    async fn fts(
2987        &self,
2988        filter_plan: &ExprFilterPlan,
2989        query: &FullTextSearchQuery,
2990    ) -> Result<Arc<dyn ExecutionPlan>> {
2991        let columns = query.columns();
2992        let mut params = query.params();
2993        if params.limit.is_none() {
2994            let search_limit = match (self.limit, self.offset) {
2995                (Some(limit), Some(offset)) => Some((limit + offset) as usize),
2996                (Some(limit), None) => Some(limit as usize),
2997                (None, Some(_)) => None, // No limit but has offset - fetch all and let limit_node handle
2998                (None, None) => None,
2999            };
3000            params = params.with_limit(search_limit);
3001        }
3002        let query = if columns.is_empty() {
3003            // the field is not specified,
3004            // try to search over all indexed fields including nested ones
3005            let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
3006            fill_fts_query_column(&query.query, &indexed_columns, false)?
3007        } else {
3008            query.query.clone()
3009        };
3010
3011        // TODO: Could maybe walk the query here to find all the indices that will be
3012        // involved in the query to calculate a more accuarate required_fragments than
3013        // get_fragments_as_bitmap but this is safe for now.
3014        let prefilter_source = self
3015            .prefilter_source(
3016                filter_plan,
3017                self.fragments_covered_by_fts_query(&query).await?,
3018            )
3019            .await?;
3020        let fts_exec = self
3021            .plan_fts(&query, &params, filter_plan, &prefilter_source)
3022            .await?;
3023        Ok(fts_exec)
3024    }
3025
3026    async fn plan_fts(
3027        &self,
3028        query: &FtsQuery,
3029        params: &FtsSearchParams,
3030        filter_plan: &ExprFilterPlan,
3031        prefilter_source: &PreFilterSource,
3032    ) -> Result<Arc<dyn ExecutionPlan>> {
3033        let plan: Arc<dyn ExecutionPlan> = match query {
3034            FtsQuery::Match(query) => {
3035                self.plan_match_query(query, params, filter_plan, prefilter_source)
3036                    .await?
3037            }
3038            FtsQuery::Phrase(query) => {
3039                self.plan_phrase_query(query, params, prefilter_source)
3040                    .await?
3041            }
3042
3043            FtsQuery::Boost(query) => {
3044                // for boost query, we need to erase the limit so that we can find
3045                // the documents that are not in the top-k results of the positive query,
3046                // but in the final top-k results.
3047                let unlimited_params = params.clone().with_limit(None);
3048                let positive_exec = Box::pin(self.plan_fts(
3049                    &query.positive,
3050                    &unlimited_params,
3051                    filter_plan,
3052                    prefilter_source,
3053                ));
3054                let negative_exec = Box::pin(self.plan_fts(
3055                    &query.negative,
3056                    &unlimited_params,
3057                    filter_plan,
3058                    prefilter_source,
3059                ));
3060                let (positive_exec, negative_exec) =
3061                    futures::future::try_join(positive_exec, negative_exec).await?;
3062                Arc::new(BoostQueryExec::new(
3063                    query.clone(),
3064                    params.clone(),
3065                    positive_exec,
3066                    negative_exec,
3067                ))
3068            }
3069
3070            FtsQuery::MultiMatch(query) => {
3071                let mut children = Vec::with_capacity(query.match_queries.len());
3072                for match_query in &query.match_queries {
3073                    let child =
3074                        self.plan_match_query(match_query, params, filter_plan, prefilter_source);
3075                    children.push(child);
3076                }
3077                let children = futures::future::try_join_all(children).await?;
3078
3079                let schema = children[0].schema();
3080                let group_expr = vec![(
3081                    expressions::col(ROW_ID, schema.as_ref())?,
3082                    ROW_ID.to_string(),
3083                )];
3084
3085                let fts_node = UnionExec::try_new(children)?;
3086                let fts_node = Arc::new(RepartitionExec::try_new(
3087                    fts_node,
3088                    Partitioning::RoundRobinBatch(1),
3089                )?);
3090                // dedup by row_id and return the max score as final score
3091                let fts_node = Arc::new(AggregateExec::try_new(
3092                    AggregateMode::Single,
3093                    PhysicalGroupBy::new_single(group_expr),
3094                    vec![Arc::new(
3095                        datafusion_physical_expr::aggregate::AggregateExprBuilder::new(
3096                            functions_aggregate::min_max::max_udaf(),
3097                            vec![expressions::col(SCORE_COL, &schema)?],
3098                        )
3099                        .schema(schema.clone())
3100                        .alias(SCORE_COL)
3101                        .build()?,
3102                    )],
3103                    vec![None],
3104                    fts_node,
3105                    schema,
3106                )?);
3107                let sort_expr = PhysicalSortExpr {
3108                    expr: expressions::col(SCORE_COL, fts_node.schema().as_ref())?,
3109                    options: SortOptions {
3110                        descending: true,
3111                        nulls_first: false,
3112                    },
3113                };
3114
3115                Arc::new(
3116                    SortExec::new([sort_expr].into(), fts_node)
3117                        .with_fetch(self.limit.map(|l| l as usize)),
3118                )
3119            }
3120            FtsQuery::Boolean(query) => {
3121                // TODO: rewrite the query for better performance
3122
3123                // we need to remove the limit from the params,
3124                // so that we won't miss possible matches
3125                let unlimited_params = params.clone().with_limit(None);
3126
3127                // For should queries, union the results of each subquery
3128                let mut should = Vec::with_capacity(query.should.len());
3129                for subquery in &query.should {
3130                    let plan = Box::pin(self.plan_fts(
3131                        subquery,
3132                        &unlimited_params,
3133                        filter_plan,
3134                        prefilter_source,
3135                    ))
3136                    .await?;
3137                    should.push(plan);
3138                }
3139                let should = if should.is_empty() {
3140                    Arc::new(EmptyExec::new(FTS_SCHEMA.clone()))
3141                } else if should.len() == 1 {
3142                    should.pop().unwrap()
3143                } else {
3144                    let unioned = UnionExec::try_new(should)?;
3145                    Arc::new(RepartitionExec::try_new(
3146                        unioned,
3147                        Partitioning::RoundRobinBatch(1),
3148                    )?)
3149                };
3150
3151                // For must queries, inner join the results of each subquery on row_id
3152                let mut must = None;
3153                for query in &query.must {
3154                    let plan = Box::pin(self.plan_fts(
3155                        query,
3156                        &unlimited_params,
3157                        filter_plan,
3158                        prefilter_source,
3159                    ))
3160                    .await?;
3161                    if let Some(joined_plan) = must {
3162                        must = Some(Arc::new(HashJoinExec::try_new(
3163                            joined_plan,
3164                            plan,
3165                            vec![(
3166                                Arc::new(Column::new_with_schema(ROW_ID, &FTS_SCHEMA)?),
3167                                Arc::new(Column::new_with_schema(ROW_ID, &FTS_SCHEMA)?),
3168                            )],
3169                            None,
3170                            &datafusion_expr::JoinType::Inner,
3171                            None,
3172                            datafusion_physical_plan::joins::PartitionMode::CollectLeft,
3173                            NullEquality::NullEqualsNothing,
3174                        )?) as _);
3175                    } else {
3176                        must = Some(plan);
3177                    }
3178                }
3179
3180                // For must_not queries, union the results of each subquery
3181                let mut must_not = Vec::with_capacity(query.must_not.len());
3182                for query in &query.must_not {
3183                    let plan = Box::pin(self.plan_fts(
3184                        query,
3185                        &unlimited_params,
3186                        filter_plan,
3187                        prefilter_source,
3188                    ))
3189                    .await?;
3190                    must_not.push(plan);
3191                }
3192                let must_not = if must_not.is_empty() {
3193                    Arc::new(EmptyExec::new(FTS_SCHEMA.clone()))
3194                } else if must_not.len() == 1 {
3195                    must_not.pop().unwrap()
3196                } else {
3197                    let unioned = UnionExec::try_new(must_not)?;
3198                    Arc::new(RepartitionExec::try_new(
3199                        unioned,
3200                        Partitioning::RoundRobinBatch(1),
3201                    )?)
3202                };
3203
3204                if query.should.is_empty() && must.is_none() {
3205                    return Err(Error::invalid_input(
3206                        "boolean query must have at least one should/must query".to_string(),
3207                    ));
3208                }
3209
3210                Arc::new(BooleanQueryExec::new(
3211                    query.clone(),
3212                    params.clone(),
3213                    should,
3214                    must,
3215                    must_not,
3216                ))
3217            }
3218        };
3219
3220        Ok(plan)
3221    }
3222
3223    async fn plan_phrase_query(
3224        &self,
3225        query: &PhraseQuery,
3226        params: &FtsSearchParams,
3227        prefilter_source: &PreFilterSource,
3228    ) -> Result<Arc<dyn ExecutionPlan>> {
3229        let column = query.column.clone().ok_or(Error::invalid_input(
3230            "the column must be specified in the query".to_string(),
3231        ))?;
3232
3233        let index_meta = self
3234            .dataset
3235            .load_scalar_index(IndexCriteria::default().for_column(&column).supports_fts())
3236            .await?
3237            .ok_or(Error::invalid_input(format!(
3238                "No Inverted index found for column {}",
3239                column
3240            )))?;
3241
3242        let details_any =
3243            crate::index::scalar::fetch_index_details(&self.dataset, &column, &index_meta).await?;
3244        let details = details_any
3245            .as_ref()
3246            .to_msg::<lance_index::pbold::InvertedIndexDetails>()?;
3247        if !details.with_position {
3248            return Err(Error::invalid_input("position is not found but required for phrase queries, try recreating the index with position"
3249                .to_string()));
3250        }
3251
3252        Ok(Arc::new(PhraseQueryExec::new(
3253            self.dataset.clone(),
3254            query.clone(),
3255            params.clone(),
3256            prefilter_source.clone(),
3257        )))
3258    }
3259
3260    async fn plan_match_query(
3261        &self,
3262        query: &MatchQuery,
3263        params: &FtsSearchParams,
3264        filter_plan: &ExprFilterPlan,
3265        prefilter_source: &PreFilterSource,
3266    ) -> Result<Arc<dyn ExecutionPlan>> {
3267        let column = query
3268            .column
3269            .as_ref()
3270            .ok_or(Error::invalid_input(
3271                "the column must be specified in the query".to_string(),
3272            ))?
3273            .clone();
3274
3275        let index = self
3276            .dataset
3277            .load_scalar_index(IndexCriteria::default().for_column(&column).supports_fts())
3278            .await?;
3279
3280        // Get target fragments
3281        let target_fragments = self
3282            .fragments
3283            .clone()
3284            .unwrap_or_else(|| self.dataset.fragments().to_vec());
3285
3286        let (match_plan, flat_match_plan) = match &index {
3287            Some(index) => {
3288                // Get unindexed fragments and filter to target fragments
3289                let unindexed_fragments = self
3290                    .retain_target_fragments(self.dataset.unindexed_fragments(&index.name).await?);
3291
3292                // If all target fragments are unindexed, skip index entirely
3293                if unindexed_fragments.len() == target_fragments.len() {
3294                    if self.fast_search {
3295                        return Ok(Arc::new(EmptyExec::new(FTS_SCHEMA.clone())));
3296                    }
3297                    let flat_match_plan = self
3298                        .plan_flat_match_query(unindexed_fragments, query, params, filter_plan)
3299                        .await?;
3300                    return Ok(flat_match_plan);
3301                }
3302
3303                // Mixed case: use index + flat search for unindexed
3304                let match_plan: Arc<dyn ExecutionPlan> = Arc::new(MatchQueryExec::new(
3305                    self.dataset.clone(),
3306                    query.clone(),
3307                    params.clone(),
3308                    prefilter_source.clone(),
3309                ));
3310
3311                if self.fast_search || unindexed_fragments.is_empty() {
3312                    (Some(match_plan), None)
3313                } else {
3314                    let flat_match_plan = self
3315                        .plan_flat_match_query(unindexed_fragments, query, params, filter_plan)
3316                        .await?;
3317                    (Some(match_plan), Some(flat_match_plan))
3318                }
3319            }
3320            None => {
3321                if self.fast_search {
3322                    return Ok(Arc::new(EmptyExec::new(FTS_SCHEMA.clone())));
3323                }
3324                // No index: flat search all target fragments
3325                let flat_match_plan = self
3326                    .plan_flat_match_query(target_fragments.clone(), query, params, filter_plan)
3327                    .await?;
3328                (None, Some(flat_match_plan))
3329            }
3330        };
3331
3332        // Combine plans
3333        let plan = match (match_plan, flat_match_plan) {
3334            (Some(match_plan), Some(flat_match_plan)) => {
3335                let match_plan = UnionExec::try_new(vec![match_plan, flat_match_plan])?;
3336                let match_plan = Arc::new(RepartitionExec::try_new(
3337                    match_plan,
3338                    Partitioning::RoundRobinBatch(1),
3339                )?);
3340                let sort_expr = PhysicalSortExpr {
3341                    expr: expressions::col(SCORE_COL, match_plan.schema().as_ref())?,
3342                    options: SortOptions {
3343                        descending: true,
3344                        nulls_first: false,
3345                    },
3346                };
3347                Arc::new(SortExec::new([sort_expr].into(), match_plan).with_fetch(params.limit))
3348            }
3349            (Some(match_plan), None) => match_plan,
3350            (None, Some(flat_match_plan)) => flat_match_plan,
3351            (None, None) => unreachable!(),
3352        };
3353
3354        Ok(plan)
3355    }
3356
3357    /// Plan match query on unindexed fragments
3358    async fn plan_flat_match_query(
3359        &self,
3360        fragments: Vec<Fragment>,
3361        query: &MatchQuery,
3362        params: &FtsSearchParams,
3363        filter_plan: &ExprFilterPlan,
3364    ) -> Result<Arc<dyn ExecutionPlan>> {
3365        let column = query
3366            .column
3367            .as_ref()
3368            .ok_or(Error::invalid_input(
3369                "the column must be specified in the query".to_string(),
3370            ))?
3371            .clone();
3372
3373        let mut columns = vec![column];
3374        if let Some(expr) = filter_plan.full_expr.as_ref() {
3375            let filter_columns = Planner::column_names_in_expr(expr);
3376            columns.extend(filter_columns);
3377        }
3378        let flat_fts_scan_schema = Arc::new(self.dataset.schema().project(&columns).unwrap());
3379        let mut scan_node = self.scan_fragments(
3380            true,
3381            false,
3382            false,
3383            false,
3384            false,
3385            flat_fts_scan_schema,
3386            Arc::new(fragments),
3387            None,
3388            false,
3389        );
3390
3391        if let Some(expr) = filter_plan.full_expr.as_ref() {
3392            // If there is a prefilter we need to manually apply it to the new data
3393            scan_node = Arc::new(LanceFilterExec::try_new(expr.clone(), scan_node)?);
3394        }
3395
3396        let flat_match_plan = Arc::new(FlatMatchQueryExec::new(
3397            self.dataset.clone(),
3398            query.clone(),
3399            params.clone(),
3400            scan_node,
3401        ));
3402        Ok(flat_match_plan)
3403    }
3404
3405    // ANN/KNN search execution node with optional prefilter
3406    async fn vector_search(
3407        &self,
3408        filter_plan: &ExprFilterPlan,
3409        q: &Query,
3410    ) -> Result<Arc<dyn ExecutionPlan>> {
3411        let mut q = q.clone();
3412
3413        // Sanity check
3414        let (vector_type, element_type) = get_vector_type(self.dataset.schema(), &q.column)?;
3415
3416        let column_id = self.dataset.schema().field_id(q.column.as_str())?;
3417        let use_index = q.use_index;
3418        let indices = if use_index {
3419            self.dataset.load_indices().await?
3420        } else {
3421            Arc::new(vec![])
3422        };
3423        // Find an index for the column and check if metric is compatible
3424        let matching_index = if let Some(index) =
3425            indices.iter().find(|i| i.fields.contains(&column_id))
3426        {
3427            // TODO: Once we do https://github.com/lance-format/lance/issues/5231, we
3428            // should be able to get the metric type directly from the index metadata,
3429            // at least for newer indexes.
3430            let idx = self
3431                .dataset
3432                .open_vector_index(
3433                    q.column.as_str(),
3434                    &index.uuid.to_string(),
3435                    &NoOpMetricsCollector,
3436                )
3437                .await?;
3438            let index_metric = idx.metric_type();
3439
3440            // Check if user's requested metric is compatible with index
3441            let use_this_index = match q.metric_type {
3442                Some(user_metric) => {
3443                    if user_metric == index_metric {
3444                        true
3445                    } else {
3446                        log::warn!(
3447                            "Requested metric {:?} is incompatible with index metric {:?}, falling back to brute-force search",
3448                            user_metric,
3449                            index_metric
3450                        );
3451                        false
3452                    }
3453                }
3454                None => true, // No preference, use index's metric
3455            };
3456
3457            if use_this_index {
3458                Some((index, idx, index_metric))
3459            } else {
3460                None
3461            }
3462        } else {
3463            None
3464        };
3465
3466        // Only return index and deltas if there is an index on the column and at least one of the target fragments are indexed
3467        let index_and_deltas = if let Some((index, _idx, index_metric)) = matching_index {
3468            let deltas = self.dataset.load_indices_by_name(&index.name).await?;
3469            let index_frags = self.get_indexed_frags(&deltas);
3470            if !index_frags.is_empty() {
3471                Some((index, deltas, index_metric))
3472            } else {
3473                None
3474            }
3475        } else {
3476            None
3477        };
3478
3479        if let Some((index, deltas, index_metric)) = index_and_deltas {
3480            log::trace!("index found for vector search");
3481            // Use the index's metric type
3482            q.metric_type = Some(index_metric);
3483            validate_distance_type_for(index_metric, &element_type)?;
3484
3485            if matches!(q.refine_factor, Some(0)) {
3486                return Err(Error::invalid_input(
3487                    "Refine factor cannot be zero".to_string(),
3488                ));
3489            }
3490            let ann_node = match vector_type {
3491                DataType::FixedSizeList(_, _) => self.ann(&q, &deltas, filter_plan).await?,
3492                DataType::List(_) => self.multivec_ann(&q, &deltas, filter_plan).await?,
3493                _ => unreachable!(),
3494            };
3495
3496            let mut knn_node = if q.refine_factor.is_some() {
3497                let vector_projection = self
3498                    .dataset
3499                    .empty_projection()
3500                    .union_column(&q.column, OnMissing::Error)
3501                    .unwrap();
3502                let knn_node_with_vector = self.take(ann_node, vector_projection)?;
3503                self.flat_knn(knn_node_with_vector, &q)?
3504            } else {
3505                ann_node
3506            }; // vector, _distance, _rowid
3507
3508            if !self.fast_search {
3509                knn_node = self.knn_combined(&q, index, knn_node, filter_plan).await?;
3510            }
3511
3512            Ok(knn_node)
3513        } else {
3514            if self.fast_search {
3515                return Ok(Arc::new(EmptyExec::new(KNN_INDEX_SCHEMA.clone())));
3516            }
3517            // Resolve metric type for flat search (use default if not specified)
3518            let metric = q
3519                .metric_type
3520                .unwrap_or_else(|| default_distance_type_for(&element_type));
3521            q.metric_type = Some(metric);
3522            validate_distance_type_for(metric, &element_type)?;
3523            // No index found. use flat search.
3524            let mut columns = vec![q.column.clone()];
3525            if let Some(refine_expr) = filter_plan.refine_expr.as_ref() {
3526                columns.extend(Planner::column_names_in_expr(refine_expr));
3527            }
3528            let mut vector_scan_projection = self
3529                .dataset
3530                .empty_projection()
3531                .with_row_id()
3532                .union_columns(&columns, OnMissing::Error)?;
3533
3534            vector_scan_projection.with_row_addr =
3535                self.projection_plan.physical_projection.with_row_addr;
3536
3537            let PlannedFilteredScan { mut plan, .. } = self
3538                .filtered_read(
3539                    filter_plan,
3540                    vector_scan_projection,
3541                    /*include_deleted_rows=*/ true,
3542                    self.fragments.clone().map(Arc::new),
3543                    None,
3544                    /*is_prefilter= */ true,
3545                )
3546                .await?;
3547
3548            if let Some(refine_expr) = &filter_plan.refine_expr {
3549                plan = Arc::new(LanceFilterExec::try_new(refine_expr.clone(), plan)?);
3550            }
3551            Ok(self.flat_knn(plan, &q)?)
3552        }
3553    }
3554
3555    /// Combine ANN results with KNN results for data appended after index creation
3556    async fn knn_combined(
3557        &self,
3558        q: &Query,
3559        index: &IndexMetadata,
3560        mut knn_node: Arc<dyn ExecutionPlan>,
3561        filter_plan: &ExprFilterPlan,
3562    ) -> Result<Arc<dyn ExecutionPlan>> {
3563        // Get unindexed fragments and filter to target fragments
3564        let unindexed_fragments =
3565            self.retain_target_fragments(self.dataset.unindexed_fragments(&index.name).await?);
3566
3567        if !unindexed_fragments.is_empty() {
3568            // need to set the metric type to be the same as the index
3569            // to make sure the distance is comparable.
3570            let idx = self
3571                .dataset
3572                .open_vector_index(
3573                    q.column.as_str(),
3574                    &index.uuid.to_string(),
3575                    &NoOpMetricsCollector,
3576                )
3577                .await?;
3578            let mut q = q.clone();
3579            q.metric_type = Some(idx.metric_type());
3580
3581            // If the vector column is not present, we need to take the vector column, so
3582            // that the distance value is comparable with the flat search ones.
3583            if knn_node.schema().column_with_name(&q.column).is_none() {
3584                let vector_projection = self
3585                    .dataset
3586                    .empty_projection()
3587                    .union_column(&q.column, OnMissing::Error)
3588                    .unwrap();
3589                knn_node = self.take(knn_node, vector_projection)?;
3590            }
3591
3592            let mut columns = vec![q.column.clone()];
3593            if let Some(expr) = filter_plan.full_expr.as_ref() {
3594                let filter_columns = Planner::column_names_in_expr(expr);
3595                columns.extend(filter_columns);
3596            }
3597            let vector_scan_projection = Arc::new(self.dataset.schema().project(&columns).unwrap());
3598            // Note: we could try and use the scalar indices here to reduce the scope of this scan but the
3599            // most common case is that fragments that are newer than the vector index are going to be newer
3600            // than the scalar indices anyways
3601            let mut scan_node = self.scan_fragments(
3602                true,
3603                false,
3604                false,
3605                false,
3606                false,
3607                vector_scan_projection,
3608                Arc::new(unindexed_fragments),
3609                // Can't pushdown limit/offset in an ANN search
3610                None,
3611                // We are re-ordering anyways, so no need to get data in data
3612                // in a deterministic order.
3613                false,
3614            );
3615
3616            if let Some(expr) = filter_plan.full_expr.as_ref() {
3617                // If there is a prefilter we need to manually apply it to the new data
3618                scan_node = Arc::new(LanceFilterExec::try_new(expr.clone(), scan_node)?);
3619            }
3620            // first we do flat search on just the new data
3621            let topk_appended = self.flat_knn(scan_node, &q)?;
3622
3623            // To do a union, we need to make the schemas match. Right now
3624            // knn_node: _distance, _rowid, vector
3625            // topk_appended: vector, <filter columns?>, _rowid, _distance
3626            let topk_appended = project(topk_appended, knn_node.schema().as_ref())?;
3627            assert!(
3628                topk_appended
3629                    .schema()
3630                    .equivalent_names_and_types(&knn_node.schema())
3631            );
3632            // union
3633            let unioned = UnionExec::try_new(vec![Arc::new(topk_appended), knn_node])?;
3634            // Enforce only 1 partition.
3635            let unioned = RepartitionExec::try_new(
3636                unioned,
3637                datafusion::physical_plan::Partitioning::RoundRobinBatch(1),
3638            )?;
3639            // then we do a flat search on KNN(new data) + ANN(indexed data)
3640            return self.flat_knn(Arc::new(unioned), &q);
3641        }
3642
3643        Ok(knn_node)
3644    }
3645
3646    #[async_recursion]
3647    async fn fragments_covered_by_index_query(
3648        &self,
3649        index_expr: &ScalarIndexExpr,
3650    ) -> Result<RoaringBitmap> {
3651        match index_expr {
3652            ScalarIndexExpr::And(lhs, rhs) => {
3653                Ok(self.fragments_covered_by_index_query(lhs).await?
3654                    & self.fragments_covered_by_index_query(rhs).await?)
3655            }
3656            ScalarIndexExpr::Or(lhs, rhs) => Ok(self.fragments_covered_by_index_query(lhs).await?
3657                & self.fragments_covered_by_index_query(rhs).await?),
3658            ScalarIndexExpr::Not(expr) => self.fragments_covered_by_index_query(expr).await,
3659            ScalarIndexExpr::Query(search) => {
3660                let idx = self
3661                    .dataset
3662                    .load_scalar_index(IndexCriteria::default().with_name(&search.index_name))
3663                    .await?
3664                    .expect("Index not found even though it must have been found earlier");
3665                Ok(idx
3666                    .fragment_bitmap
3667                    .expect("scalar indices should always have a fragment bitmap"))
3668            }
3669        }
3670    }
3671
3672    /// Given an index query, split the fragments into two sets
3673    ///
3674    /// The first set is the relevant fragments, which are covered by ALL indices in the query
3675    /// The second set is the missing fragments, which are missed by at least one index
3676    ///
3677    /// There is no point in handling the case where a fragment is covered by some (but not all)
3678    /// of the indices.  If we have to do a full scan of the fragment then we do it
3679    async fn partition_frags_by_coverage(
3680        &self,
3681        index_expr: &ScalarIndexExpr,
3682        fragments: Arc<Vec<Fragment>>,
3683    ) -> Result<(Vec<Fragment>, Vec<Fragment>)> {
3684        let covered_frags = self.fragments_covered_by_index_query(index_expr).await?;
3685        let mut relevant_frags = Vec::with_capacity(fragments.len());
3686        let mut missing_frags = Vec::with_capacity(fragments.len());
3687        for fragment in fragments.iter() {
3688            if covered_frags.contains(fragment.id as u32) {
3689                relevant_frags.push(fragment.clone());
3690            } else {
3691                missing_frags.push(fragment.clone());
3692            }
3693        }
3694        Ok((relevant_frags, missing_frags))
3695    }
3696
3697    // First perform a lookup in a scalar index for ids and then perform a take on the
3698    // target fragments with those ids
3699    async fn scalar_indexed_scan(
3700        &self,
3701        projection: Projection,
3702        filter_plan: &ExprFilterPlan,
3703        fragments: Arc<Vec<Fragment>>,
3704    ) -> Result<Arc<dyn ExecutionPlan>> {
3705        log::trace!("scalar indexed scan");
3706        // One or more scalar indices cover this data and there is a filter which is
3707        // compatible with the indices.  Use that filter to perform a take instead of
3708        // a full scan.
3709
3710        // If this unwrap fails we have a bug because we shouldn't be using this function unless we've already
3711        // checked that there is an index query
3712        let index_expr = filter_plan.index_query.as_ref().unwrap();
3713
3714        let needs_recheck = index_expr.needs_recheck();
3715
3716        // Figure out which fragments are covered by ALL indices
3717        let (relevant_frags, missing_frags) = self
3718            .partition_frags_by_coverage(index_expr, fragments)
3719            .await?;
3720
3721        let mut plan: Arc<dyn ExecutionPlan> = Arc::new(MaterializeIndexExec::new(
3722            self.dataset.clone(),
3723            index_expr.clone(),
3724            Arc::new(relevant_frags),
3725        ));
3726
3727        let refine_expr = filter_plan.refine_expr.as_ref();
3728
3729        // If all we want is the row ids then we can skip the take.  However, if there is a refine
3730        // or a recheck then we still need to do a take because we need filter columns.
3731        let needs_take =
3732            needs_recheck || projection.has_data_fields() || filter_plan.refine_expr.is_some();
3733        if needs_take {
3734            let mut take_projection = projection.clone();
3735            if needs_recheck {
3736                // If we need to recheck then we need to also take the columns used for the filter
3737                let filter_expr = index_expr.to_expr();
3738                let filter_cols = Planner::column_names_in_expr(&filter_expr);
3739                take_projection = take_projection.union_columns(filter_cols, OnMissing::Error)?;
3740            }
3741            if let Some(refine_expr) = refine_expr {
3742                let refine_cols = Planner::column_names_in_expr(refine_expr);
3743                take_projection = take_projection.union_columns(refine_cols, OnMissing::Error)?;
3744            }
3745            log::trace!("need to take additional columns for scalar_indexed_scan");
3746            plan = self.take(plan, take_projection)?;
3747        }
3748
3749        let post_take_filter = match (needs_recheck, refine_expr) {
3750            (false, None) => None,
3751            (true, None) => {
3752                // If we need to recheck then we need to apply the filter to the results
3753                Some(index_expr.to_expr())
3754            }
3755            (true, Some(_)) => Some(filter_plan.full_expr.as_ref().unwrap().clone()),
3756            (false, Some(refine_expr)) => Some(refine_expr.clone()),
3757        };
3758
3759        if let Some(post_take_filter) = post_take_filter {
3760            let planner = Planner::new(plan.schema());
3761            let optimized_filter = planner.optimize_expr(post_take_filter)?;
3762
3763            log::trace!("applying post-take filter to indexed scan");
3764            plan = Arc::new(LanceFilterExec::try_new(optimized_filter, plan)?);
3765        }
3766
3767        if self.projection_plan.physical_projection.with_row_addr {
3768            plan = Arc::new(AddRowAddrExec::try_new(plan, self.dataset.clone(), 0)?);
3769        }
3770
3771        let new_data_path: Option<Arc<dyn ExecutionPlan>> = if !missing_frags.is_empty() {
3772            log::trace!(
3773                "scalar_indexed_scan will need full scan of {} missing fragments",
3774                missing_frags.len()
3775            );
3776
3777            // If there is new data then we need this:
3778            //
3779            // MaterializeIndexExec(old_frags) -> Take -> Union
3780            // Scan(new_frags) -> Filter -> Project    -|
3781            //
3782            // The project is to drop any columns we had to include
3783            // in the full scan merely for the sake of fulfilling the
3784            // filter.
3785            //
3786            // If there were no extra columns then we still need the project
3787            // because Materialize -> Take puts the row id at the left and
3788            // Scan puts the row id at the right
3789            let filter = filter_plan.full_expr.as_ref().unwrap();
3790            let filter_cols = Planner::column_names_in_expr(filter);
3791            let scan_projection = projection.union_columns(filter_cols, OnMissing::Error)?;
3792
3793            let scan_schema = Arc::new(scan_projection.to_bare_schema());
3794            let scan_arrow_schema = Arc::new(scan_schema.as_ref().into());
3795            let planner = Planner::new(scan_arrow_schema);
3796            let optimized_filter = planner.optimize_expr(filter.clone())?;
3797
3798            let new_data_scan = self.scan_fragments(
3799                true,
3800                self.projection_plan.physical_projection.with_row_addr,
3801                self.projection_plan
3802                    .physical_projection
3803                    .with_row_last_updated_at_version,
3804                self.projection_plan
3805                    .physical_projection
3806                    .with_row_created_at_version,
3807                false,
3808                scan_schema,
3809                missing_frags.into(),
3810                // No pushdown of limit/offset when doing scalar indexed scan
3811                None,
3812                false,
3813            );
3814            let filtered = Arc::new(LanceFilterExec::try_new(optimized_filter, new_data_scan)?);
3815            Some(Arc::new(project(filtered, plan.schema().as_ref())?))
3816        } else {
3817            log::trace!("scalar_indexed_scan will not need full scan of any missing fragments");
3818            None
3819        };
3820
3821        if let Some(new_data_path) = new_data_path {
3822            let unioned = UnionExec::try_new(vec![plan, new_data_path])?;
3823            // Enforce only 1 partition.
3824            let unioned = Arc::new(RepartitionExec::try_new(
3825                unioned,
3826                datafusion::physical_plan::Partitioning::RoundRobinBatch(1),
3827            )?);
3828            Ok(unioned)
3829        } else {
3830            Ok(plan)
3831        }
3832    }
3833
3834    fn get_io_buffer_size(&self) -> u64 {
3835        self.io_buffer_size.unwrap_or(*DEFAULT_IO_BUFFER_SIZE)
3836    }
3837
3838    /// Create an Execution plan with a scan node
3839    ///
3840    /// Setting `with_make_deletions_null` will use the validity of the _rowid
3841    /// column as a selection vector. Read more in [crate::io::FileReader].
3842    #[allow(clippy::too_many_arguments)]
3843    pub(crate) fn scan(
3844        &self,
3845        with_row_id: bool,
3846        with_row_address: bool,
3847        with_row_last_updated_at_version: bool,
3848        with_row_created_at_version: bool,
3849        with_make_deletions_null: bool,
3850        range: Option<Range<u64>>,
3851        projection: Arc<Schema>,
3852    ) -> Arc<dyn ExecutionPlan> {
3853        let fragments = if let Some(fragment) = self.fragments.as_ref() {
3854            Arc::new(fragment.clone())
3855        } else {
3856            self.dataset.fragments().clone()
3857        };
3858        let ordered = if self.ordering.is_some() || self.nearest.is_some() {
3859            // If we are sorting the results there is no need to scan in order
3860            false
3861        } else {
3862            self.ordered
3863        };
3864        self.scan_fragments(
3865            with_row_id,
3866            with_row_address,
3867            with_row_last_updated_at_version,
3868            with_row_created_at_version,
3869            with_make_deletions_null,
3870            projection,
3871            fragments,
3872            range,
3873            ordered,
3874        )
3875    }
3876
3877    #[allow(clippy::too_many_arguments)]
3878    fn scan_fragments(
3879        &self,
3880        with_row_id: bool,
3881        with_row_address: bool,
3882        with_row_last_updated_at_version: bool,
3883        with_row_created_at_version: bool,
3884        with_make_deletions_null: bool,
3885        projection: Arc<Schema>,
3886        fragments: Arc<Vec<Fragment>>,
3887        range: Option<Range<u64>>,
3888        ordered: bool,
3889    ) -> Arc<dyn ExecutionPlan> {
3890        log::trace!("scan_fragments covered {} fragments", fragments.len());
3891        let config = LanceScanConfig {
3892            batch_size: self.get_batch_size(),
3893            batch_readahead: self.batch_readahead,
3894            fragment_readahead: self.fragment_readahead,
3895            io_buffer_size: self.get_io_buffer_size(),
3896            with_row_id,
3897            with_row_address,
3898            with_row_last_updated_at_version,
3899            with_row_created_at_version,
3900            with_make_deletions_null,
3901            ordered_output: ordered,
3902        };
3903        Arc::new(LanceScanExec::new(
3904            self.dataset.clone(),
3905            fragments,
3906            range,
3907            projection,
3908            config,
3909        ))
3910    }
3911
3912    fn pushdown_scan(
3913        &self,
3914        make_deletions_null: bool,
3915        filter_plan: &ExprFilterPlan,
3916    ) -> Result<Arc<dyn ExecutionPlan>> {
3917        log::trace!("pushdown_scan");
3918
3919        let config = ScanConfig {
3920            batch_readahead: self.batch_readahead,
3921            fragment_readahead: self
3922                .fragment_readahead
3923                .unwrap_or(LEGACY_DEFAULT_FRAGMENT_READAHEAD),
3924            with_row_id: self.projection_plan.physical_projection.with_row_id,
3925            with_row_address: self.projection_plan.physical_projection.with_row_addr,
3926            make_deletions_null,
3927            ordered_output: self.ordered,
3928            file_reader_options: self
3929                .file_reader_options
3930                .clone()
3931                .or_else(|| self.dataset.file_reader_options.clone()),
3932        };
3933
3934        let fragments = if let Some(fragment) = self.fragments.as_ref() {
3935            Arc::new(fragment.clone())
3936        } else {
3937            self.dataset.fragments().clone()
3938        };
3939
3940        Ok(Arc::new(LancePushdownScanExec::try_new(
3941            self.dataset.clone(),
3942            fragments,
3943            Arc::new(self.projection_plan.physical_projection.to_bare_schema()),
3944            filter_plan.refine_expr.clone().unwrap(),
3945            config,
3946        )?))
3947    }
3948
3949    /// Here we use a full text search as a post-filter.  Any rows that
3950    /// do not contain at least one query token are removed.
3951    ///
3952    /// Only valid (currently) for match queries.
3953    async fn flat_fts_filter(
3954        &self,
3955        input: Arc<dyn ExecutionPlan>,
3956        q: &FullTextSearchQuery,
3957    ) -> Result<Arc<dyn ExecutionPlan>> {
3958        let fts_query = if q.columns().is_empty() {
3959            let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
3960            fill_fts_query_column(&q.query, &indexed_columns, false)?
3961        } else {
3962            q.query.clone()
3963        };
3964
3965        match &fts_query {
3966            FtsQuery::Match(match_query) => {
3967                let schema = Arc::new((input.schema()).try_with_column(SCORE_FIELD.clone())?);
3968
3969                let column = match_query
3970                    .column
3971                    .as_ref()
3972                    .ok_or(Error::invalid_input(
3973                        "the column must be specified in the query".to_string(),
3974                    ))?
3975                    .clone();
3976                let input = if schema.column_with_name(&column).is_none() {
3977                    let projection = self
3978                        .dataset
3979                        .empty_projection()
3980                        .union_column(&column, OnMissing::Error)?;
3981                    self.take(input, projection)?
3982                } else {
3983                    input
3984                };
3985
3986                Ok(Arc::new(FlatMatchFilterExec::new(
3987                    input,
3988                    self.dataset.clone(),
3989                    match_query.clone(),
3990                    q.params(),
3991                )))
3992            }
3993            _ => Err(Error::not_supported(
3994                "Only Match queries are supported currently when using FTS as a post-filter",
3995            )),
3996        }
3997    }
3998
3999    /// Here we consume all input (as unindexed) and rerank according to BM25 scores
4000    ///
4001    /// If there is an index on the column then we still use the index to determine the
4002    /// tokenizer and inform the BM25 scoring (e.g. avg doc length, token frequency, etc.)
4003    async fn fts_rerank(
4004        &self,
4005        input: Arc<dyn ExecutionPlan>,
4006        q: &FullTextSearchQuery,
4007    ) -> Result<Arc<dyn ExecutionPlan>> {
4008        let fts_query = if q.columns().is_empty() {
4009            let indexed_columns = fts_indexed_columns(self.dataset.clone()).await?;
4010            fill_fts_query_column(&q.query, &indexed_columns, false)?
4011        } else {
4012            q.query.clone()
4013        };
4014
4015        match &fts_query {
4016            FtsQuery::Match(match_query) => {
4017                let schema = Arc::new((input.schema()).try_with_column(SCORE_FIELD.clone())?);
4018
4019                let column = match_query
4020                    .column
4021                    .as_ref()
4022                    .ok_or(Error::invalid_input(
4023                        "the column must be specified in the query".to_string(),
4024                    ))?
4025                    .clone();
4026                let input = if schema.column_with_name(&column).is_none() {
4027                    let projection = self
4028                        .dataset
4029                        .empty_projection()
4030                        .union_column(&column, OnMissing::Error)?;
4031                    self.take(input, projection)?
4032                } else {
4033                    input
4034                };
4035
4036                Ok(Arc::new(FlatMatchQueryExec::new(
4037                    self.dataset.clone(),
4038                    match_query.clone(),
4039                    q.params(),
4040                    input,
4041                )))
4042            }
4043            _ => {
4044                let default_filter = ExprFilterPlan::default();
4045                let fts_plan = self.fts(&default_filter, q).await?;
4046
4047                let vector_row_id = Column::new_with_schema(ROW_ID, input.schema().as_ref())?;
4048                let fts_row_id = Column::new_with_schema(ROW_ID, fts_plan.schema().as_ref())?;
4049                let join = HashJoinExec::try_new(
4050                    input,
4051                    fts_plan,
4052                    vec![(Arc::new(vector_row_id), Arc::new(fts_row_id))],
4053                    None,
4054                    &JoinType::Inner,
4055                    None,
4056                    PartitionMode::CollectLeft,
4057                    NullEquality::NullEqualsNull,
4058                )?;
4059
4060                let schema = join.schema();
4061                let mut projection_exprs = Vec::new();
4062                let mut contain_rowid = false;
4063                for field in schema.fields() {
4064                    if field.name() == ROW_ID {
4065                        if contain_rowid {
4066                            continue;
4067                        }
4068                        contain_rowid = true;
4069                    }
4070                    projection_exprs.push((
4071                        Arc::new(Column::new_with_schema(field.name(), schema.as_ref())?)
4072                            as Arc<dyn PhysicalExpr>,
4073                        field.name().clone(),
4074                    ));
4075                }
4076
4077                let projection_exec = ProjectionExec::try_new(projection_exprs, Arc::new(join))?;
4078                Ok(Arc::new(projection_exec))
4079            }
4080        }
4081    }
4082
4083    /// Add a knn search node to the input plan
4084    fn flat_knn(&self, input: Arc<dyn ExecutionPlan>, q: &Query) -> Result<Arc<dyn ExecutionPlan>> {
4085        // Resolve metric_type if not set (use default for the column's element type)
4086        let metric_type = match q.metric_type {
4087            Some(m) => m,
4088            None => {
4089                let (_, element_type) = get_vector_type(self.dataset.schema(), &q.column)?;
4090                default_distance_type_for(&element_type)
4091            }
4092        };
4093        let flat_dist = Arc::new(KNNVectorDistanceExec::try_new(
4094            input,
4095            &q.column,
4096            q.key.clone(),
4097            metric_type,
4098        )?);
4099
4100        let lower: Option<(Expr, Arc<dyn PhysicalExpr>)> = q
4101            .lower_bound
4102            .map(|v| -> Result<(Expr, Arc<dyn PhysicalExpr>)> {
4103                let logical = col(DIST_COL).gt_eq(lit(v));
4104                let schema = flat_dist.schema();
4105                let df_schema = DFSchema::try_from(schema)?;
4106                let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4107                Ok::<(Expr, Arc<dyn PhysicalExpr>), _>((logical, physical))
4108            })
4109            .transpose()?;
4110
4111        let upper = q
4112            .upper_bound
4113            .map(|v| -> Result<(Expr, Arc<dyn PhysicalExpr>)> {
4114                let logical = col(DIST_COL).lt(lit(v));
4115                let schema = flat_dist.schema();
4116                let df_schema = DFSchema::try_from(schema)?;
4117                let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4118                Ok::<(Expr, Arc<dyn PhysicalExpr>), _>((logical, physical))
4119            })
4120            .transpose()?;
4121
4122        let filter_expr = match (lower, upper) {
4123            (Some((llog, _)), Some((ulog, _))) => {
4124                let logical = llog.and(ulog);
4125                let schema = flat_dist.schema();
4126                let df_schema = DFSchema::try_from(schema)?;
4127                let physical = create_physical_expr(&logical, &df_schema, &ExecutionProps::new())?;
4128                Some((logical, physical))
4129            }
4130            (Some((llog, lphys)), None) => Some((llog, lphys)),
4131            (None, Some((ulog, uphys))) => Some((ulog, uphys)),
4132            (None, None) => None,
4133        };
4134
4135        let knn_plan: Arc<dyn ExecutionPlan> = if let Some(filter_expr) = filter_expr {
4136            Arc::new(LanceFilterExec::try_new(filter_expr.0, flat_dist)?)
4137        } else {
4138            flat_dist
4139        };
4140
4141        // Use DataFusion's [SortExec] for Top-K search
4142        let sort = SortExec::new(
4143            [
4144                PhysicalSortExpr {
4145                    expr: expressions::col(DIST_COL, knn_plan.schema().as_ref())?,
4146                    options: SortOptions {
4147                        descending: false,
4148                        nulls_first: false,
4149                    },
4150                },
4151                PhysicalSortExpr {
4152                    expr: expressions::col(ROW_ID, knn_plan.schema().as_ref())?,
4153                    options: SortOptions {
4154                        descending: false,
4155                        nulls_first: false,
4156                    },
4157                },
4158            ]
4159            .into(),
4160            knn_plan,
4161        )
4162        .with_fetch(Some(q.k));
4163
4164        let logical_not_null = col(DIST_COL).is_not_null();
4165        let not_nulls = Arc::new(LanceFilterExec::try_new(logical_not_null, Arc::new(sort))?);
4166
4167        Ok(not_nulls)
4168    }
4169
4170    fn get_fragments_as_bitmap(&self) -> RoaringBitmap {
4171        if let Some(fragments) = &self.fragments {
4172            RoaringBitmap::from_iter(fragments.iter().map(|f| f.id as u32))
4173        } else {
4174            self.dataset.fragment_bitmap.as_ref().clone()
4175        }
4176    }
4177
4178    /// Retain only fragments that are in the user-specified fragment list.
4179    /// If no fragment list is specified, returns the fragments unchanged.
4180    fn retain_target_fragments(&self, mut fragments: Vec<Fragment>) -> Vec<Fragment> {
4181        if let Some(target) = &self.fragments {
4182            let bitmap = RoaringBitmap::from_iter(target.iter().map(|f| f.id as u32));
4183            fragments.retain(|f| bitmap.contains(f.id as u32));
4184        }
4185        fragments
4186    }
4187
4188    fn get_indexed_frags(&self, index: &[IndexMetadata]) -> RoaringBitmap {
4189        let all_fragments = self.get_fragments_as_bitmap();
4190
4191        let mut all_indexed_frags = RoaringBitmap::new();
4192        for idx in index {
4193            if let Some(fragmap) = idx.fragment_bitmap.as_ref() {
4194                all_indexed_frags |= fragmap;
4195            } else {
4196                // If any index is missing the fragment bitmap it is safest to just assume we
4197                // need all fragments
4198                return all_fragments;
4199            }
4200        }
4201
4202        all_indexed_frags & all_fragments
4203    }
4204
4205    /// Create an Execution plan to do indexed ANN search
4206    async fn ann(
4207        &self,
4208        q: &Query,
4209        index: &[IndexMetadata],
4210        filter_plan: &ExprFilterPlan,
4211    ) -> Result<Arc<dyn ExecutionPlan>> {
4212        let prefilter_source = self
4213            .prefilter_source(filter_plan, self.get_indexed_frags(index))
4214            .await?;
4215        let inner_fanout_search = new_knn_exec(self.dataset.clone(), index, q, prefilter_source)?;
4216        let sort_expr = PhysicalSortExpr {
4217            expr: expressions::col(DIST_COL, inner_fanout_search.schema().as_ref())?,
4218            options: SortOptions {
4219                descending: false,
4220                nulls_first: false,
4221            },
4222        };
4223        let sort_expr_row_id = PhysicalSortExpr {
4224            expr: expressions::col(ROW_ID, inner_fanout_search.schema().as_ref())?,
4225            options: SortOptions {
4226                descending: false,
4227                nulls_first: false,
4228            },
4229        };
4230        Ok(Arc::new(
4231            SortExec::new([sort_expr, sort_expr_row_id].into(), inner_fanout_search)
4232                .with_fetch(Some(q.k * q.refine_factor.unwrap_or(1) as usize)),
4233        ))
4234    }
4235
4236    // Create an Execution plan to do ANN over multivectors
4237    async fn multivec_ann(
4238        &self,
4239        q: &Query,
4240        index: &[IndexMetadata],
4241        filter_plan: &ExprFilterPlan,
4242    ) -> Result<Arc<dyn ExecutionPlan>> {
4243        // we split the query procedure into two steps:
4244        // 1. collect the candidates by vector searching on each query vector
4245        // 2. scoring the candidates
4246
4247        let over_fetch_factor = *DEFAULT_XTR_OVERFETCH;
4248
4249        let prefilter_source = self
4250            .prefilter_source(filter_plan, self.get_indexed_frags(index))
4251            .await?;
4252        let dim = get_vector_dim(self.dataset.schema(), &q.column)?;
4253
4254        let num_queries = q.key.len() / dim;
4255        let new_queries = (0..num_queries)
4256            .map(|i| q.key.slice(i * dim, dim))
4257            .map(|query_vec| {
4258                let mut new_query = q.clone();
4259                new_query.key = query_vec;
4260                // with XTR, we don't need to refine the result with original vectors,
4261                // but here we really need to over-fetch the candidates to reach good enough recall.
4262                // TODO: improve the recall with WARP, expose this parameter to the users.
4263                new_query.refine_factor = Some(over_fetch_factor);
4264                new_query
4265            });
4266        let mut ann_nodes = Vec::with_capacity(new_queries.len());
4267        for query in new_queries {
4268            // this produces `nprobes * k * over_fetch_factor * num_indices` candidates
4269            let ann_node = new_knn_exec(
4270                self.dataset.clone(),
4271                index,
4272                &query,
4273                prefilter_source.clone(),
4274            )?;
4275            let sort_expr = PhysicalSortExpr {
4276                expr: expressions::col(DIST_COL, ann_node.schema().as_ref())?,
4277                options: SortOptions {
4278                    descending: false,
4279                    nulls_first: false,
4280                },
4281            };
4282            let sort_expr_row_id = PhysicalSortExpr {
4283                expr: expressions::col(ROW_ID, ann_node.schema().as_ref())?,
4284                options: SortOptions {
4285                    descending: false,
4286                    nulls_first: false,
4287                },
4288            };
4289            let ann_node = Arc::new(
4290                SortExec::new([sort_expr, sort_expr_row_id].into(), ann_node)
4291                    .with_fetch(Some(q.k * over_fetch_factor as usize)),
4292            );
4293            ann_nodes.push(ann_node as Arc<dyn ExecutionPlan>);
4294        }
4295
4296        let ann_node = Arc::new(MultivectorScoringExec::try_new(ann_nodes, q.clone())?);
4297
4298        let sort_expr = PhysicalSortExpr {
4299            expr: expressions::col(DIST_COL, ann_node.schema().as_ref())?,
4300            options: SortOptions {
4301                descending: false,
4302                nulls_first: false,
4303            },
4304        };
4305        let sort_expr_row_id = PhysicalSortExpr {
4306            expr: expressions::col(ROW_ID, ann_node.schema().as_ref())?,
4307            options: SortOptions {
4308                descending: false,
4309                nulls_first: false,
4310            },
4311        };
4312        let ann_node = Arc::new(
4313            SortExec::new([sort_expr, sort_expr_row_id].into(), ann_node)
4314                .with_fetch(Some(q.k * q.refine_factor.unwrap_or(1) as usize)),
4315        );
4316
4317        Ok(ann_node)
4318    }
4319
4320    /// Create prefilter source from filter plan
4321    ///
4322    /// A prefilter is an input to a vector or fts search.  It tells us which rows are eligible
4323    /// for the search.  A prefilter is calculated by doing a filtered read of the row id column.
4324    async fn prefilter_source(
4325        &self,
4326        filter_plan: &ExprFilterPlan,
4327        required_frags: RoaringBitmap,
4328    ) -> Result<PreFilterSource> {
4329        if filter_plan.is_empty() && self.fragments.is_none() {
4330            log::trace!("no filter plan, no prefilter");
4331            return Ok(PreFilterSource::None);
4332        }
4333
4334        // get fragments covered by index
4335        let fragments: Vec<Fragment> = self
4336            .dataset
4337            .manifest
4338            .fragments
4339            .iter()
4340            .filter(|f| required_frags.contains(f.id as u32))
4341            .cloned()
4342            .collect();
4343
4344        // If explicitly specified fragments with .with_fragments(), intersect with those
4345        let fragments = Arc::new(self.retain_target_fragments(fragments));
4346
4347        // Can only use ScalarIndexExec when the scalar index is exact and we are not scanning
4348        // a subset of the fragments.
4349        //
4350        // TODO: We could enhance ScalarIndexExec with a fragment bitmap to filter out rows that
4351        // are not in the fragments we are scanning.
4352        if filter_plan.is_exact_index_search() && self.fragments.is_none() {
4353            let index_query = filter_plan.index_query.as_ref().expect_ok()?;
4354            let (_, missing_frags) = self
4355                .partition_frags_by_coverage(index_query, fragments.clone())
4356                .await?;
4357
4358            if missing_frags.is_empty() {
4359                log::trace!("prefilter entirely satisfied by exact index search");
4360                // We can only avoid materializing the index for a prefilter if:
4361                // 1. The search is indexed
4362                // 2. The index search is an exact search with no recheck or refine
4363                // 3. The indices cover at least the same fragments as the vector index
4364                return Ok(PreFilterSource::ScalarIndexQuery(Arc::new(
4365                    ScalarIndexExec::new(self.dataset.clone(), index_query.clone()),
4366                )));
4367            } else {
4368                log::trace!("exact index search did not cover all fragments");
4369            }
4370        }
4371
4372        // If one of our criteria is not met, we need to do a filtered read of just the row id column
4373        log::trace!(
4374            "prefilter is a filtered read of {} fragments",
4375            fragments.len()
4376        );
4377        let PlannedFilteredScan { plan, .. } = self
4378            .filtered_read(
4379                filter_plan,
4380                self.dataset.empty_projection().with_row_id(),
4381                false,
4382                Some(fragments),
4383                None,
4384                /*is_prefilter= */ true,
4385            )
4386            .await?;
4387        Ok(PreFilterSource::FilteredRowIds(plan))
4388    }
4389
4390    /// Take row indices produced by input plan from the dataset (with projection)
4391    fn take(
4392        &self,
4393        input: Arc<dyn ExecutionPlan>,
4394        output_projection: Projection,
4395    ) -> Result<Arc<dyn ExecutionPlan>> {
4396        let coalesced = Arc::new(CoalesceBatchesExec::new(
4397            input.clone(),
4398            self.get_batch_size(),
4399        ));
4400        if let Some(take_plan) =
4401            TakeExec::try_new(self.dataset.clone(), coalesced, output_projection)?
4402        {
4403            Ok(Arc::new(take_plan))
4404        } else {
4405            // No new columns needed
4406            Ok(input)
4407        }
4408    }
4409
4410    /// Global offset-limit of the result of the input plan
4411    fn limit_node(&self, plan: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
4412        Arc::new(GlobalLimitExec::new(
4413            plan,
4414            *self.offset.as_ref().unwrap_or(&0) as usize,
4415            self.limit.map(|l| l as usize),
4416        ))
4417    }
4418
4419    #[instrument(level = "info", skip(self))]
4420    pub async fn analyze_plan(&self) -> Result<String> {
4421        let plan = self.create_plan().await?;
4422        analyze_plan(
4423            plan,
4424            LanceExecutionOptions {
4425                batch_size: self.batch_size,
4426                ..Default::default()
4427            },
4428        )
4429        .await
4430    }
4431
4432    #[instrument(level = "info", skip(self))]
4433    pub async fn explain_plan(&self, verbose: bool) -> Result<String> {
4434        let plan = self.create_plan().await?;
4435        let display = DisplayableExecutionPlan::new(plan.as_ref());
4436
4437        Ok(format!("{}", display.indent(verbose)))
4438    }
4439}
4440
4441// Search over all indexed fields including nested ones, collecting columns that have an
4442// inverted index
4443async fn fts_indexed_columns(dataset: Arc<Dataset>) -> Result<Vec<String>> {
4444    let mut indexed_columns = Vec::new();
4445    for field in dataset.schema().fields_pre_order() {
4446        // Check if this field is a string type that could have an inverted index
4447        let is_string_field = match field.data_type() {
4448            DataType::Utf8 | DataType::LargeUtf8 => true,
4449            DataType::List(inner_field) | DataType::LargeList(inner_field) => {
4450                matches!(
4451                    inner_field.data_type(),
4452                    DataType::Utf8 | DataType::LargeUtf8
4453                )
4454            }
4455            _ => false,
4456        };
4457
4458        if is_string_field {
4459            // Build the full field path for nested fields
4460            let column_path =
4461                if let Some(ancestors) = dataset.schema().field_ancestry_by_id(field.id) {
4462                    let field_refs: Vec<&str> = ancestors.iter().map(|f| f.name.as_str()).collect();
4463                    format_field_path(&field_refs)
4464                } else {
4465                    continue; // Skip if we can't find the field ancestry
4466                };
4467
4468            // Check if this field has an inverted index
4469            let has_fts_index = dataset
4470                .load_scalar_index(
4471                    IndexCriteria::default()
4472                        .for_column(&column_path)
4473                        .supports_fts(),
4474                )
4475                .await?
4476                .is_some();
4477
4478            if has_fts_index {
4479                indexed_columns.push(column_path);
4480            }
4481        }
4482    }
4483    Ok(indexed_columns)
4484}
4485
4486/// [`DatasetRecordBatchStream`] wraps the dataset into a [`RecordBatchStream`] for
4487/// consumption by the user.
4488///
4489#[pin_project::pin_project]
4490pub struct DatasetRecordBatchStream {
4491    #[pin]
4492    exec_node: SendableRecordBatchStream,
4493    span: Span,
4494}
4495
4496impl DatasetRecordBatchStream {
4497    pub fn new(exec_node: SendableRecordBatchStream) -> Self {
4498        let schema = exec_node.schema();
4499        let adapter = SchemaAdapter::new(schema.clone());
4500        let exec_node = if SchemaAdapter::requires_logical_conversion(&schema) {
4501            adapter.to_logical_stream(exec_node)
4502        } else {
4503            exec_node
4504        };
4505
4506        let span = info_span!("DatasetRecordBatchStream");
4507        Self { exec_node, span }
4508    }
4509}
4510
4511impl RecordBatchStream for DatasetRecordBatchStream {
4512    fn schema(&self) -> SchemaRef {
4513        self.exec_node.schema()
4514    }
4515}
4516
4517impl Stream for DatasetRecordBatchStream {
4518    type Item = Result<RecordBatch>;
4519
4520    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
4521        let mut this = self.project();
4522        let _guard = this.span.enter();
4523        match this.exec_node.poll_next_unpin(cx) {
4524            Poll::Ready(result) => Poll::Ready(result.map(|r| Ok(r?))),
4525            Poll::Pending => Poll::Pending,
4526        }
4527    }
4528}
4529
4530impl From<DatasetRecordBatchStream> for SendableRecordBatchStream {
4531    fn from(stream: DatasetRecordBatchStream) -> Self {
4532        stream.exec_node
4533    }
4534}
4535
4536#[cfg(test)]
4537pub mod test_dataset {
4538
4539    use super::*;
4540
4541    use std::{collections::HashMap, vec};
4542
4543    use arrow_array::{
4544        ArrayRef, FixedSizeListArray, Int32Array, RecordBatch, RecordBatchIterator, StringArray,
4545    };
4546    use arrow_schema::{ArrowError, DataType};
4547    use lance_arrow::FixedSizeListArrayExt;
4548    use lance_core::utils::tempfile::TempStrDir;
4549    use lance_file::version::LanceFileVersion;
4550    use lance_index::{
4551        IndexType,
4552        scalar::{ScalarIndexParams, inverted::tokenizer::InvertedIndexParams},
4553    };
4554
4555    use crate::dataset::WriteParams;
4556    use crate::index::vector::VectorIndexParams;
4557
4558    // Creates a dataset with 5 batches where each batch has 80 rows
4559    //
4560    // The dataset has the following columns:
4561    //
4562    //  i   - i32      : [0, 1, ..., 399]
4563    //  s   - &str     : ["s-0", "s-1", ..., "s-399"]
4564    //  vec - [f32; 32]: [[0, 1, ... 31], [32, ..., 63], ... [..., (80 * 5 * 32) - 1]]
4565    //
4566    // An IVF-PQ index with 2 partitions is trained on this data
4567    pub struct TestVectorDataset {
4568        pub tmp_dir: TempStrDir,
4569        pub schema: Arc<ArrowSchema>,
4570        pub dataset: Dataset,
4571        dimension: u32,
4572    }
4573
4574    impl TestVectorDataset {
4575        pub async fn new(
4576            data_storage_version: LanceFileVersion,
4577            stable_row_ids: bool,
4578        ) -> Result<Self> {
4579            Self::new_with_dimension(data_storage_version, stable_row_ids, 32).await
4580        }
4581
4582        pub async fn new_with_dimension(
4583            data_storage_version: LanceFileVersion,
4584            stable_row_ids: bool,
4585            dimension: u32,
4586        ) -> Result<Self> {
4587            let path = TempStrDir::default();
4588
4589            // Make sure the schema has metadata so it tests all paths that re-construct the schema along the way
4590            let metadata: HashMap<String, String> =
4591                vec![("dataset".to_string(), "vector".to_string())]
4592                    .into_iter()
4593                    .collect();
4594
4595            let schema = Arc::new(ArrowSchema::new_with_metadata(
4596                vec![
4597                    ArrowField::new("i", DataType::Int32, true),
4598                    ArrowField::new("s", DataType::Utf8, true),
4599                    ArrowField::new(
4600                        "vec",
4601                        DataType::FixedSizeList(
4602                            Arc::new(ArrowField::new("item", DataType::Float32, true)),
4603                            dimension as i32,
4604                        ),
4605                        true,
4606                    ),
4607                ],
4608                metadata,
4609            ));
4610
4611            let batches: Vec<RecordBatch> = (0..5)
4612                .map(|i| {
4613                    let vector_values: Float32Array =
4614                        (0..dimension * 80).map(|v| v as f32).collect();
4615                    let vectors =
4616                        FixedSizeListArray::try_new_from_values(vector_values, dimension as i32)
4617                            .unwrap();
4618                    RecordBatch::try_new(
4619                        schema.clone(),
4620                        vec![
4621                            Arc::new(Int32Array::from_iter_values(i * 80..(i + 1) * 80)),
4622                            Arc::new(StringArray::from_iter_values(
4623                                (i * 80..(i + 1) * 80).map(|v| format!("s-{}", v)),
4624                            )),
4625                            Arc::new(vectors),
4626                        ],
4627                    )
4628                })
4629                .collect::<std::result::Result<Vec<_>, ArrowError>>()?;
4630
4631            let params = WriteParams {
4632                max_rows_per_group: 10,
4633                max_rows_per_file: 200,
4634                data_storage_version: Some(data_storage_version),
4635                enable_stable_row_ids: stable_row_ids,
4636                ..Default::default()
4637            };
4638            let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
4639
4640            let dataset = Dataset::write(reader, &path, Some(params)).await?;
4641
4642            Ok(Self {
4643                tmp_dir: path,
4644                schema,
4645                dataset,
4646                dimension,
4647            })
4648        }
4649
4650        pub async fn make_vector_index(&mut self) -> Result<()> {
4651            let params = VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2);
4652            self.dataset
4653                .create_index(
4654                    &["vec"],
4655                    IndexType::Vector,
4656                    Some("idx".to_string()),
4657                    &params,
4658                    true,
4659                )
4660                .await?;
4661            Ok(())
4662        }
4663
4664        pub async fn make_scalar_index(&mut self) -> Result<()> {
4665            self.dataset
4666                .create_index(
4667                    &["i"],
4668                    IndexType::Scalar,
4669                    None,
4670                    &ScalarIndexParams::default(),
4671                    true,
4672                )
4673                .await?;
4674            Ok(())
4675        }
4676
4677        pub async fn make_fts_index(&mut self) -> Result<()> {
4678            let params = InvertedIndexParams::default().with_position(true);
4679            self.dataset
4680                .create_index(&["s"], IndexType::Inverted, None, &params, true)
4681                .await?;
4682            Ok(())
4683        }
4684
4685        pub async fn append_new_data(&mut self) -> Result<()> {
4686            self.append_data_with_range(400, 410).await
4687        }
4688
4689        pub async fn append_data_with_range(&mut self, start: i32, end: i32) -> Result<()> {
4690            let count = (end - start) as usize;
4691            let vector_values: Float32Array = (0..count)
4692                .flat_map(|i| vec![i as f32; self.dimension as usize].into_iter())
4693                .collect();
4694            let new_vectors =
4695                FixedSizeListArray::try_new_from_values(vector_values, self.dimension as i32)
4696                    .unwrap();
4697            let new_data: Vec<ArrayRef> = vec![
4698                Arc::new(Int32Array::from_iter_values(start..end)),
4699                Arc::new(StringArray::from_iter_values(
4700                    (start..end).map(|v| format!("s-{}", v)),
4701                )),
4702                Arc::new(new_vectors),
4703            ];
4704            let reader = RecordBatchIterator::new(
4705                vec![RecordBatch::try_new(self.schema.clone(), new_data).unwrap()]
4706                    .into_iter()
4707                    .map(Ok),
4708                self.schema.clone(),
4709            );
4710            self.dataset.append(reader, None).await?;
4711            Ok(())
4712        }
4713    }
4714}
4715
4716#[cfg(test)]
4717mod test {
4718
4719    use std::collections::BTreeSet;
4720    use std::time::{Duration, Instant};
4721    use std::vec;
4722
4723    use arrow::array::as_primitive_array;
4724    use arrow::datatypes::{Float64Type, Int32Type, Int64Type};
4725    use arrow_array::cast::AsArray;
4726    use arrow_array::types::{Float32Type, UInt64Type};
4727    use arrow_array::{
4728        ArrayRef, FixedSizeListArray, Float16Array, Int32Array, LargeStringArray, PrimitiveArray,
4729        RecordBatchIterator, StringArray, StructArray, UInt8Array,
4730    };
4731
4732    use arrow_ord::sort::sort_to_indices;
4733    use arrow_schema::Fields;
4734    use arrow_select::take;
4735    use datafusion::logical_expr::{col, lit};
4736    use half::f16;
4737    use lance_arrow::{FixedSizeListArrayExt, SchemaExt};
4738    use lance_core::utils::tempfile::TempStrDir;
4739    use lance_core::{ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION};
4740    use lance_datagen::{
4741        ArrayGeneratorExt, BatchCount, ByteCount, Dimension, RowCount, array, gen_batch,
4742    };
4743    use lance_file::version::LanceFileVersion;
4744    use lance_index::optimize::OptimizeOptions;
4745    use lance_index::scalar::inverted::query::{MatchQuery, PhraseQuery};
4746    use lance_index::vector::hnsw::builder::HnswBuildParams;
4747    use lance_index::vector::ivf::IvfBuildParams;
4748    use lance_index::vector::pq::PQBuildParams;
4749    use lance_index::vector::sq::builder::SQBuildParams;
4750    use lance_index::{IndexType, scalar::ScalarIndexParams};
4751    use lance_io::assert_io_gt;
4752    use lance_io::object_store::ObjectStoreParams;
4753
4754    use lance_linalg::distance::DistanceType;
4755    use lance_testing::datagen::{BatchGenerator, IncrementingInt32, RandomVector};
4756    use object_store::throttle::ThrottleConfig;
4757    use rstest::rstest;
4758
4759    use super::*;
4760    use crate::dataset::WriteMode;
4761    use crate::dataset::WriteParams;
4762    use crate::dataset::optimize::{CompactionOptions, compact_files};
4763    use crate::dataset::scanner::test_dataset::TestVectorDataset;
4764    use crate::index::vector::{StageParams, VectorIndexParams};
4765    use crate::utils::test::{
4766        DatagenExt, FragmentCount, FragmentRowCount, ThrottledStoreWrapper, assert_plan_node_equals,
4767    };
4768
4769    #[test]
4770    fn test_env_var_parsing() {
4771        // Test that invalid environment variable values don't panic
4772
4773        // Test invalid LANCE_DEFAULT_BATCH_SIZE
4774        unsafe {
4775            std::env::set_var("LANCE_DEFAULT_BATCH_SIZE", "not_a_number");
4776        }
4777        let result = get_default_batch_size();
4778        assert_eq!(result, None, "Should return None for invalid batch size");
4779
4780        // Test valid LANCE_DEFAULT_BATCH_SIZE
4781        unsafe {
4782            std::env::set_var("LANCE_DEFAULT_BATCH_SIZE", "2048");
4783        }
4784        let result = get_default_batch_size();
4785        assert_eq!(result, Some(2048), "Should parse valid batch size");
4786
4787        // Test unset LANCE_DEFAULT_BATCH_SIZE
4788        unsafe {
4789            std::env::remove_var("LANCE_DEFAULT_BATCH_SIZE");
4790        }
4791        let result = get_default_batch_size();
4792        assert_eq!(result, None, "Should return None when env var is not set");
4793    }
4794
4795    #[test]
4796    fn test_parse_env_var() {
4797        // Test parse_env_var with different types to ensure full coverage
4798
4799        // Test with a unique env var name to avoid conflicts
4800        let test_var = "LANCE_TEST_PARSE_ENV_VAR_USIZE";
4801
4802        // Test valid usize parsing
4803        unsafe {
4804            std::env::set_var(test_var, "12345");
4805        }
4806        let result: Option<usize> = parse_env_var(test_var, "Using default.");
4807        assert_eq!(result, Some(12345));
4808
4809        // Test invalid usize parsing (triggers warning log)
4810        unsafe {
4811            std::env::set_var(test_var, "not_a_number");
4812        }
4813        let result: Option<usize> = parse_env_var(test_var, "Using default.");
4814        assert_eq!(result, None);
4815
4816        // Test unset env var
4817        unsafe {
4818            std::env::remove_var(test_var);
4819        }
4820        let result: Option<usize> = parse_env_var(test_var, "Using default.");
4821        assert_eq!(result, None);
4822
4823        // Test with u32 type
4824        let test_var_u32 = "LANCE_TEST_PARSE_ENV_VAR_U32";
4825        unsafe {
4826            std::env::set_var(test_var_u32, "42");
4827        }
4828        let result: Option<u32> = parse_env_var(test_var_u32, "Using default value.");
4829        assert_eq!(result, Some(42));
4830
4831        unsafe {
4832            std::env::set_var(test_var_u32, "invalid");
4833        }
4834        let result: Option<u32> = parse_env_var(test_var_u32, "Using default value.");
4835        assert_eq!(result, None);
4836
4837        unsafe {
4838            std::env::remove_var(test_var_u32);
4839        }
4840
4841        // Test with u64 type
4842        let test_var_u64 = "LANCE_TEST_PARSE_ENV_VAR_U64";
4843        unsafe {
4844            std::env::set_var(test_var_u64, "9999999999");
4845        }
4846        let result: Option<u64> = parse_env_var(test_var_u64, "Using default value.");
4847        assert_eq!(result, Some(9999999999));
4848
4849        unsafe {
4850            std::env::set_var(test_var_u64, "-1");
4851        }
4852        let result: Option<u64> = parse_env_var(test_var_u64, "Using default value.");
4853        assert_eq!(result, None);
4854
4855        unsafe {
4856            std::env::remove_var(test_var_u64);
4857        }
4858    }
4859
4860    async fn make_binary_vector_dataset() -> Result<(TempStrDir, Dataset)> {
4861        let tmp_dir = TempStrDir::default();
4862        let dim = 4;
4863        let schema = Arc::new(ArrowSchema::new(vec![
4864            ArrowField::new("id", DataType::Int32, false),
4865            ArrowField::new(
4866                "bin",
4867                DataType::FixedSizeList(
4868                    Arc::new(ArrowField::new("item", DataType::UInt8, true)),
4869                    dim,
4870                ),
4871                false,
4872            ),
4873        ]));
4874
4875        let vectors = FixedSizeListArray::try_new_from_values(
4876            UInt8Array::from(vec![
4877                0b0000_1111u8,
4878                0,
4879                0,
4880                0, //
4881                0b0000_0011u8,
4882                0,
4883                0,
4884                0, //
4885                0u8,
4886                0,
4887                0,
4888                0,
4889            ]),
4890            dim,
4891        )?;
4892        let ids = Int32Array::from(vec![0, 1, 2]);
4893
4894        let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(ids), Arc::new(vectors)])?;
4895        let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
4896        Dataset::write(reader, &tmp_dir, None).await?;
4897        let dataset = Dataset::open(&tmp_dir).await?;
4898        Ok((tmp_dir, dataset))
4899    }
4900
4901    #[tokio::test]
4902    async fn test_batch_size() {
4903        let schema = Arc::new(ArrowSchema::new(vec![
4904            ArrowField::new("i", DataType::Int32, true),
4905            ArrowField::new("s", DataType::Utf8, true),
4906        ]));
4907
4908        let batches: Vec<RecordBatch> = (0..5)
4909            .map(|i| {
4910                RecordBatch::try_new(
4911                    schema.clone(),
4912                    vec![
4913                        Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20)),
4914                        Arc::new(StringArray::from_iter_values(
4915                            (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
4916                        )),
4917                    ],
4918                )
4919                .unwrap()
4920            })
4921            .collect();
4922
4923        for use_filter in [false, true] {
4924            let test_dir = TempStrDir::default();
4925            let test_uri = &test_dir;
4926            let write_params = WriteParams {
4927                max_rows_per_file: 40,
4928                max_rows_per_group: 10,
4929                ..Default::default()
4930            };
4931            let batches =
4932                RecordBatchIterator::new(batches.clone().into_iter().map(Ok), schema.clone());
4933            Dataset::write(batches, test_uri, Some(write_params))
4934                .await
4935                .unwrap();
4936
4937            let dataset = Dataset::open(test_uri).await.unwrap();
4938            let mut builder = dataset.scan();
4939            builder.batch_size(8);
4940            if use_filter {
4941                builder.filter("i IS NOT NULL").unwrap();
4942            }
4943            let mut stream = builder.try_into_stream().await.unwrap();
4944            let mut rows_read = 0;
4945            while let Some(next) = stream.next().await {
4946                let next = next.unwrap();
4947                let expected = 8.min(100 - rows_read);
4948                assert_eq!(next.num_rows(), expected);
4949                rows_read += next.num_rows();
4950            }
4951        }
4952    }
4953
4954    #[tokio::test]
4955    async fn test_strict_batch_size() {
4956        let dataset = lance_datagen::gen_batch()
4957            .col("x", array::step::<Int32Type>())
4958            .anon_col(array::step::<Int64Type>())
4959            .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
4960            .await
4961            .unwrap();
4962
4963        let mut scan = dataset.scan();
4964        scan.batch_size(10)
4965            .strict_batch_size(true)
4966            .filter("x % 2 == 0")
4967            .unwrap();
4968
4969        let batches = scan
4970            .try_into_stream()
4971            .await
4972            .unwrap()
4973            .try_collect::<Vec<_>>()
4974            .await
4975            .unwrap();
4976
4977        let batch_sizes = batches.iter().map(|b| b.num_rows()).collect::<Vec<_>>();
4978        assert_eq!(batch_sizes, vec![10, 10, 1]);
4979    }
4980
4981    #[tokio::test]
4982    async fn test_column_not_exist() {
4983        let dataset = lance_datagen::gen_batch()
4984            .col("x", array::step::<Int32Type>())
4985            .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
4986            .await
4987            .unwrap();
4988
4989        let check_err_msg = |r: Result<DatasetRecordBatchStream>| {
4990            let Err(err) = r else {
4991                panic!(
4992                    "Expected an error to be raised saying column y is not found but got no error"
4993                )
4994            };
4995
4996            assert!(
4997                err.to_string().contains("No field named y"),
4998                "Expected error to contain 'No field named y' but got {}",
4999                err
5000            );
5001        };
5002
5003        let mut scan = dataset.scan();
5004        scan.project(&["x", "y"]).unwrap();
5005        check_err_msg(scan.try_into_stream().await);
5006
5007        let mut scan = dataset.scan();
5008        scan.project(&["y"]).unwrap();
5009        check_err_msg(scan.try_into_stream().await);
5010
5011        // This represents a query like `SELECT 1 AS foo` which we could _technically_ satisfy
5012        // but it is not supported today
5013        let mut scan = dataset.scan();
5014        scan.project_with_transform(&[("foo", "1")]).unwrap();
5015        match scan.try_into_stream().await {
5016            Ok(_) => panic!("Expected an error to be raised saying not supported"),
5017            Err(e) => {
5018                assert!(
5019                    e.to_string().contains("Received only dynamic expressions"),
5020                    "Expected error to contain 'Received only dynamic expressions' but got {}",
5021                    e
5022                );
5023            }
5024        }
5025    }
5026
5027    #[cfg(not(windows))]
5028    #[tokio::test]
5029    async fn test_local_object_store() {
5030        let schema = Arc::new(ArrowSchema::new(vec![
5031            ArrowField::new("i", DataType::Int32, true),
5032            ArrowField::new("s", DataType::Utf8, true),
5033        ]));
5034
5035        let batches: Vec<RecordBatch> = (0..5)
5036            .map(|i| {
5037                RecordBatch::try_new(
5038                    schema.clone(),
5039                    vec![
5040                        Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20)),
5041                        Arc::new(StringArray::from_iter_values(
5042                            (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
5043                        )),
5044                    ],
5045                )
5046                .unwrap()
5047            })
5048            .collect();
5049
5050        let test_dir = TempStrDir::default();
5051        let test_uri = &test_dir;
5052        let write_params = WriteParams {
5053            max_rows_per_file: 40,
5054            max_rows_per_group: 10,
5055            ..Default::default()
5056        };
5057        let batches = RecordBatchIterator::new(batches.clone().into_iter().map(Ok), schema.clone());
5058        Dataset::write(batches, test_uri, Some(write_params))
5059            .await
5060            .unwrap();
5061
5062        let dataset = Dataset::open(&format!("file-object-store://{}", test_uri))
5063            .await
5064            .unwrap();
5065        let mut builder = dataset.scan();
5066        builder.batch_size(8);
5067        let mut stream = builder.try_into_stream().await.unwrap();
5068        let mut rows_read = 0;
5069        while let Some(next) = stream.next().await {
5070            let next = next.unwrap();
5071            let expected = 8.min(100 - rows_read);
5072            assert_eq!(next.num_rows(), expected);
5073            rows_read += next.num_rows();
5074        }
5075    }
5076
5077    #[tokio::test]
5078    async fn test_filter_parsing() -> Result<()> {
5079        let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false).await?;
5080        let dataset = &test_ds.dataset;
5081
5082        let mut scan = dataset.scan();
5083        assert!(scan.filter.is_none());
5084
5085        scan.filter("i > 50")?;
5086        assert_eq!(scan.get_expr_filter().unwrap(), Some(col("i").gt(lit(50))));
5087
5088        for use_stats in [false, true] {
5089            let batches = scan
5090                .project(&["s"])?
5091                .use_stats(use_stats)
5092                .try_into_stream()
5093                .await?
5094                .try_collect::<Vec<_>>()
5095                .await?;
5096            let batch = concat_batches(&batches[0].schema(), &batches)?;
5097
5098            let expected_batch = RecordBatch::try_new(
5099                // Projected just "s"
5100                Arc::new(test_ds.schema.project(&[1])?),
5101                vec![Arc::new(StringArray::from_iter_values(
5102                    (51..400).map(|v| format!("s-{}", v)),
5103                ))],
5104            )?;
5105            assert_eq!(batch, expected_batch);
5106        }
5107        Ok(())
5108    }
5109
5110    #[tokio::test]
5111    async fn test_scan_regexp_match_and_non_empty_captions() {
5112        // Build a small dataset with three Utf8 columns and verify the full
5113        // scan().filter(...) path handles regexp_match combined with non-null/non-empty checks.
5114        let schema = Arc::new(ArrowSchema::new(vec![
5115            ArrowField::new("keywords", DataType::Utf8, true),
5116            ArrowField::new("natural_caption", DataType::Utf8, true),
5117            ArrowField::new("poetic_caption", DataType::Utf8, true),
5118        ]));
5119
5120        let batch = RecordBatch::try_new(
5121            schema.clone(),
5122            vec![
5123                Arc::new(StringArray::from(vec![
5124                    Some("Liberty for all"),
5125                    Some("peace"),
5126                    Some("revolution now"),
5127                    Some("Liberty"),
5128                    Some("revolutionary"),
5129                    Some("none"),
5130                ])) as ArrayRef,
5131                Arc::new(StringArray::from(vec![
5132                    Some("a"),
5133                    Some("b"),
5134                    None,
5135                    Some(""),
5136                    Some("c"),
5137                    Some("d"),
5138                ])) as ArrayRef,
5139                Arc::new(StringArray::from(vec![
5140                    Some("x"),
5141                    Some(""),
5142                    Some("y"),
5143                    Some("z"),
5144                    None,
5145                    Some("w"),
5146                ])) as ArrayRef,
5147            ],
5148        )
5149        .unwrap();
5150
5151        let reader = RecordBatchIterator::new(vec![Ok(batch.clone())], schema.clone());
5152        let dataset = Dataset::write(reader, "memory://", None).await.unwrap();
5153
5154        let mut scan = dataset.scan();
5155        scan.filter(
5156            "regexp_match(keywords, 'Liberty|revolution') AND \
5157             (natural_caption IS NOT NULL AND natural_caption <> '' AND \
5158              poetic_caption IS NOT NULL AND poetic_caption <> '')",
5159        )
5160        .unwrap();
5161
5162        let out = scan.try_into_batch().await.unwrap();
5163        assert_eq!(out.num_rows(), 1);
5164
5165        let out_keywords = out
5166            .column_by_name("keywords")
5167            .unwrap()
5168            .as_string::<i32>()
5169            .value(0);
5170        let out_nat = out
5171            .column_by_name("natural_caption")
5172            .unwrap()
5173            .as_string::<i32>()
5174            .value(0);
5175        let out_poetic = out
5176            .column_by_name("poetic_caption")
5177            .unwrap()
5178            .as_string::<i32>()
5179            .value(0);
5180
5181        assert_eq!(out_keywords, "Liberty for all");
5182        assert_eq!(out_nat, "a");
5183        assert_eq!(out_poetic, "x");
5184    }
5185
5186    #[tokio::test]
5187    async fn test_nested_projection() {
5188        let point_fields: Fields = vec![
5189            ArrowField::new("x", DataType::Float32, true),
5190            ArrowField::new("y", DataType::Float32, true),
5191        ]
5192        .into();
5193        let metadata_fields: Fields = vec![
5194            ArrowField::new("location", DataType::Struct(point_fields), true),
5195            ArrowField::new("age", DataType::Int32, true),
5196        ]
5197        .into();
5198        let metadata_field = ArrowField::new("metadata", DataType::Struct(metadata_fields), true);
5199        let schema = Arc::new(ArrowSchema::new(vec![
5200            metadata_field,
5201            ArrowField::new("idx", DataType::Int32, true),
5202        ]));
5203        let data = lance_datagen::rand(&schema)
5204            .into_ram_dataset(FragmentCount::from(7), FragmentRowCount::from(6))
5205            .await
5206            .unwrap();
5207
5208        let mut scan = data.scan();
5209        scan.project(&["metadata.location.x", "metadata.age"])
5210            .unwrap();
5211        let batch = scan.try_into_batch().await.unwrap();
5212
5213        assert_eq!(
5214            batch.schema().as_ref(),
5215            &ArrowSchema::new(vec![
5216                ArrowField::new("metadata.location.x", DataType::Float32, true),
5217                ArrowField::new("metadata.age", DataType::Int32, true),
5218            ])
5219        );
5220
5221        // 0 - metadata
5222        // 2 - x
5223        // 4 - age
5224        let take_schema = data.schema().project_by_ids(&[0, 2, 4], false);
5225
5226        let taken = data.take_rows(&[0, 5], take_schema).await.unwrap();
5227
5228        // The expected schema drops y from the location field
5229        let part_point_fields = Fields::from(vec![ArrowField::new("x", DataType::Float32, true)]);
5230        let part_metadata_fields = Fields::from(vec![
5231            ArrowField::new("location", DataType::Struct(part_point_fields), true),
5232            ArrowField::new("age", DataType::Int32, true),
5233        ]);
5234        let part_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
5235            "metadata",
5236            DataType::Struct(part_metadata_fields),
5237            true,
5238        )]));
5239
5240        assert_eq!(taken.schema(), part_schema);
5241    }
5242
5243    #[rstest]
5244    #[tokio::test]
5245    async fn test_limit(
5246        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5247        data_storage_version: LanceFileVersion,
5248    ) -> Result<()> {
5249        let test_ds = TestVectorDataset::new(data_storage_version, false).await?;
5250        let dataset = &test_ds.dataset;
5251
5252        let full_data = dataset.scan().try_into_batch().await?.slice(19, 2);
5253
5254        let actual = dataset
5255            .scan()
5256            .limit(Some(2), Some(19))?
5257            .try_into_batch()
5258            .await?;
5259
5260        assert_eq!(actual.num_rows(), 2);
5261        assert_eq!(actual, full_data);
5262        Ok(())
5263    }
5264
5265    #[test_log::test(tokio::test)]
5266    async fn test_limit_cancel() {
5267        // If there is a filter and a limit and we can't use the index to satisfy
5268        // the filter, then we have to read until we have enough matching rows and
5269        // then cancel the scan.
5270        //
5271        // This test regresses the case where we fail to cancel the scan for whatever
5272        // reason.
5273
5274        // Make the store slow so that if we don't cancel the scan, it will take a loooong time.
5275        let throttled = Arc::new(ThrottledStoreWrapper {
5276            config: ThrottleConfig {
5277                wait_get_per_call: Duration::from_secs(1),
5278                ..Default::default()
5279            },
5280        });
5281        let write_params = WriteParams {
5282            store_params: Some(ObjectStoreParams {
5283                object_store_wrapper: Some(throttled.clone()),
5284                ..Default::default()
5285            }),
5286            max_rows_per_file: 1,
5287            ..Default::default()
5288        };
5289
5290        // Make a dataset with lots of tiny fragments, that will make it more obvious if we fail to cancel the scan.
5291        let dataset = gen_batch()
5292            .col("i", array::step::<Int32Type>().with_random_nulls(0.1))
5293            .into_ram_dataset_with_params(
5294                FragmentCount::from(2000),
5295                FragmentRowCount::from(1),
5296                Some(write_params),
5297            )
5298            .await
5299            .unwrap();
5300
5301        let mut scan = dataset.scan();
5302        scan.filter("i IS NOT NULL").unwrap();
5303        scan.limit(Some(10), None).unwrap();
5304
5305        let start = Instant::now();
5306        scan.try_into_stream()
5307            .await
5308            .unwrap()
5309            .try_collect::<Vec<_>>()
5310            .await
5311            .unwrap();
5312        let duration = start.elapsed();
5313
5314        // This test is a timing test, which is unfortunate, as it may be flaky.  I'm hoping
5315        // we have enough wiggle room here.  The failure case is 30s on my machine and the pass
5316        // case is 2-3s.
5317        assert!(duration < Duration::from_secs(10));
5318    }
5319
5320    #[rstest]
5321    #[tokio::test]
5322    async fn test_knn_nodes(
5323        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5324        data_storage_version: LanceFileVersion,
5325        #[values(false, true)] stable_row_ids: bool,
5326        #[values(false, true)] build_index: bool,
5327    ) {
5328        let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5329            .await
5330            .unwrap();
5331        if build_index {
5332            test_ds.make_vector_index().await.unwrap();
5333        }
5334        let dataset = &test_ds.dataset;
5335
5336        let mut scan = dataset.scan();
5337        let key: Float32Array = (32..64).map(|v| v as f32).collect();
5338        scan.nearest("vec", &key, 5).unwrap();
5339        scan.refine(5);
5340
5341        let batch = scan.try_into_batch().await.unwrap();
5342
5343        assert_eq!(batch.num_rows(), 5);
5344        assert_eq!(
5345            batch.schema().as_ref(),
5346            &ArrowSchema::new(vec![
5347                ArrowField::new("i", DataType::Int32, true),
5348                ArrowField::new("s", DataType::Utf8, true),
5349                ArrowField::new(
5350                    "vec",
5351                    DataType::FixedSizeList(
5352                        Arc::new(ArrowField::new("item", DataType::Float32, true)),
5353                        32,
5354                    ),
5355                    true,
5356                ),
5357                ArrowField::new(DIST_COL, DataType::Float32, true),
5358            ])
5359            .with_metadata([("dataset".into(), "vector".into())].into())
5360        );
5361
5362        let expected_i = BTreeSet::from_iter(vec![1, 81, 161, 241, 321]);
5363        let column_i = batch.column_by_name("i").unwrap();
5364        let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5365            .values()
5366            .iter()
5367            .copied()
5368            .collect();
5369        assert_eq!(expected_i, actual_i);
5370    }
5371
5372    #[rstest]
5373    #[tokio::test]
5374    async fn test_can_project_distance() {
5375        let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
5376            .await
5377            .unwrap();
5378        let dataset = &test_ds.dataset;
5379
5380        let mut scan = dataset.scan();
5381        let key: Float32Array = (32..64).map(|v| v as f32).collect();
5382        scan.nearest("vec", &key, 5).unwrap();
5383        scan.refine(5);
5384        scan.project(&["_distance"]).unwrap();
5385
5386        let batch = scan.try_into_batch().await.unwrap();
5387
5388        assert_eq!(batch.num_rows(), 5);
5389        assert_eq!(batch.num_columns(), 1);
5390    }
5391
5392    #[rstest]
5393    #[tokio::test]
5394    async fn test_knn_with_new_data(
5395        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5396        data_storage_version: LanceFileVersion,
5397        #[values(false, true)] stable_row_ids: bool,
5398    ) {
5399        let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5400            .await
5401            .unwrap();
5402        test_ds.make_vector_index().await.unwrap();
5403        test_ds.append_new_data().await.unwrap();
5404        let dataset = &test_ds.dataset;
5405
5406        // Create a bunch of queries
5407        let key: Float32Array = [0f32; 32].into_iter().collect();
5408        // Set as larger than the number of new rows that aren't in the index to
5409        // force result sets to be combined between index and flat scan.
5410        let k = 20;
5411
5412        #[derive(Debug)]
5413        struct TestCase {
5414            filter: Option<&'static str>,
5415            limit: Option<i64>,
5416            use_index: bool,
5417        }
5418
5419        let mut cases = vec![];
5420        for filter in [Some("i > 100"), None] {
5421            for limit in [None, Some(10)] {
5422                for use_index in [true, false] {
5423                    cases.push(TestCase {
5424                        filter,
5425                        limit,
5426                        use_index,
5427                    });
5428                }
5429            }
5430        }
5431
5432        // Validate them all.
5433        for case in cases {
5434            let mut scanner = dataset.scan();
5435            scanner
5436                .nearest("vec", &key, k)
5437                .unwrap()
5438                .limit(case.limit, None)
5439                .unwrap()
5440                .refine(3)
5441                .use_index(case.use_index);
5442            if let Some(filter) = case.filter {
5443                scanner.filter(filter).unwrap();
5444            }
5445
5446            let result = scanner
5447                .try_into_stream()
5448                .await
5449                .unwrap()
5450                .try_collect::<Vec<_>>()
5451                .await
5452                .unwrap();
5453            assert!(!result.is_empty());
5454            let result = concat_batches(&result[0].schema(), result.iter()).unwrap();
5455
5456            if case.filter.is_some() {
5457                let result_rows = result.num_rows();
5458                let expected_rows = case.limit.unwrap_or(k as i64) as usize;
5459                assert!(
5460                    result_rows <= expected_rows,
5461                    "Expected less than {} rows, got {}",
5462                    expected_rows,
5463                    result_rows
5464                );
5465            } else {
5466                // Exactly equal count
5467                assert_eq!(result.num_rows(), case.limit.unwrap_or(k as i64) as usize);
5468            }
5469
5470            // Top one should be the first value of new data
5471            assert_eq!(
5472                as_primitive_array::<Int32Type>(result.column(0).as_ref()).value(0),
5473                400
5474            );
5475        }
5476    }
5477
5478    #[rstest]
5479    #[tokio::test]
5480    async fn test_knn_with_prefilter(
5481        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5482        data_storage_version: LanceFileVersion,
5483        #[values(false, true)] stable_row_ids: bool,
5484    ) {
5485        let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5486            .await
5487            .unwrap();
5488        test_ds.make_vector_index().await.unwrap();
5489        let dataset = &test_ds.dataset;
5490
5491        let mut scan = dataset.scan();
5492        let key: Float32Array = (32..64).map(|v| v as f32).collect();
5493        scan.filter("i > 100").unwrap();
5494        scan.prefilter(true);
5495        scan.project(&["i", "vec"]).unwrap();
5496        scan.nearest("vec", &key, 5).unwrap();
5497        scan.use_index(false);
5498
5499        let results = scan
5500            .try_into_stream()
5501            .await
5502            .unwrap()
5503            .try_collect::<Vec<_>>()
5504            .await
5505            .unwrap();
5506
5507        assert_eq!(results.len(), 1);
5508        let batch = &results[0];
5509
5510        assert_eq!(batch.num_rows(), 5);
5511        assert_eq!(
5512            batch.schema().as_ref(),
5513            &ArrowSchema::new(vec![
5514                ArrowField::new("i", DataType::Int32, true),
5515                ArrowField::new(
5516                    "vec",
5517                    DataType::FixedSizeList(
5518                        Arc::new(ArrowField::new("item", DataType::Float32, true)),
5519                        32,
5520                    ),
5521                    true,
5522                ),
5523                ArrowField::new(DIST_COL, DataType::Float32, true),
5524            ])
5525            .with_metadata([("dataset".into(), "vector".into())].into())
5526        );
5527
5528        // These match the query exactly.  The 5 results must include these 3.
5529        let exact_i = BTreeSet::from_iter(vec![161, 241, 321]);
5530        // These also include those 1 off from the query.  The remaining 2 results must be in this set.
5531        let close_i = BTreeSet::from_iter(vec![161, 241, 321, 160, 162, 240, 242, 320, 322]);
5532        let column_i = batch.column_by_name("i").unwrap();
5533        let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5534            .values()
5535            .iter()
5536            .copied()
5537            .collect();
5538        assert!(exact_i.is_subset(&actual_i));
5539        assert!(actual_i.is_subset(&close_i));
5540    }
5541
5542    #[rstest]
5543    #[tokio::test]
5544    async fn test_knn_filter_new_data(
5545        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5546        data_storage_version: LanceFileVersion,
5547        #[values(false, true)] stable_row_ids: bool,
5548    ) {
5549        // This test verifies that a filter (prefilter or postfilter) gets applied to the flat KNN results
5550        // in a combined KNN scan (a scan that combines results from an indexed ANN with an unindexed flat
5551        // search of new data)
5552        let mut test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5553            .await
5554            .unwrap();
5555        test_ds.make_vector_index().await.unwrap();
5556        test_ds.append_new_data().await.unwrap();
5557        let dataset = &test_ds.dataset;
5558
5559        // This query will match exactly the new row with i = 400 which should be excluded by the prefilter
5560        let key: Float32Array = [0f32; 32].into_iter().collect();
5561
5562        let mut query = dataset.scan();
5563        query.nearest("vec", &key, 20).unwrap();
5564
5565        // Sanity check that 400 is in our results
5566        let results = query
5567            .try_into_stream()
5568            .await
5569            .unwrap()
5570            .try_collect::<Vec<_>>()
5571            .await
5572            .unwrap();
5573
5574        let results_i = results[0]["i"]
5575            .as_primitive::<Int32Type>()
5576            .values()
5577            .iter()
5578            .copied()
5579            .collect::<BTreeSet<_>>();
5580
5581        assert!(results_i.contains(&400));
5582
5583        // Both prefilter and postfilter should remove 400 from our results
5584        for prefilter in [false, true] {
5585            let mut query = dataset.scan();
5586            query
5587                .filter("i != 400")
5588                .unwrap()
5589                .prefilter(prefilter)
5590                .nearest("vec", &key, 20)
5591                .unwrap();
5592
5593            let results = query
5594                .try_into_stream()
5595                .await
5596                .unwrap()
5597                .try_collect::<Vec<_>>()
5598                .await
5599                .unwrap();
5600
5601            let results_i = results[0]["i"]
5602                .as_primitive::<Int32Type>()
5603                .values()
5604                .iter()
5605                .copied()
5606                .collect::<BTreeSet<_>>();
5607
5608            assert!(!results_i.contains(&400));
5609        }
5610    }
5611
5612    #[rstest]
5613    #[tokio::test]
5614    async fn test_knn_with_filter(
5615        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5616        data_storage_version: LanceFileVersion,
5617        #[values(false, true)] stable_row_ids: bool,
5618    ) {
5619        let test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5620            .await
5621            .unwrap();
5622        let dataset = &test_ds.dataset;
5623
5624        let mut scan = dataset.scan();
5625        let key: Float32Array = (32..64).map(|v| v as f32).collect();
5626        scan.nearest("vec", &key, 5).unwrap();
5627        scan.filter("i > 100").unwrap();
5628        scan.project(&["i", "vec"]).unwrap();
5629        scan.refine(5);
5630
5631        let results = scan
5632            .try_into_stream()
5633            .await
5634            .unwrap()
5635            .try_collect::<Vec<_>>()
5636            .await
5637            .unwrap();
5638
5639        assert_eq!(results.len(), 1);
5640        let batch = &results[0];
5641
5642        assert_eq!(batch.num_rows(), 3);
5643        assert_eq!(
5644            batch.schema().as_ref(),
5645            &ArrowSchema::new(vec![
5646                ArrowField::new("i", DataType::Int32, true),
5647                ArrowField::new(
5648                    "vec",
5649                    DataType::FixedSizeList(
5650                        Arc::new(ArrowField::new("item", DataType::Float32, true)),
5651                        32,
5652                    ),
5653                    true,
5654                ),
5655                ArrowField::new(DIST_COL, DataType::Float32, true),
5656            ])
5657            .with_metadata([("dataset".into(), "vector".into())].into())
5658        );
5659
5660        let expected_i = BTreeSet::from_iter(vec![161, 241, 321]);
5661        let column_i = batch.column_by_name("i").unwrap();
5662        let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5663            .values()
5664            .iter()
5665            .copied()
5666            .collect();
5667        assert_eq!(expected_i, actual_i);
5668    }
5669
5670    #[rstest]
5671    #[tokio::test]
5672    async fn test_refine_factor(
5673        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5674        data_storage_version: LanceFileVersion,
5675        #[values(false, true)] stable_row_ids: bool,
5676    ) {
5677        let test_ds = TestVectorDataset::new(data_storage_version, stable_row_ids)
5678            .await
5679            .unwrap();
5680        let dataset = &test_ds.dataset;
5681
5682        let mut scan = dataset.scan();
5683        let key: Float32Array = (32..64).map(|v| v as f32).collect();
5684        scan.nearest("vec", &key, 5).unwrap();
5685        scan.refine(5);
5686
5687        let results = scan
5688            .try_into_stream()
5689            .await
5690            .unwrap()
5691            .try_collect::<Vec<_>>()
5692            .await
5693            .unwrap();
5694
5695        assert_eq!(results.len(), 1);
5696        let batch = &results[0];
5697
5698        assert_eq!(batch.num_rows(), 5);
5699        assert_eq!(
5700            batch.schema().as_ref(),
5701            &ArrowSchema::new(vec![
5702                ArrowField::new("i", DataType::Int32, true),
5703                ArrowField::new("s", DataType::Utf8, true),
5704                ArrowField::new(
5705                    "vec",
5706                    DataType::FixedSizeList(
5707                        Arc::new(ArrowField::new("item", DataType::Float32, true)),
5708                        32,
5709                    ),
5710                    true,
5711                ),
5712                ArrowField::new(DIST_COL, DataType::Float32, true),
5713            ])
5714            .with_metadata([("dataset".into(), "vector".into())].into())
5715        );
5716
5717        let expected_i = BTreeSet::from_iter(vec![1, 81, 161, 241, 321]);
5718        let column_i = batch.column_by_name("i").unwrap();
5719        let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
5720            .values()
5721            .iter()
5722            .copied()
5723            .collect();
5724        assert_eq!(expected_i, actual_i);
5725    }
5726
5727    #[tokio::test]
5728    async fn test_binary_vectors_default_to_hamming() {
5729        let (_tmp_dir, dataset) = make_binary_vector_dataset().await.unwrap();
5730        let query = UInt8Array::from(vec![0b0000_1111u8, 0, 0, 0]);
5731
5732        let mut scan = dataset.scan();
5733        scan.nearest("bin", &query, 3).unwrap();
5734
5735        // metric_type is None initially; it will be resolved to Hamming during search
5736        assert_eq!(scan.nearest.as_ref().unwrap().metric_type, None);
5737
5738        let batch = scan.try_into_batch().await.unwrap();
5739        let ids = batch
5740            .column_by_name("id")
5741            .unwrap()
5742            .as_primitive::<Int32Type>()
5743            .values();
5744        assert_eq!(ids, &[0, 1, 2]);
5745        let distances = batch
5746            .column_by_name(DIST_COL)
5747            .unwrap()
5748            .as_primitive::<Float32Type>()
5749            .values();
5750        assert_eq!(distances, &[0.0, 2.0, 4.0]);
5751    }
5752
5753    #[tokio::test]
5754    async fn test_binary_vectors_invalid_distance_error() {
5755        let (_tmp_dir, dataset) = make_binary_vector_dataset().await.unwrap();
5756        let query = UInt8Array::from(vec![0b0000_1111u8, 0, 0, 0]);
5757
5758        let mut scan = dataset.scan();
5759        scan.nearest("bin", &query, 1).unwrap();
5760        scan.distance_metric(DistanceType::L2);
5761
5762        let err = scan.try_into_batch().await.unwrap_err();
5763        assert!(matches!(err, Error::InvalidInput { .. }));
5764        let message = err.to_string();
5765        assert!(
5766            message.contains("l2") && message.contains("UInt8"),
5767            "unexpected message: {message}"
5768        );
5769    }
5770
5771    /// Test that when query specifies a metric different from the index,
5772    /// we fall back to flat search and return correct distances.
5773    /// Regression test for https://github.com/lance-format/lance/issues/5608
5774    #[tokio::test]
5775    async fn test_knn_metric_mismatch_falls_back_to_flat_search() {
5776        let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
5777            .await
5778            .unwrap();
5779        // Create IVF_PQ index with L2 metric
5780        test_ds.make_vector_index().await.unwrap();
5781
5782        let dataset = &test_ds.dataset;
5783        let key: Float32Array = (32..64).map(|v| v as f32).collect();
5784
5785        // Query with Dot metric (different from the L2 index)
5786        let mut scan = dataset.scan();
5787        scan.nearest("vec", &key, 5).unwrap();
5788        scan.distance_metric(DistanceType::Dot);
5789
5790        // Verify the explain plan does NOT show ANNSubIndex (should use flat search)
5791        let plan = scan.explain_plan(false).await.unwrap();
5792        assert!(
5793            !plan.contains("ANNSubIndex"),
5794            "Expected flat search, but got ANN index in plan:\n{}",
5795            plan
5796        );
5797        // Should show flat KNN with Dot metric (metric is displayed lowercase)
5798        assert!(
5799            plan.contains("KNNVectorDistance") && plan.to_lowercase().contains("dot"),
5800            "Expected flat KNN with Dot metric in plan:\n{}",
5801            plan
5802        );
5803
5804        // Also verify the distances are different from L2 results
5805        let dot_batch = dataset
5806            .scan()
5807            .nearest("vec", &key, 5)
5808            .unwrap()
5809            .distance_metric(DistanceType::Dot)
5810            .try_into_batch()
5811            .await
5812            .unwrap();
5813
5814        let l2_batch = dataset
5815            .scan()
5816            .nearest("vec", &key, 5)
5817            .unwrap()
5818            .distance_metric(DistanceType::L2)
5819            .try_into_batch()
5820            .await
5821            .unwrap();
5822
5823        let dot_distances: Vec<f32> = dot_batch
5824            .column_by_name(DIST_COL)
5825            .unwrap()
5826            .as_primitive::<Float32Type>()
5827            .values()
5828            .to_vec();
5829        let l2_distances: Vec<f32> = l2_batch
5830            .column_by_name(DIST_COL)
5831            .unwrap()
5832            .as_primitive::<Float32Type>()
5833            .values()
5834            .to_vec();
5835
5836        // Dot and L2 distances should be different (this verifies we're using the correct metric)
5837        assert_ne!(dot_distances, l2_distances);
5838    }
5839
5840    /// Test that when query does not specify a metric, we use the index's metric.
5841    /// Regression test for https://github.com/lance-format/lance/issues/5608
5842    #[tokio::test]
5843    async fn test_knn_no_metric_uses_index_metric() {
5844        let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, true)
5845            .await
5846            .unwrap();
5847        // Create IVF_PQ index with L2 metric
5848        test_ds.make_vector_index().await.unwrap();
5849
5850        let dataset = &test_ds.dataset;
5851        let key: Float32Array = (32..64).map(|v| v as f32).collect();
5852
5853        // Query without specifying metric
5854        let mut scan = dataset.scan();
5855        scan.nearest("vec", &key, 5).unwrap();
5856        // Don't call distance_metric() - should use index's L2
5857
5858        // Verify the explain plan shows ANNSubIndex with L2 metric
5859        let plan = scan.explain_plan(false).await.unwrap();
5860        assert!(
5861            plan.contains("ANNSubIndex") && plan.to_lowercase().contains("l2"),
5862            "Expected ANN index with L2 metric in plan:\n{}",
5863            plan
5864        );
5865    }
5866
5867    #[rstest]
5868    #[tokio::test]
5869    async fn test_only_row_id(
5870        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
5871        data_storage_version: LanceFileVersion,
5872    ) {
5873        let test_ds = TestVectorDataset::new(data_storage_version, false)
5874            .await
5875            .unwrap();
5876        let dataset = &test_ds.dataset;
5877
5878        let mut scan = dataset.scan();
5879        scan.project::<&str>(&[]).unwrap().with_row_id();
5880
5881        let batch = scan.try_into_batch().await.unwrap();
5882
5883        assert_eq!(batch.num_columns(), 1);
5884        assert_eq!(batch.num_rows(), 400);
5885        let expected_schema =
5886            ArrowSchema::new(vec![ArrowField::new(ROW_ID, DataType::UInt64, true)])
5887                .with_metadata(dataset.schema().metadata.clone());
5888        assert_eq!(batch.schema().as_ref(), &expected_schema,);
5889
5890        let expected_row_ids: Vec<u64> = (0..200_u64).chain((1 << 32)..((1 << 32) + 200)).collect();
5891        let actual_row_ids: Vec<u64> = as_primitive_array::<UInt64Type>(batch.column(0).as_ref())
5892            .values()
5893            .iter()
5894            .copied()
5895            .collect();
5896        assert_eq!(expected_row_ids, actual_row_ids);
5897    }
5898
5899    #[tokio::test]
5900    async fn test_scan_unordered_with_row_id() {
5901        // This test doesn't make sense for v2 files, there is no way to get an out-of-order scan
5902        let test_ds = TestVectorDataset::new(LanceFileVersion::Legacy, false)
5903            .await
5904            .unwrap();
5905        let dataset = &test_ds.dataset;
5906
5907        let mut scan = dataset.scan();
5908        scan.with_row_id();
5909
5910        let ordered_batches = scan
5911            .try_into_stream()
5912            .await
5913            .unwrap()
5914            .try_collect::<Vec<RecordBatch>>()
5915            .await
5916            .unwrap();
5917        assert!(ordered_batches.len() > 2);
5918        let ordered_batch =
5919            concat_batches(&ordered_batches[0].schema(), ordered_batches.iter()).unwrap();
5920
5921        // Attempt to get out-of-order scan, but that might take multiple attempts.
5922        scan.scan_in_order(false);
5923        for _ in 0..10 {
5924            let unordered_batches = scan
5925                .try_into_stream()
5926                .await
5927                .unwrap()
5928                .try_collect::<Vec<RecordBatch>>()
5929                .await
5930                .unwrap();
5931            let unordered_batch =
5932                concat_batches(&unordered_batches[0].schema(), unordered_batches.iter()).unwrap();
5933
5934            assert_eq!(ordered_batch.num_rows(), unordered_batch.num_rows());
5935
5936            // If they aren't equal, they should be equal if we sort by row id
5937            if ordered_batch != unordered_batch {
5938                let sort_indices = sort_to_indices(&unordered_batch[ROW_ID], None, None).unwrap();
5939
5940                let ordered_i = ordered_batch["i"].clone();
5941                let sorted_i = take::take(&unordered_batch["i"], &sort_indices, None).unwrap();
5942
5943                assert_eq!(&ordered_i, &sorted_i);
5944
5945                break;
5946            }
5947        }
5948    }
5949
5950    #[tokio::test]
5951    async fn test_scan_with_wildcard() {
5952        let data = gen_batch()
5953            .col("x", array::step::<Float64Type>())
5954            .col("y", array::step::<Float64Type>())
5955            .into_ram_dataset(FragmentCount::from(1), FragmentRowCount::from(100))
5956            .await
5957            .unwrap();
5958
5959        let check_cols = async |projection: &[&str], expected_cols: &[&str]| {
5960            let mut scan = data.scan();
5961            scan.project(projection).unwrap();
5962            let stream = scan.try_into_stream().await.unwrap();
5963            let schema = stream.schema();
5964            let field_names = schema.field_names();
5965            assert_eq!(field_names, expected_cols);
5966        };
5967
5968        check_cols(&["*"], &["x", "y"]).await;
5969        check_cols(&["x", "y"], &["x", "y"]).await;
5970        check_cols(&["x"], &["x"]).await;
5971        check_cols(&["_rowid", "*"], &["_rowid", "x", "y"]).await;
5972        check_cols(&["*", "_rowid"], &["x", "y", "_rowid"]).await;
5973        check_cols(
5974            &["_rowid", "*", "_rowoffset"],
5975            &["_rowid", "x", "y", "_rowoffset"],
5976        )
5977        .await;
5978
5979        let check_exprs = async |exprs: &[&str], expected_cols: &[&str]| {
5980            let mut scan = data.scan();
5981            let projection = exprs
5982                .iter()
5983                .map(|e| (e.to_string(), e.to_string()))
5984                .collect::<Vec<_>>();
5985            scan.project_with_transform(&projection).unwrap();
5986            let stream = scan.try_into_stream().await.unwrap();
5987            let schema = stream.schema();
5988            let field_names = schema.field_names();
5989            assert_eq!(field_names, expected_cols);
5990        };
5991
5992        // Make sure we can reference * fields in exprs and add new columns
5993        check_exprs(&["_rowid", "*", "x * 2"], &["_rowid", "x", "y", "x * 2"]).await;
5994
5995        let check_fails = |projection: &[&str]| {
5996            let mut scan = data.scan();
5997            assert!(scan.project(projection).is_err());
5998        };
5999
6000        // Would duplicate x
6001        check_fails(&["x", "*"]);
6002        check_fails(&["_rowid", "_rowid"]);
6003    }
6004
6005    #[rstest]
6006    #[tokio::test]
6007    async fn test_scan_order(
6008        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6009        data_storage_version: LanceFileVersion,
6010    ) {
6011        let test_dir = TempStrDir::default();
6012        let test_uri = &test_dir;
6013
6014        let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6015            "i",
6016            DataType::Int32,
6017            true,
6018        )]));
6019
6020        let batch1 = RecordBatch::try_new(
6021            schema.clone(),
6022            vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5]))],
6023        )
6024        .unwrap();
6025
6026        let batch2 = RecordBatch::try_new(
6027            schema.clone(),
6028            vec![Arc::new(Int32Array::from(vec![6, 7, 8]))],
6029        )
6030        .unwrap();
6031
6032        let params = WriteParams {
6033            mode: WriteMode::Append,
6034            data_storage_version: Some(data_storage_version),
6035            ..Default::default()
6036        };
6037
6038        let write_batch = |batch: RecordBatch| async {
6039            let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
6040            Dataset::write(reader, test_uri, Some(params)).await
6041        };
6042
6043        write_batch.clone()(batch1.clone()).await.unwrap();
6044        write_batch(batch2.clone()).await.unwrap();
6045
6046        let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6047        let fragment1 = dataset.get_fragment(0).unwrap().metadata().clone();
6048        let fragment2 = dataset.get_fragment(1).unwrap().metadata().clone();
6049
6050        // 1 then 2
6051        let mut scanner = dataset.scan();
6052        scanner.with_fragments(vec![fragment1.clone(), fragment2.clone()]);
6053        let output = scanner
6054            .try_into_stream()
6055            .await
6056            .unwrap()
6057            .try_collect::<Vec<_>>()
6058            .await
6059            .unwrap();
6060        assert_eq!(output.len(), 2);
6061        assert_eq!(output[0], batch1);
6062        assert_eq!(output[1], batch2);
6063
6064        // 2 then 1
6065        let mut scanner = dataset.scan();
6066        scanner.with_fragments(vec![fragment2, fragment1]);
6067        let output = scanner
6068            .try_into_stream()
6069            .await
6070            .unwrap()
6071            .try_collect::<Vec<_>>()
6072            .await
6073            .unwrap();
6074        assert_eq!(output.len(), 2);
6075        assert_eq!(output[0], batch2);
6076        assert_eq!(output[1], batch1);
6077    }
6078
6079    #[rstest]
6080    #[tokio::test]
6081    async fn test_scan_sort(
6082        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6083        data_storage_version: LanceFileVersion,
6084    ) {
6085        let test_dir = TempStrDir::default();
6086        let test_uri = &test_dir;
6087
6088        let data = gen_batch()
6089            .col("int", array::cycle::<Int32Type>(vec![5, 4, 1, 2, 3]))
6090            .col(
6091                "str",
6092                array::cycle_utf8_literals(&["a", "b", "c", "e", "d"]),
6093            );
6094
6095        let sorted_by_int = gen_batch()
6096            .col("int", array::cycle::<Int32Type>(vec![1, 2, 3, 4, 5]))
6097            .col(
6098                "str",
6099                array::cycle_utf8_literals(&["c", "e", "d", "b", "a"]),
6100            )
6101            .into_batch_rows(RowCount::from(5))
6102            .unwrap();
6103
6104        let sorted_by_str = gen_batch()
6105            .col("int", array::cycle::<Int32Type>(vec![5, 4, 1, 3, 2]))
6106            .col(
6107                "str",
6108                array::cycle_utf8_literals(&["a", "b", "c", "d", "e"]),
6109            )
6110            .into_batch_rows(RowCount::from(5))
6111            .unwrap();
6112
6113        Dataset::write(
6114            data.into_reader_rows(RowCount::from(5), BatchCount::from(1)),
6115            test_uri,
6116            Some(WriteParams {
6117                data_storage_version: Some(data_storage_version),
6118                ..Default::default()
6119            }),
6120        )
6121        .await
6122        .unwrap();
6123
6124        let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6125
6126        let batches_by_int = dataset
6127            .scan()
6128            .order_by(Some(vec![ColumnOrdering::asc_nulls_first(
6129                "int".to_string(),
6130            )]))
6131            .unwrap()
6132            .try_into_stream()
6133            .await
6134            .unwrap()
6135            .try_collect::<Vec<_>>()
6136            .await
6137            .unwrap();
6138
6139        assert_eq!(batches_by_int[0], sorted_by_int);
6140
6141        let batches_by_str = dataset
6142            .scan()
6143            .order_by(Some(vec![ColumnOrdering::asc_nulls_first(
6144                "str".to_string(),
6145            )]))
6146            .unwrap()
6147            .try_into_stream()
6148            .await
6149            .unwrap()
6150            .try_collect::<Vec<_>>()
6151            .await
6152            .unwrap();
6153
6154        assert_eq!(batches_by_str[0], sorted_by_str);
6155
6156        // Ensure an empty sort vec does not break anything (sorting is disabled)
6157        dataset
6158            .scan()
6159            .order_by(Some(vec![]))
6160            .unwrap()
6161            .try_into_stream()
6162            .await
6163            .unwrap()
6164            .try_collect::<Vec<_>>()
6165            .await
6166            .unwrap();
6167    }
6168
6169    #[rstest]
6170    #[tokio::test]
6171    async fn test_sort_multi_columns(
6172        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6173        data_storage_version: LanceFileVersion,
6174    ) {
6175        let test_dir = TempStrDir::default();
6176        let test_uri = &test_dir;
6177
6178        let data = gen_batch()
6179            .col("int", array::cycle::<Int32Type>(vec![5, 5, 1, 1, 3]))
6180            .col(
6181                "float",
6182                array::cycle::<Float32Type>(vec![7.3, -f32::NAN, f32::NAN, 4.3, f32::INFINITY]),
6183            );
6184
6185        let sorted_by_int_then_float = gen_batch()
6186            .col("int", array::cycle::<Int32Type>(vec![1, 1, 3, 5, 5]))
6187            .col(
6188                "float",
6189                // floats should be sorted using total order so -NAN is before all and NAN is after all
6190                array::cycle::<Float32Type>(vec![4.3, f32::NAN, f32::INFINITY, -f32::NAN, 7.3]),
6191            )
6192            .into_batch_rows(RowCount::from(5))
6193            .unwrap();
6194
6195        Dataset::write(
6196            data.into_reader_rows(RowCount::from(5), BatchCount::from(1)),
6197            test_uri,
6198            Some(WriteParams {
6199                data_storage_version: Some(data_storage_version),
6200                ..Default::default()
6201            }),
6202        )
6203        .await
6204        .unwrap();
6205
6206        let dataset = Arc::new(Dataset::open(test_uri).await.unwrap());
6207
6208        let batches_by_int_then_float = dataset
6209            .scan()
6210            .order_by(Some(vec![
6211                ColumnOrdering::asc_nulls_first("int".to_string()),
6212                ColumnOrdering::asc_nulls_first("float".to_string()),
6213            ]))
6214            .unwrap()
6215            .try_into_stream()
6216            .await
6217            .unwrap()
6218            .try_collect::<Vec<_>>()
6219            .await
6220            .unwrap();
6221
6222        assert_eq!(batches_by_int_then_float[0], sorted_by_int_then_float);
6223    }
6224
6225    #[rstest]
6226    #[tokio::test]
6227    async fn test_ann_prefilter(
6228        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6229        data_storage_version: LanceFileVersion,
6230        #[values(false, true)] stable_row_ids: bool,
6231        #[values(
6232            VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2),
6233            VectorIndexParams::with_ivf_hnsw_sq_params(
6234                MetricType::L2,
6235                IvfBuildParams::new(2),
6236                HnswBuildParams::default(),
6237                SQBuildParams::default()
6238            )
6239        )]
6240        index_params: VectorIndexParams,
6241    ) {
6242        use lance_arrow::{FixedSizeListArrayExt, fixed_size_list_type};
6243
6244        let test_dir = TempStrDir::default();
6245        let test_uri = &test_dir;
6246
6247        let schema = Arc::new(ArrowSchema::new(vec![
6248            ArrowField::new("filterable", DataType::Int32, true),
6249            ArrowField::new("vector", fixed_size_list_type(2, DataType::Float32), true),
6250        ]));
6251
6252        let vector_values = Float32Array::from_iter_values((0..600).map(|x| x as f32));
6253
6254        let batches = vec![
6255            RecordBatch::try_new(
6256                schema.clone(),
6257                vec![
6258                    Arc::new(Int32Array::from_iter_values(0..300)),
6259                    Arc::new(FixedSizeListArray::try_new_from_values(vector_values, 2).unwrap()),
6260                ],
6261            )
6262            .unwrap(),
6263        ];
6264
6265        let write_params = WriteParams {
6266            data_storage_version: Some(data_storage_version),
6267            max_rows_per_file: 300, // At least two files to make sure stable row ids make a difference
6268            enable_stable_row_ids: stable_row_ids,
6269            ..Default::default()
6270        };
6271        let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6272        let mut dataset = Dataset::write(batches, test_uri, Some(write_params))
6273            .await
6274            .unwrap();
6275
6276        dataset
6277            .create_index(&["vector"], IndexType::Vector, None, &index_params, false)
6278            .await
6279            .unwrap();
6280
6281        let query_key = Arc::new(Float32Array::from_iter_values((0..2).map(|x| x as f32)));
6282        let mut scan = dataset.scan();
6283        scan.filter("filterable > 5").unwrap();
6284        scan.nearest("vector", query_key.as_ref(), 1).unwrap();
6285        scan.minimum_nprobes(100);
6286        scan.ef(100);
6287        scan.with_row_id();
6288
6289        let batches = scan
6290            .try_into_stream()
6291            .await
6292            .unwrap()
6293            .try_collect::<Vec<_>>()
6294            .await
6295            .unwrap();
6296
6297        assert_eq!(batches.len(), 0);
6298
6299        scan.prefilter(true);
6300
6301        let batches = scan
6302            .try_into_stream()
6303            .await
6304            .unwrap()
6305            .try_collect::<Vec<_>>()
6306            .await
6307            .unwrap();
6308        assert_eq!(batches.len(), 1);
6309
6310        let first_match = batches[0][ROW_ID].as_primitive::<UInt64Type>().values()[0];
6311
6312        assert_eq!(6, first_match);
6313    }
6314
6315    #[rstest]
6316    #[tokio::test]
6317    async fn test_filter_on_large_utf8(
6318        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6319        data_storage_version: LanceFileVersion,
6320    ) {
6321        let test_dir = TempStrDir::default();
6322        let test_uri = &test_dir;
6323
6324        let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6325            "ls",
6326            DataType::LargeUtf8,
6327            true,
6328        )]));
6329
6330        let batches = vec![
6331            RecordBatch::try_new(
6332                schema.clone(),
6333                vec![Arc::new(LargeStringArray::from_iter_values(
6334                    (0..10).map(|v| format!("s-{}", v)),
6335                ))],
6336            )
6337            .unwrap(),
6338        ];
6339
6340        let write_params = WriteParams {
6341            data_storage_version: Some(data_storage_version),
6342            ..Default::default()
6343        };
6344        let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6345        Dataset::write(batches, test_uri, Some(write_params))
6346            .await
6347            .unwrap();
6348
6349        let dataset = Dataset::open(test_uri).await.unwrap();
6350        let mut scan = dataset.scan();
6351        scan.filter("ls = 's-8'").unwrap();
6352
6353        let batches = scan
6354            .try_into_stream()
6355            .await
6356            .unwrap()
6357            .try_collect::<Vec<_>>()
6358            .await
6359            .unwrap();
6360        let batch = &batches[0];
6361
6362        let expected = RecordBatch::try_new(
6363            schema.clone(),
6364            vec![Arc::new(LargeStringArray::from_iter_values(
6365                (8..9).map(|v| format!("s-{}", v)),
6366            ))],
6367        )
6368        .unwrap();
6369
6370        assert_eq!(batch, &expected);
6371    }
6372
6373    #[rstest]
6374    #[tokio::test]
6375    async fn test_filter_with_regex(
6376        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6377        data_storage_version: LanceFileVersion,
6378    ) {
6379        let test_dir = TempStrDir::default();
6380        let test_uri = &test_dir;
6381
6382        let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
6383            "ls",
6384            DataType::Utf8,
6385            true,
6386        )]));
6387
6388        let batches = vec![
6389            RecordBatch::try_new(
6390                schema.clone(),
6391                vec![Arc::new(StringArray::from_iter_values(
6392                    (0..20).map(|v| format!("s-{}", v)),
6393                ))],
6394            )
6395            .unwrap(),
6396        ];
6397
6398        let write_params = WriteParams {
6399            data_storage_version: Some(data_storage_version),
6400            ..Default::default()
6401        };
6402        let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6403        Dataset::write(batches, test_uri, Some(write_params))
6404            .await
6405            .unwrap();
6406
6407        let dataset = Dataset::open(test_uri).await.unwrap();
6408        let mut scan = dataset.scan();
6409        scan.filter("regexp_match(ls, 's-1.')").unwrap();
6410
6411        let stream = scan.try_into_stream().await.unwrap();
6412        let batches = stream.try_collect::<Vec<_>>().await.unwrap();
6413        let batch = &batches[0];
6414
6415        let expected = RecordBatch::try_new(
6416            schema.clone(),
6417            vec![Arc::new(StringArray::from_iter_values(
6418                (10..=19).map(|v| format!("s-{}", v)),
6419            ))],
6420        )
6421        .unwrap();
6422
6423        assert_eq!(batch, &expected);
6424    }
6425
6426    #[tokio::test]
6427    async fn test_filter_proj_bug() {
6428        let struct_i_field = ArrowField::new("i", DataType::Int32, true);
6429        let struct_o_field = ArrowField::new("o", DataType::Utf8, true);
6430        let schema = Arc::new(ArrowSchema::new(vec![
6431            ArrowField::new(
6432                "struct",
6433                DataType::Struct(vec![struct_i_field.clone(), struct_o_field.clone()].into()),
6434                true,
6435            ),
6436            ArrowField::new("s", DataType::Utf8, true),
6437        ]));
6438
6439        let input_batches: Vec<RecordBatch> = (0..5)
6440            .map(|i| {
6441                let struct_i_arr: Arc<Int32Array> =
6442                    Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20));
6443                let struct_o_arr: Arc<StringArray> = Arc::new(StringArray::from_iter_values(
6444                    (i * 20..(i + 1) * 20).map(|v| format!("o-{:02}", v)),
6445                ));
6446                RecordBatch::try_new(
6447                    schema.clone(),
6448                    vec![
6449                        Arc::new(StructArray::from(vec![
6450                            (Arc::new(struct_i_field.clone()), struct_i_arr as ArrayRef),
6451                            (Arc::new(struct_o_field.clone()), struct_o_arr as ArrayRef),
6452                        ])),
6453                        Arc::new(StringArray::from_iter_values(
6454                            (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
6455                        )),
6456                    ],
6457                )
6458                .unwrap()
6459            })
6460            .collect();
6461        let batches =
6462            RecordBatchIterator::new(input_batches.clone().into_iter().map(Ok), schema.clone());
6463        let test_dir = TempStrDir::default();
6464        let test_uri = &test_dir;
6465        let write_params = WriteParams {
6466            max_rows_per_file: 40,
6467            max_rows_per_group: 10,
6468            data_storage_version: Some(LanceFileVersion::Legacy),
6469            ..Default::default()
6470        };
6471        Dataset::write(batches, test_uri, Some(write_params))
6472            .await
6473            .unwrap();
6474
6475        let dataset = Dataset::open(test_uri).await.unwrap();
6476        let batches = dataset
6477            .scan()
6478            .filter("struct.i >= 20")
6479            .unwrap()
6480            .try_into_stream()
6481            .await
6482            .unwrap()
6483            .try_collect::<Vec<_>>()
6484            .await
6485            .unwrap();
6486        let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
6487
6488        let expected_batch = concat_batches(&schema, &input_batches.as_slice()[1..]).unwrap();
6489        assert_eq!(batch, expected_batch);
6490
6491        // different order
6492        let batches = dataset
6493            .scan()
6494            .filter("struct.o >= 'o-20'")
6495            .unwrap()
6496            .try_into_stream()
6497            .await
6498            .unwrap()
6499            .try_collect::<Vec<_>>()
6500            .await
6501            .unwrap();
6502        let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
6503        assert_eq!(batch, expected_batch);
6504
6505        // other reported bug with nested top level column access
6506        let batches = dataset
6507            .scan()
6508            .project(vec!["struct"].as_slice())
6509            .unwrap()
6510            .try_into_stream()
6511            .await
6512            .unwrap()
6513            .try_collect::<Vec<_>>()
6514            .await
6515            .unwrap();
6516        concat_batches(&batches[0].schema(), &batches).unwrap();
6517    }
6518
6519    #[rstest]
6520    #[tokio::test]
6521    async fn test_ann_with_deletion(
6522        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6523        data_storage_version: LanceFileVersion,
6524        #[values(false, true)] stable_row_ids: bool,
6525    ) {
6526        let vec_params = vec![
6527            // TODO: re-enable diskann test when we can tune to get reproducible results.
6528            // VectorIndexParams::with_diskann_params(MetricType::L2, DiskANNParams::new(10, 1.5, 10)),
6529            VectorIndexParams::ivf_pq(4, 8, 2, MetricType::L2, 2),
6530        ];
6531        for params in vec_params {
6532            use lance_arrow::FixedSizeListArrayExt;
6533
6534            let test_dir = TempStrDir::default();
6535            let test_uri = &test_dir;
6536
6537            // make dataset
6538            let schema = Arc::new(ArrowSchema::new(vec![
6539                ArrowField::new("i", DataType::Int32, true),
6540                ArrowField::new(
6541                    "vec",
6542                    DataType::FixedSizeList(
6543                        Arc::new(ArrowField::new("item", DataType::Float32, true)),
6544                        32,
6545                    ),
6546                    true,
6547                ),
6548            ]));
6549
6550            // vectors are [1, 1, 1, ...] [2, 2, 2, ...]
6551            let vector_values: Float32Array =
6552                (0..32 * 512).map(|v| (v / 32) as f32 + 1.0).collect();
6553            let vectors = FixedSizeListArray::try_new_from_values(vector_values, 32).unwrap();
6554
6555            let batches = vec![
6556                RecordBatch::try_new(
6557                    schema.clone(),
6558                    vec![
6559                        Arc::new(Int32Array::from_iter_values(0..512)),
6560                        Arc::new(vectors.clone()),
6561                    ],
6562                )
6563                .unwrap(),
6564            ];
6565
6566            let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6567            let mut dataset = Dataset::write(
6568                reader,
6569                test_uri,
6570                Some(WriteParams {
6571                    data_storage_version: Some(data_storage_version),
6572                    enable_stable_row_ids: stable_row_ids,
6573                    ..Default::default()
6574                }),
6575            )
6576            .await
6577            .unwrap();
6578
6579            assert_eq!(dataset.index_cache_entry_count().await, 0);
6580            dataset
6581                .create_index(
6582                    &["vec"],
6583                    IndexType::Vector,
6584                    Some("idx".to_string()),
6585                    &params,
6586                    true,
6587                )
6588                .await
6589                .unwrap();
6590
6591            let mut scan = dataset.scan();
6592            // closest be i = 0..5
6593            let key: Float32Array = (0..32).map(|_v| 1.0_f32).collect();
6594            scan.nearest("vec", &key, 5).unwrap();
6595            scan.refine(100);
6596            scan.minimum_nprobes(100);
6597
6598            assert_eq!(
6599                dataset.index_cache_entry_count().await,
6600                2, // 2 for index metadata at version 1 and 2.
6601            );
6602            let results = scan
6603                .try_into_stream()
6604                .await
6605                .unwrap()
6606                .try_collect::<Vec<_>>()
6607                .await
6608                .unwrap();
6609
6610            assert_eq!(
6611                dataset.index_cache_entry_count().await,
6612                5 + dataset.versions().await.unwrap().len()
6613            );
6614            assert_eq!(results.len(), 1);
6615            let batch = &results[0];
6616
6617            let expected_i = BTreeSet::from_iter(vec![0, 1, 2, 3, 4]);
6618            let column_i = batch.column_by_name("i").unwrap();
6619            let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6620                .values()
6621                .iter()
6622                .copied()
6623                .collect();
6624            assert_eq!(expected_i, actual_i);
6625
6626            // DELETE top result and search again
6627
6628            dataset.delete("i = 1").await.unwrap();
6629            let mut scan = dataset.scan();
6630            scan.nearest("vec", &key, 5).unwrap();
6631            scan.refine(100);
6632            scan.minimum_nprobes(100);
6633
6634            let results = scan
6635                .try_into_stream()
6636                .await
6637                .unwrap()
6638                .try_collect::<Vec<_>>()
6639                .await
6640                .unwrap();
6641
6642            assert_eq!(results.len(), 1);
6643            let batch = &results[0];
6644
6645            // i=1 was deleted, and 5 is the next best, the reset shouldn't change
6646            let expected_i = BTreeSet::from_iter(vec![0, 2, 3, 4, 5]);
6647            let column_i = batch.column_by_name("i").unwrap();
6648            let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6649                .values()
6650                .iter()
6651                .copied()
6652                .collect();
6653            assert_eq!(expected_i, actual_i);
6654
6655            // Add a second fragment and test the case where there are no deletion
6656            // files but there are missing fragments.
6657            let batches = vec![
6658                RecordBatch::try_new(
6659                    schema.clone(),
6660                    vec![
6661                        Arc::new(Int32Array::from_iter_values(512..1024)),
6662                        Arc::new(vectors),
6663                    ],
6664                )
6665                .unwrap(),
6666            ];
6667
6668            let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema.clone());
6669            let mut dataset = Dataset::write(
6670                reader,
6671                test_uri,
6672                Some(WriteParams {
6673                    mode: WriteMode::Append,
6674                    data_storage_version: Some(data_storage_version),
6675                    ..Default::default()
6676                }),
6677            )
6678            .await
6679            .unwrap();
6680            dataset
6681                .create_index(
6682                    &["vec"],
6683                    IndexType::Vector,
6684                    Some("idx".to_string()),
6685                    &params,
6686                    true,
6687                )
6688                .await
6689                .unwrap();
6690
6691            dataset.delete("i < 512").await.unwrap();
6692
6693            let mut scan = dataset.scan();
6694            scan.nearest("vec", &key, 5).unwrap();
6695            scan.refine(100);
6696            scan.minimum_nprobes(100);
6697
6698            let results = scan
6699                .try_into_stream()
6700                .await
6701                .unwrap()
6702                .try_collect::<Vec<_>>()
6703                .await
6704                .unwrap();
6705
6706            assert_eq!(results.len(), 1);
6707            let batch = &results[0];
6708
6709            // It should not pick up any results from the first fragment
6710            let expected_i = BTreeSet::from_iter(vec![512, 513, 514, 515, 516]);
6711            let column_i = batch.column_by_name("i").unwrap();
6712            let actual_i: BTreeSet<i32> = as_primitive_array::<Int32Type>(column_i.as_ref())
6713                .values()
6714                .iter()
6715                .copied()
6716                .collect();
6717            assert_eq!(expected_i, actual_i);
6718        }
6719    }
6720
6721    #[tokio::test]
6722    async fn test_projection_order() {
6723        let vec_params = VectorIndexParams::ivf_pq(4, 8, 2, MetricType::L2, 2);
6724        let mut data = gen_batch()
6725            .col("vec", array::rand_vec::<Float32Type>(Dimension::from(4)))
6726            .col("text", array::rand_utf8(ByteCount::from(10), false))
6727            .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(100))
6728            .await
6729            .unwrap();
6730        data.create_index(&["vec"], IndexType::Vector, None, &vec_params, true)
6731            .await
6732            .unwrap();
6733
6734        let mut scan = data.scan();
6735        scan.nearest("vec", &Float32Array::from(vec![1.0, 1.0, 1.0, 1.0]), 5)
6736            .unwrap();
6737        scan.with_row_id().project(&["text"]).unwrap();
6738
6739        let results = scan
6740            .try_into_stream()
6741            .await
6742            .unwrap()
6743            .try_collect::<Vec<_>>()
6744            .await
6745            .unwrap();
6746
6747        assert_eq!(
6748            results[0].schema().field_names(),
6749            vec!["text", "_distance", "_rowid"]
6750        );
6751    }
6752
6753    #[rstest]
6754    #[tokio::test]
6755    async fn test_count_rows_with_filter(
6756        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6757        data_storage_version: LanceFileVersion,
6758    ) {
6759        let test_dir = TempStrDir::default();
6760        let test_uri = &test_dir;
6761        let mut data_gen = BatchGenerator::new().col(Box::new(
6762            IncrementingInt32::new().named("Filter_me".to_owned()),
6763        ));
6764        Dataset::write(
6765            data_gen.batch(32),
6766            test_uri,
6767            Some(WriteParams {
6768                data_storage_version: Some(data_storage_version),
6769                ..Default::default()
6770            }),
6771        )
6772        .await
6773        .unwrap();
6774
6775        let dataset = Dataset::open(test_uri).await.unwrap();
6776        assert_eq!(32, dataset.count_rows(None).await.unwrap());
6777        assert_eq!(
6778            16,
6779            dataset
6780                .count_rows(Some("`Filter_me` > 15".to_string()))
6781                .await
6782                .unwrap()
6783        );
6784    }
6785
6786    #[rstest]
6787    #[tokio::test]
6788    async fn test_dynamic_projection(
6789        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6790        data_storage_version: LanceFileVersion,
6791    ) {
6792        let test_dir = TempStrDir::default();
6793        let test_uri = &test_dir;
6794        let mut data_gen =
6795            BatchGenerator::new().col(Box::new(IncrementingInt32::new().named("i".to_owned())));
6796        Dataset::write(
6797            data_gen.batch(32),
6798            test_uri,
6799            Some(WriteParams {
6800                data_storage_version: Some(data_storage_version),
6801                ..Default::default()
6802            }),
6803        )
6804        .await
6805        .unwrap();
6806
6807        let dataset = Dataset::open(test_uri).await.unwrap();
6808        assert_eq!(dataset.count_rows(None).await.unwrap(), 32);
6809
6810        let mut scanner = dataset.scan();
6811
6812        let scan_res = scanner
6813            .project_with_transform(&[("bool", "i > 15")])
6814            .unwrap()
6815            .try_into_batch()
6816            .await
6817            .unwrap();
6818
6819        assert_eq!(1, scan_res.num_columns());
6820
6821        let bool_col = scan_res
6822            .column_by_name("bool")
6823            .expect("bool column should exist");
6824        let bool_arr = bool_col.as_boolean();
6825        for i in 0..32 {
6826            if i > 15 {
6827                assert!(bool_arr.value(i));
6828            } else {
6829                assert!(!bool_arr.value(i));
6830            }
6831        }
6832    }
6833
6834    #[rstest]
6835    #[tokio::test]
6836    async fn test_column_casting_function(
6837        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
6838        data_storage_version: LanceFileVersion,
6839    ) {
6840        let test_dir = TempStrDir::default();
6841        let test_uri = &test_dir;
6842        let mut data_gen =
6843            BatchGenerator::new().col(Box::new(RandomVector::new().named("vec".to_owned())));
6844        Dataset::write(
6845            data_gen.batch(32),
6846            test_uri,
6847            Some(WriteParams {
6848                data_storage_version: Some(data_storage_version),
6849                ..Default::default()
6850            }),
6851        )
6852        .await
6853        .unwrap();
6854
6855        let dataset = Dataset::open(test_uri).await.unwrap();
6856        assert_eq!(dataset.count_rows(None).await.unwrap(), 32);
6857
6858        let mut scanner = dataset.scan();
6859
6860        let scan_res = scanner
6861            .project_with_transform(&[("f16", "_cast_list_f16(vec)")])
6862            .unwrap()
6863            .try_into_batch()
6864            .await
6865            .unwrap();
6866
6867        assert_eq!(1, scan_res.num_columns());
6868        assert_eq!(32, scan_res.num_rows());
6869        assert_eq!("f16", scan_res.schema().field(0).name());
6870
6871        let mut scanner = dataset.scan();
6872        let scan_res_original = scanner
6873            .project(&["vec"])
6874            .unwrap()
6875            .try_into_batch()
6876            .await
6877            .unwrap();
6878
6879        let f32_col: &Float32Array = scan_res_original
6880            .column_by_name("vec")
6881            .unwrap()
6882            .as_fixed_size_list()
6883            .values()
6884            .as_primitive();
6885        let f16_col: &Float16Array = scan_res
6886            .column_by_name("f16")
6887            .unwrap()
6888            .as_fixed_size_list()
6889            .values()
6890            .as_primitive();
6891
6892        for (f32_val, f16_val) in f32_col.iter().zip(f16_col.iter()) {
6893            let f32_val = f32_val.unwrap();
6894            let f16_val = f16_val.unwrap();
6895            assert_eq!(f16::from_f32(f32_val), f16_val);
6896        }
6897    }
6898
6899    struct ScalarIndexTestFixture {
6900        _test_dir: TempStrDir,
6901        dataset: Dataset,
6902        sample_query: Arc<dyn Array>,
6903        delete_query: Arc<dyn Array>,
6904        // The original version of the data, two fragments, rows 0-1000
6905        original_version: u64,
6906        // The original version of the data, 1 row deleted, compacted to a single fragment
6907        compact_version: u64,
6908        // The original version of the data + an extra 1000 unindexed
6909        append_version: u64,
6910        // The original version of the data + an extra 1000 rows, with indices updated so all rows indexed
6911        updated_version: u64,
6912        // The original version of the data with 1 deleted row
6913        delete_version: u64,
6914        // The original version of the data + an extra 1000 uindexed + 1 deleted row
6915        append_then_delete_version: u64,
6916    }
6917
6918    #[derive(Debug, PartialEq)]
6919    struct ScalarTestParams {
6920        use_index: bool,
6921        use_projection: bool,
6922        use_deleted_data: bool,
6923        use_new_data: bool,
6924        with_row_id: bool,
6925        use_compaction: bool,
6926        use_updated: bool,
6927    }
6928
6929    impl ScalarIndexTestFixture {
6930        async fn new(data_storage_version: LanceFileVersion, use_stable_row_ids: bool) -> Self {
6931            let test_dir = TempStrDir::default();
6932            let test_uri = &test_dir;
6933
6934            // Write 1000 rows.  Train indices.  Then write 1000 new rows with the same vector data.
6935            // Then delete a row from the trained data.
6936            //
6937            // The first row where indexed == 50 is our sample query.
6938            // The first row where indexed == 75 is our deleted row (and delete query)
6939            let data = gen_batch()
6940                .col(
6941                    "vector",
6942                    array::rand_vec::<Float32Type>(Dimension::from(32)),
6943                )
6944                .col("indexed", array::step::<Int32Type>())
6945                .col("not_indexed", array::step::<Int32Type>())
6946                .into_batch_rows(RowCount::from(1000))
6947                .unwrap();
6948
6949            // Write as two batches so we can later compact
6950            let mut dataset = Dataset::write(
6951                RecordBatchIterator::new(vec![Ok(data.clone())], data.schema().clone()),
6952                test_uri,
6953                Some(WriteParams {
6954                    max_rows_per_file: 500,
6955                    data_storage_version: Some(data_storage_version),
6956                    enable_stable_row_ids: use_stable_row_ids,
6957                    ..Default::default()
6958                }),
6959            )
6960            .await
6961            .unwrap();
6962
6963            dataset
6964                .create_index(
6965                    &["vector"],
6966                    IndexType::Vector,
6967                    None,
6968                    &VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 2),
6969                    false,
6970                )
6971                .await
6972                .unwrap();
6973
6974            dataset
6975                .create_index(
6976                    &["indexed"],
6977                    IndexType::Scalar,
6978                    None,
6979                    &ScalarIndexParams::default(),
6980                    false,
6981                )
6982                .await
6983                .unwrap();
6984
6985            let original_version = dataset.version().version;
6986            let sample_query = data["vector"].as_fixed_size_list().value(50);
6987            let delete_query = data["vector"].as_fixed_size_list().value(75);
6988
6989            // APPEND DATA
6990
6991            // Re-use the vector column in the new batch but add 1000 to the indexed/not_indexed columns so
6992            // they are distinct.  This makes our checks easier.
6993            let new_indexed =
6994                arrow_arith::numeric::add(&data["indexed"], &Int32Array::new_scalar(1000)).unwrap();
6995            let new_not_indexed =
6996                arrow_arith::numeric::add(&data["indexed"], &Int32Array::new_scalar(1000)).unwrap();
6997            let append_data = RecordBatch::try_new(
6998                data.schema(),
6999                vec![data["vector"].clone(), new_indexed, new_not_indexed],
7000            )
7001            .unwrap();
7002
7003            dataset
7004                .append(
7005                    RecordBatchIterator::new(vec![Ok(append_data)], data.schema()),
7006                    Some(WriteParams {
7007                        data_storage_version: Some(data_storage_version),
7008                        ..Default::default()
7009                    }),
7010                )
7011                .await
7012                .unwrap();
7013
7014            let append_version = dataset.version().version;
7015
7016            // UPDATE
7017
7018            dataset
7019                .optimize_indices(&OptimizeOptions::merge(1))
7020                .await
7021                .unwrap();
7022            let updated_version = dataset.version().version;
7023
7024            // APPEND -> DELETE
7025
7026            dataset.checkout_version(append_version).await.unwrap();
7027            dataset.restore().await.unwrap();
7028
7029            dataset.delete("not_indexed = 75").await.unwrap();
7030
7031            let append_then_delete_version = dataset.version().version;
7032
7033            // DELETE
7034
7035            let mut dataset = dataset.checkout_version(original_version).await.unwrap();
7036            dataset.restore().await.unwrap();
7037
7038            dataset.delete("not_indexed = 75").await.unwrap();
7039
7040            let delete_version = dataset.version().version;
7041
7042            // COMPACT (this should materialize the deletion)
7043            compact_files(&mut dataset, CompactionOptions::default(), None)
7044                .await
7045                .unwrap();
7046            let compact_version = dataset.version().version;
7047            dataset.checkout_version(original_version).await.unwrap();
7048            dataset.restore().await.unwrap();
7049
7050            Self {
7051                _test_dir: test_dir,
7052                dataset,
7053                sample_query,
7054                delete_query,
7055                original_version,
7056                compact_version,
7057                append_version,
7058                updated_version,
7059                delete_version,
7060                append_then_delete_version,
7061            }
7062        }
7063
7064        fn sample_query(&self) -> &PrimitiveArray<Float32Type> {
7065            self.sample_query.as_primitive::<Float32Type>()
7066        }
7067
7068        fn delete_query(&self) -> &PrimitiveArray<Float32Type> {
7069            self.delete_query.as_primitive::<Float32Type>()
7070        }
7071
7072        async fn get_dataset(&self, params: &ScalarTestParams) -> Dataset {
7073            let version = if params.use_compaction {
7074                // These combinations should not be possible
7075                if params.use_deleted_data || params.use_new_data || params.use_updated {
7076                    panic!(
7077                        "There is no test data combining new/deleted/updated data with compaction"
7078                    );
7079                } else {
7080                    self.compact_version
7081                }
7082            } else if params.use_updated {
7083                // These combinations should not be possible
7084                if params.use_deleted_data || params.use_new_data || params.use_compaction {
7085                    panic!(
7086                        "There is no test data combining updated data with new/deleted/compaction"
7087                    );
7088                } else {
7089                    self.updated_version
7090                }
7091            } else {
7092                match (params.use_new_data, params.use_deleted_data) {
7093                    (false, false) => self.original_version,
7094                    (false, true) => self.delete_version,
7095                    (true, false) => self.append_version,
7096                    (true, true) => self.append_then_delete_version,
7097                }
7098            };
7099            self.dataset.checkout_version(version).await.unwrap()
7100        }
7101
7102        async fn run_query(
7103            &self,
7104            query: &str,
7105            vector: Option<&PrimitiveArray<Float32Type>>,
7106            params: &ScalarTestParams,
7107        ) -> (String, RecordBatch) {
7108            let dataset = self.get_dataset(params).await;
7109            let mut scan = dataset.scan();
7110            if let Some(vector) = vector {
7111                scan.nearest("vector", vector, 10).unwrap();
7112            }
7113            if params.use_projection {
7114                scan.project(&["indexed"]).unwrap();
7115            }
7116            if params.with_row_id {
7117                scan.with_row_id();
7118            }
7119            scan.scan_in_order(true);
7120            scan.use_index(params.use_index);
7121            scan.filter(query).unwrap();
7122            scan.prefilter(true);
7123
7124            let plan = scan.explain_plan(true).await.unwrap();
7125            let batch = scan.try_into_batch().await.unwrap();
7126
7127            if params.use_projection {
7128                // 1 projected column
7129                let mut expected_columns = 1;
7130                if vector.is_some() {
7131                    // distance column if included always (TODO: it shouldn't)
7132                    expected_columns += 1;
7133                }
7134                if params.with_row_id {
7135                    expected_columns += 1;
7136                }
7137                assert_eq!(batch.num_columns(), expected_columns);
7138            } else {
7139                let mut expected_columns = 3;
7140                if vector.is_some() {
7141                    // distance column
7142                    expected_columns += 1;
7143                }
7144                if params.with_row_id {
7145                    expected_columns += 1;
7146                }
7147                // vector, indexed, not_indexed, _distance
7148                assert_eq!(batch.num_columns(), expected_columns);
7149            }
7150
7151            (plan, batch)
7152        }
7153
7154        fn assert_none<F: Fn(i32) -> bool>(
7155            &self,
7156            batch: &RecordBatch,
7157            predicate: F,
7158            message: &str,
7159        ) {
7160            let indexed = batch["indexed"].as_primitive::<Int32Type>();
7161            if indexed.iter().map(|val| val.unwrap()).any(predicate) {
7162                panic!("{}", message);
7163            }
7164        }
7165
7166        fn assert_one<F: Fn(i32) -> bool>(&self, batch: &RecordBatch, predicate: F, message: &str) {
7167            let indexed = batch["indexed"].as_primitive::<Int32Type>();
7168            if !indexed.iter().map(|val| val.unwrap()).any(predicate) {
7169                panic!("{}", message);
7170            }
7171        }
7172
7173        async fn check_vector_scalar_indexed_and_refine(&self, params: &ScalarTestParams) {
7174            let (query_plan, batch) = self
7175                .run_query(
7176                    "indexed != 50 AND ((not_indexed < 100) OR (not_indexed >= 1000 AND not_indexed < 1100))",
7177                    Some(self.sample_query()),
7178                    params,
7179                )
7180                .await;
7181            // Materialization is always required if there is a refine
7182            if self.dataset.is_legacy_storage() {
7183                assert!(query_plan.contains("MaterializeIndex"));
7184            }
7185            // The result should not include the sample query
7186            self.assert_none(
7187                &batch,
7188                |val| val == 50,
7189                "The query contained 50 even though it was filtered",
7190            );
7191            if !params.use_new_data {
7192                // Refine should have been applied
7193                self.assert_none(
7194                    &batch,
7195                    |val| (100..1000).contains(&val) || (val >= 1100),
7196                    "The non-indexed refine filter was not applied",
7197                );
7198            }
7199
7200            // If there is new data then the dupe of row 50 should be in the results
7201            if params.use_new_data || params.use_updated {
7202                self.assert_one(
7203                    &batch,
7204                    |val| val == 1050,
7205                    "The query did not contain 1050 from the new data",
7206                );
7207            }
7208        }
7209
7210        async fn check_vector_scalar_indexed_only(&self, params: &ScalarTestParams) {
7211            let (query_plan, batch) = self
7212                .run_query("indexed != 50", Some(self.sample_query()), params)
7213                .await;
7214            if self.dataset.is_legacy_storage() {
7215                if params.use_index {
7216                    // An ANN search whose prefilter is fully satisfied by the index should be
7217                    // able to use a ScalarIndexQuery
7218                    assert!(query_plan.contains("ScalarIndexQuery"));
7219                } else {
7220                    // A KNN search requires materialization of the index
7221                    assert!(query_plan.contains("MaterializeIndex"));
7222                }
7223            }
7224            // The result should not include the sample query
7225            self.assert_none(
7226                &batch,
7227                |val| val == 50,
7228                "The query contained 50 even though it was filtered",
7229            );
7230            // If there is new data then the dupe of row 50 should be in the results
7231            if params.use_new_data {
7232                self.assert_one(
7233                    &batch,
7234                    |val| val == 1050,
7235                    "The query did not contain 1050 from the new data",
7236                );
7237                if !params.use_new_data {
7238                    // Let's also make sure our filter can target something in the new data only
7239                    let (_, batch) = self
7240                        .run_query("indexed == 1050", Some(self.sample_query()), params)
7241                        .await;
7242                    assert_eq!(batch.num_rows(), 1);
7243                }
7244            }
7245            if params.use_deleted_data {
7246                let (_, batch) = self
7247                    .run_query("indexed == 75", Some(self.delete_query()), params)
7248                    .await;
7249                if !params.use_new_data {
7250                    assert_eq!(batch.num_rows(), 0);
7251                }
7252            }
7253        }
7254
7255        async fn check_vector_queries(&self, params: &ScalarTestParams) {
7256            self.check_vector_scalar_indexed_only(params).await;
7257            self.check_vector_scalar_indexed_and_refine(params).await;
7258        }
7259
7260        async fn check_simple_indexed_only(&self, params: &ScalarTestParams) {
7261            let (query_plan, batch) = self.run_query("indexed != 50", None, params).await;
7262            // Materialization is always required for non-vector search
7263            if self.dataset.is_legacy_storage() {
7264                assert!(query_plan.contains("MaterializeIndex"));
7265            } else {
7266                assert!(query_plan.contains("LanceRead"));
7267            }
7268            // The result should not include the targeted row
7269            self.assert_none(
7270                &batch,
7271                |val| val == 50,
7272                "The query contained 50 even though it was filtered",
7273            );
7274            let mut expected_num_rows = if params.use_new_data || params.use_updated {
7275                1999
7276            } else {
7277                999
7278            };
7279            if params.use_deleted_data || params.use_compaction {
7280                expected_num_rows -= 1;
7281            }
7282            assert_eq!(batch.num_rows(), expected_num_rows);
7283
7284            // Let's also make sure our filter can target something in the new data only
7285            if params.use_new_data || params.use_updated {
7286                let (_, batch) = self.run_query("indexed == 1050", None, params).await;
7287                assert_eq!(batch.num_rows(), 1);
7288            }
7289
7290            // Also make sure we don't return deleted data
7291            if params.use_deleted_data || params.use_compaction {
7292                let (_, batch) = self.run_query("indexed == 75", None, params).await;
7293                assert_eq!(batch.num_rows(), 0);
7294            }
7295        }
7296
7297        async fn check_simple_indexed_and_refine(&self, params: &ScalarTestParams) {
7298            let (query_plan, batch) = self.run_query(
7299                "indexed != 50 AND ((not_indexed < 100) OR (not_indexed >= 1000 AND not_indexed < 1100))",
7300                None,
7301                params
7302            ).await;
7303            // Materialization is always required for non-vector search
7304            if self.dataset.is_legacy_storage() {
7305                assert!(query_plan.contains("MaterializeIndex"));
7306            } else {
7307                assert!(query_plan.contains("LanceRead"));
7308            }
7309            // The result should not include the targeted row
7310            self.assert_none(
7311                &batch,
7312                |val| val == 50,
7313                "The query contained 50 even though it was filtered",
7314            );
7315            // The refine should be applied
7316            self.assert_none(
7317                &batch,
7318                |val| (100..1000).contains(&val) || (val >= 1100),
7319                "The non-indexed refine filter was not applied",
7320            );
7321
7322            let mut expected_num_rows = if params.use_new_data || params.use_updated {
7323                199
7324            } else {
7325                99
7326            };
7327            if params.use_deleted_data || params.use_compaction {
7328                expected_num_rows -= 1;
7329            }
7330            assert_eq!(batch.num_rows(), expected_num_rows);
7331        }
7332
7333        async fn check_simple_queries(&self, params: &ScalarTestParams) {
7334            self.check_simple_indexed_only(params).await;
7335            self.check_simple_indexed_and_refine(params).await;
7336        }
7337    }
7338
7339    // There are many different ways that a query can be run and they all have slightly different
7340    // effects on the plan that gets built.  This test attempts to run the same queries in various
7341    // different configurations to ensure that we get consistent results
7342    #[rstest]
7343    #[tokio::test]
7344    async fn test_secondary_index_scans(
7345        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
7346        data_storage_version: LanceFileVersion,
7347        #[values(false, true)] use_stable_row_ids: bool,
7348    ) {
7349        let fixture = Box::pin(ScalarIndexTestFixture::new(
7350            data_storage_version,
7351            use_stable_row_ids,
7352        ))
7353        .await;
7354
7355        for use_index in [false, true] {
7356            for use_projection in [false, true] {
7357                for use_deleted_data in [false, true] {
7358                    for use_new_data in [false, true] {
7359                        // Don't test compaction in conjunction with deletion and new data, it's too
7360                        // many combinations with no clear benefit.  Feel free to update if there is
7361                        // a need
7362                        // TODO: enable compaction for stable row id once supported.
7363                        let compaction_choices =
7364                            if use_deleted_data || use_new_data || use_stable_row_ids {
7365                                vec![false]
7366                            } else {
7367                                vec![false, true]
7368                            };
7369                        for use_compaction in compaction_choices {
7370                            let updated_choices =
7371                                if use_deleted_data || use_new_data || use_compaction {
7372                                    vec![false]
7373                                } else {
7374                                    vec![false, true]
7375                                };
7376                            for use_updated in updated_choices {
7377                                for with_row_id in [false, true] {
7378                                    let params = ScalarTestParams {
7379                                        use_index,
7380                                        use_projection,
7381                                        use_deleted_data,
7382                                        use_new_data,
7383                                        with_row_id,
7384                                        use_compaction,
7385                                        use_updated,
7386                                    };
7387                                    fixture.check_vector_queries(&params).await;
7388                                    fixture.check_simple_queries(&params).await;
7389                                }
7390                            }
7391                        }
7392                    }
7393                }
7394            }
7395        }
7396    }
7397
7398    #[tokio::test]
7399    async fn can_filter_row_id() {
7400        let dataset = lance_datagen::gen_batch()
7401            .col("x", array::step::<Int32Type>())
7402            .into_ram_dataset(FragmentCount::from(1), FragmentRowCount::from(1000))
7403            .await
7404            .unwrap();
7405
7406        let mut scan = dataset.scan();
7407        scan.with_row_id();
7408        scan.project::<&str>(&[]).unwrap();
7409        scan.filter("_rowid == 50").unwrap();
7410        let batch = scan.try_into_batch().await.unwrap();
7411        assert_eq!(batch.num_rows(), 1);
7412        assert_eq!(batch.column(0).as_primitive::<UInt64Type>().values()[0], 50);
7413    }
7414
7415    #[rstest]
7416    #[tokio::test]
7417    async fn test_index_take_batch_size() {
7418        let fixture = Box::pin(ScalarIndexTestFixture::new(LanceFileVersion::Stable, false)).await;
7419        let stream = fixture
7420            .dataset
7421            .scan()
7422            .filter("indexed > 0")
7423            .unwrap()
7424            .batch_size(16)
7425            .try_into_stream()
7426            .await
7427            .unwrap();
7428        let batches = stream.collect::<Vec<_>>().await;
7429        assert_eq!(batches.len(), 1000_usize.div_ceil(16));
7430    }
7431
7432    /// Assert that the plan when formatted matches the expected string.
7433    ///
7434    /// Within expected, you can use `...` to match any number of characters.
7435    async fn assert_plan_equals(
7436        dataset: &Dataset,
7437        plan: impl Fn(&mut Scanner) -> Result<&mut Scanner>,
7438        expected: &str,
7439    ) -> Result<()> {
7440        let mut scan = dataset.scan();
7441        plan(&mut scan)?;
7442        let exec_plan = scan.create_plan().await?;
7443        assert_plan_node_equals(exec_plan, expected).await
7444    }
7445
7446    #[tokio::test]
7447    async fn test_inexact_scalar_index_plans() {
7448        let data = gen_batch()
7449            .col("ngram", array::rand_utf8(ByteCount::from(5), false))
7450            .col("exact", array::rand_type(&DataType::UInt32))
7451            .col("no_index", array::rand_type(&DataType::UInt32))
7452            .into_reader_rows(RowCount::from(1000), BatchCount::from(5));
7453
7454        let mut dataset = Dataset::write(data, "memory://test", None).await.unwrap();
7455        dataset
7456            .create_index(
7457                &["ngram"],
7458                IndexType::NGram,
7459                None,
7460                &ScalarIndexParams::default(),
7461                true,
7462            )
7463            .await
7464            .unwrap();
7465        dataset
7466            .create_index(
7467                &["exact"],
7468                IndexType::BTree,
7469                None,
7470                &ScalarIndexParams::default(),
7471                true,
7472            )
7473            .await
7474            .unwrap();
7475
7476        // Simple in-exact filter
7477        assert_plan_equals(
7478            &dataset,
7479            |scanner| scanner.filter("contains(ngram, 'test string')"),
7480            "LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, \
7481             range_before=None, range_after=None, row_id=false, row_addr=false, \
7482             full_filter=contains(ngram, Utf8(\"test string\")), refine_filter=--
7483               ScalarIndexQuery: query=[contains(ngram, Utf8(\"test string\"))]@ngram_idx",
7484        )
7485        .await
7486        .unwrap();
7487
7488        // Combined with exact filter
7489        assert_plan_equals(
7490            &dataset,
7491            |scanner| scanner.filter("contains(ngram, 'test string') and exact < 50"),
7492            "LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, \
7493            range_before=None, range_after=None, row_id=false, row_addr=false, \
7494            full_filter=contains(ngram, Utf8(\"test string\")) AND exact < UInt32(50), \
7495            refine_filter=--
7496              ScalarIndexQuery: query=AND([contains(ngram, Utf8(\"test string\"))]@ngram_idx,[exact < 50]@exact_idx)",
7497        )
7498        .await
7499        .unwrap();
7500
7501        // All three filters
7502        assert_plan_equals(
7503            &dataset,
7504            |scanner| {
7505                scanner.filter("contains(ngram, 'test string') and exact < 50 AND no_index > 100")
7506            },
7507            "ProjectionExec: expr=[ngram@0 as ngram, exact@1 as exact, no_index@2 as no_index]
7508  LanceRead: uri=..., projection=[ngram, exact, no_index], num_fragments=1, range_before=None, \
7509  range_after=None, row_id=true, row_addr=false, full_filter=contains(ngram, Utf8(\"test string\")) AND exact < UInt32(50) AND no_index > UInt32(100), \
7510  refine_filter=no_index > UInt32(100)
7511    ScalarIndexQuery: query=AND([contains(ngram, Utf8(\"test string\"))]@ngram_idx,[exact < 50]@exact_idx)",
7512        )
7513        .await
7514        .unwrap();
7515    }
7516
7517    #[tokio::test]
7518    async fn test_like_prefix_with_btree_index() {
7519        // Create dataset with string data that has various prefixes
7520        // Avoid LIKE special characters (%, _) in data to keep tests simple
7521        let data = gen_batch()
7522            .col(
7523                "name",
7524                array::cycle_utf8_literals(&[
7525                    "apple",
7526                    "application",
7527                    "app",
7528                    "banana",
7529                    "band",
7530                    "testns1",
7531                    "testns2",
7532                    "test",
7533                    "testing",
7534                    "zoo",
7535                ]),
7536            )
7537            .col("id", array::step::<Int32Type>())
7538            .into_reader_rows(RowCount::from(100), BatchCount::from(1));
7539
7540        let mut dataset = Dataset::write(data, "memory://test_like", None)
7541            .await
7542            .unwrap();
7543
7544        // Create BTree index on string column
7545        dataset
7546            .create_index(
7547                &["name"],
7548                IndexType::BTree,
7549                None,
7550                &ScalarIndexParams::default(),
7551                true,
7552            )
7553            .await
7554            .unwrap();
7555
7556        // Test 1: Verify LIKE 'app%' uses scalar index and returns correct results
7557        assert_plan_equals(
7558            &dataset,
7559            |scanner| scanner.filter("name LIKE 'app%'"),
7560            "LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7561             range_before=None, range_after=None, row_id=false, row_addr=false, \
7562             full_filter=name LIKE Utf8(\"app%\"), refine_filter=--
7563               ScalarIndexQuery: query=[name LIKE 'app%']@name_idx",
7564        )
7565        .await
7566        .unwrap();
7567
7568        // Verify correct results for LIKE 'app%'
7569        let results = dataset
7570            .scan()
7571            .filter("name LIKE 'app%'")
7572            .unwrap()
7573            .try_into_batch()
7574            .await
7575            .unwrap();
7576        let names: Vec<&str> = results
7577            .column_by_name("name")
7578            .unwrap()
7579            .as_any()
7580            .downcast_ref::<StringArray>()
7581            .unwrap()
7582            .iter()
7583            .map(|s| s.unwrap())
7584            .collect();
7585        // Should match: apple, application, app (repeated in cycle)
7586        assert!(names.iter().all(|n| n.starts_with("app")));
7587        assert!(!names.is_empty());
7588
7589        // Test 2: Verify starts_with() uses scalar index (simple prefix without special chars)
7590        // Note: DataFusion optimizes starts_with() to LIKE before our index planning
7591        assert_plan_equals(
7592            &dataset,
7593            |scanner| scanner.filter("starts_with(name, 'ban')"),
7594            "LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7595             range_before=None, range_after=None, row_id=false, row_addr=false, \
7596             full_filter=name LIKE Utf8(\"ban%\"), refine_filter=--
7597               ScalarIndexQuery: query=[name LIKE 'ban%']@name_idx",
7598        )
7599        .await
7600        .unwrap();
7601
7602        // Verify correct results for starts_with
7603        let results = dataset
7604            .scan()
7605            .filter("starts_with(name, 'ban')")
7606            .unwrap()
7607            .try_into_batch()
7608            .await
7609            .unwrap();
7610        let names: Vec<&str> = results
7611            .column_by_name("name")
7612            .unwrap()
7613            .as_any()
7614            .downcast_ref::<StringArray>()
7615            .unwrap()
7616            .iter()
7617            .map(|s| s.unwrap())
7618            .collect();
7619        // Should match: banana, band
7620        assert!(names.iter().all(|n| n.starts_with("ban")));
7621        assert!(!names.is_empty());
7622
7623        // Test 3: LIKE with pattern requiring refine (e.g., 'test%2')
7624        assert_plan_equals(
7625            &dataset,
7626            |scanner| scanner.filter("name LIKE 'test%2'"),
7627            "ProjectionExec: expr=[name@0 as name, id@1 as id]
7628  LanceRead: uri=..., projection=[name, id], num_fragments=1, \
7629range_before=None, range_after=None, row_id=true, row_addr=false, \
7630full_filter=name LIKE Utf8(\"test%2\"), refine_filter=name LIKE Utf8(\"test%2\")
7631    ScalarIndexQuery: query=[name LIKE 'test%']@name_idx",
7632        )
7633        .await
7634        .unwrap();
7635
7636        // Verify correct results for LIKE 'test%2' (needs refine)
7637        let results = dataset
7638            .scan()
7639            .filter("name LIKE 'test%2'")
7640            .unwrap()
7641            .try_into_batch()
7642            .await
7643            .unwrap();
7644        let names: Vec<&str> = results
7645            .column_by_name("name")
7646            .unwrap()
7647            .as_any()
7648            .downcast_ref::<StringArray>()
7649            .unwrap()
7650            .iter()
7651            .map(|s| s.unwrap())
7652            .collect();
7653        // Should match: testns2 (ends with '2')
7654        assert!(
7655            names
7656                .iter()
7657                .all(|n| n.starts_with("test") && n.ends_with("2"))
7658        );
7659
7660        // Test 4: LIKE starting with wildcard should NOT use scalar index for pruning
7661        // Verify by checking the plan does NOT have ScalarIndexQuery
7662        let mut scanner = dataset.scan();
7663        scanner.filter("name LIKE '%app%'").unwrap();
7664        let plan = scanner.create_plan().await.unwrap();
7665        let plan_str = format!("{:?}", plan);
7666        assert!(
7667            !plan_str.contains("ScalarIndexQuery"),
7668            "LIKE '%app%' should not use scalar index, but got: {}",
7669            plan_str
7670        );
7671
7672        // Verify correct results for LIKE '%app%'
7673        let results = dataset
7674            .scan()
7675            .filter("name LIKE '%app%'")
7676            .unwrap()
7677            .try_into_batch()
7678            .await
7679            .unwrap();
7680        let names: Vec<&str> = results
7681            .column_by_name("name")
7682            .unwrap()
7683            .as_any()
7684            .downcast_ref::<StringArray>()
7685            .unwrap()
7686            .iter()
7687            .map(|s| s.unwrap())
7688            .collect();
7689        // Should match: apple, application, app (contain 'app')
7690        assert!(names.iter().all(|n| n.contains("app")));
7691
7692        // Test 5: NOT LIKE should NOT use scalar index
7693        let mut scanner = dataset.scan();
7694        scanner.filter("name NOT LIKE 'app%'").unwrap();
7695        let plan = scanner.create_plan().await.unwrap();
7696        let plan_str = format!("{:?}", plan);
7697        assert!(
7698            !plan_str.contains("ScalarIndexQuery"),
7699            "NOT LIKE should not use scalar index, but got: {}",
7700            plan_str
7701        );
7702    }
7703
7704    #[tokio::test]
7705    async fn test_like_prefix_correctness_with_btree_index() {
7706        // Create dataset with deterministic string data for exact result verification
7707        let names: Vec<&str> = vec![
7708            "alpha", "alphabet", "beta", "gamma", "delta", "epsilon", "eta", "theta", "iota",
7709            "kappa",
7710        ];
7711        let data = RecordBatch::try_new(
7712            Arc::new(ArrowSchema::new(vec![
7713                ArrowField::new("name", DataType::Utf8, false),
7714                ArrowField::new("id", DataType::Int32, false),
7715            ])),
7716            vec![
7717                Arc::new(StringArray::from(names.clone())),
7718                Arc::new(Int32Array::from_iter_values(0..10)),
7719            ],
7720        )
7721        .unwrap();
7722
7723        let reader = RecordBatchIterator::new(
7724            vec![Ok(data)],
7725            Arc::new(ArrowSchema::new(vec![
7726                ArrowField::new("name", DataType::Utf8, false),
7727                ArrowField::new("id", DataType::Int32, false),
7728            ])),
7729        );
7730
7731        let mut dataset = Dataset::write(reader, "memory://test_like_correctness", None)
7732            .await
7733            .unwrap();
7734
7735        // Create BTree index
7736        dataset
7737            .create_index(
7738                &["name"],
7739                IndexType::BTree,
7740                None,
7741                &ScalarIndexParams::default(),
7742                true,
7743            )
7744            .await
7745            .unwrap();
7746
7747        // Test with index
7748        let with_index = dataset
7749            .scan()
7750            .filter("name LIKE 'alpha%'")
7751            .unwrap()
7752            .try_into_batch()
7753            .await
7754            .unwrap();
7755
7756        // Test without index (for comparison)
7757        let without_index = dataset
7758            .scan()
7759            .use_scalar_index(false)
7760            .filter("name LIKE 'alpha%'")
7761            .unwrap()
7762            .try_into_batch()
7763            .await
7764            .unwrap();
7765
7766        // Both should return same results: alpha, alphabet
7767        assert_eq!(with_index.num_rows(), without_index.num_rows());
7768        assert_eq!(with_index.num_rows(), 2);
7769
7770        let with_index_names: BTreeSet<String> = with_index
7771            .column_by_name("name")
7772            .unwrap()
7773            .as_any()
7774            .downcast_ref::<StringArray>()
7775            .unwrap()
7776            .iter()
7777            .map(|s| s.unwrap().to_string())
7778            .collect();
7779
7780        let without_index_names: BTreeSet<String> = without_index
7781            .column_by_name("name")
7782            .unwrap()
7783            .as_any()
7784            .downcast_ref::<StringArray>()
7785            .unwrap()
7786            .iter()
7787            .map(|s| s.unwrap().to_string())
7788            .collect();
7789
7790        assert_eq!(with_index_names, without_index_names);
7791        assert_eq!(
7792            with_index_names,
7793            BTreeSet::from(["alpha".to_string(), "alphabet".to_string()])
7794        );
7795
7796        // Test starts_with correctness
7797        let starts_with_result = dataset
7798            .scan()
7799            .filter("starts_with(name, 'e')")
7800            .unwrap()
7801            .try_into_batch()
7802            .await
7803            .unwrap();
7804
7805        let starts_with_names: BTreeSet<String> = starts_with_result
7806            .column_by_name("name")
7807            .unwrap()
7808            .as_any()
7809            .downcast_ref::<StringArray>()
7810            .unwrap()
7811            .iter()
7812            .map(|s| s.unwrap().to_string())
7813            .collect();
7814
7815        // Should match: epsilon, eta
7816        assert_eq!(
7817            starts_with_names,
7818            BTreeSet::from(["epsilon".to_string(), "eta".to_string()])
7819        );
7820    }
7821
7822    #[tokio::test]
7823    async fn test_like_prefix_with_zone_map() {
7824        use lance_index::scalar::BuiltinIndexType;
7825
7826        // Create dataset with string data that has various prefixes
7827        let data = gen_batch()
7828            .col(
7829                "name",
7830                array::cycle_utf8_literals(&[
7831                    "apple",
7832                    "application",
7833                    "app",
7834                    "banana",
7835                    "band",
7836                    "testns1",
7837                    "testns2",
7838                    "test",
7839                    "testing",
7840                    "zoo",
7841                ]),
7842            )
7843            .col("id", array::step::<Int32Type>())
7844            .into_reader_rows(RowCount::from(100), BatchCount::from(1));
7845
7846        let mut dataset = Dataset::write(data, "memory://test_like_zonemap", None)
7847            .await
7848            .unwrap();
7849
7850        // Create ZoneMap index on string column
7851        let params = ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap);
7852        dataset
7853            .create_index(
7854                &["name"],
7855                IndexType::Scalar,
7856                Some("name_zonemap".to_string()),
7857                &params,
7858                true,
7859            )
7860            .await
7861            .unwrap();
7862
7863        // Test 1: Verify LIKE 'app%' uses zone map index
7864        let mut scanner = dataset.scan();
7865        scanner.filter("name LIKE 'app%'").unwrap();
7866        let plan = scanner.create_plan().await.unwrap();
7867        let plan_str = format!("{:?}", plan);
7868        // Zone map uses ScalarIndexExec with LikePrefix query
7869        assert!(
7870            plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
7871            "LIKE 'app%' should use zone map index with LikePrefix, but got: {}",
7872            plan_str
7873        );
7874
7875        // Verify correct results for LIKE 'app%'
7876        let results = dataset
7877            .scan()
7878            .filter("name LIKE 'app%'")
7879            .unwrap()
7880            .try_into_batch()
7881            .await
7882            .unwrap();
7883        let names: Vec<&str> = results
7884            .column_by_name("name")
7885            .unwrap()
7886            .as_any()
7887            .downcast_ref::<StringArray>()
7888            .unwrap()
7889            .iter()
7890            .map(|s| s.unwrap())
7891            .collect();
7892        assert!(names.iter().all(|n| n.starts_with("app")));
7893        assert!(!names.is_empty());
7894
7895        // Test 2: Verify starts_with() uses zone map index
7896        let mut scanner = dataset.scan();
7897        scanner.filter("starts_with(name, 'ban')").unwrap();
7898        let plan = scanner.create_plan().await.unwrap();
7899        let plan_str = format!("{:?}", plan);
7900        assert!(
7901            plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
7902            "starts_with should use zone map index with LikePrefix, but got: {}",
7903            plan_str
7904        );
7905
7906        // Verify correct results
7907        let results = dataset
7908            .scan()
7909            .filter("starts_with(name, 'ban')")
7910            .unwrap()
7911            .try_into_batch()
7912            .await
7913            .unwrap();
7914        let names: Vec<&str> = results
7915            .column_by_name("name")
7916            .unwrap()
7917            .as_any()
7918            .downcast_ref::<StringArray>()
7919            .unwrap()
7920            .iter()
7921            .map(|s| s.unwrap())
7922            .collect();
7923        assert!(names.iter().all(|n| n.starts_with("ban")));
7924
7925        // Test 3: LIKE with refine pattern still uses zone map for prefix pruning
7926        let mut scanner = dataset.scan();
7927        scanner.filter("name LIKE 'test%2'").unwrap();
7928        let plan = scanner.create_plan().await.unwrap();
7929        let plan_str = format!("{:?}", plan);
7930        assert!(
7931            plan_str.contains("ScalarIndexExec") && plan_str.contains("LikePrefix"),
7932            "LIKE 'test%2' should use zone map index for prefix, but got: {}",
7933            plan_str
7934        );
7935
7936        // Test 4: LIKE starting with wildcard should NOT use zone map
7937        let mut scanner = dataset.scan();
7938        scanner.filter("name LIKE '%app%'").unwrap();
7939        let plan = scanner.create_plan().await.unwrap();
7940        let plan_str = format!("{:?}", plan);
7941        assert!(
7942            !plan_str.contains("LikePrefix"),
7943            "LIKE '%app%' should not use LikePrefix index, but got: {}",
7944            plan_str
7945        );
7946    }
7947
7948    #[tokio::test]
7949    async fn test_like_prefix_correctness_with_zone_map() {
7950        use lance_index::scalar::BuiltinIndexType;
7951
7952        // Create dataset with deterministic string data for exact result verification
7953        let names: Vec<&str> = vec![
7954            "alpha", "alphabet", "beta", "gamma", "delta", "epsilon", "eta", "theta", "iota",
7955            "kappa",
7956        ];
7957        let data = RecordBatch::try_new(
7958            Arc::new(ArrowSchema::new(vec![
7959                ArrowField::new("name", DataType::Utf8, false),
7960                ArrowField::new("id", DataType::Int32, false),
7961            ])),
7962            vec![
7963                Arc::new(StringArray::from(names.clone())),
7964                Arc::new(Int32Array::from_iter_values(0..10)),
7965            ],
7966        )
7967        .unwrap();
7968
7969        let reader = RecordBatchIterator::new(
7970            vec![Ok(data)],
7971            Arc::new(ArrowSchema::new(vec![
7972                ArrowField::new("name", DataType::Utf8, false),
7973                ArrowField::new("id", DataType::Int32, false),
7974            ])),
7975        );
7976
7977        let mut dataset = Dataset::write(reader, "memory://test_like_correctness_zonemap", None)
7978            .await
7979            .unwrap();
7980
7981        // Create ZoneMap index
7982        let params = ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap);
7983        dataset
7984            .create_index(
7985                &["name"],
7986                IndexType::Scalar,
7987                Some("name_zonemap".to_string()),
7988                &params,
7989                true,
7990            )
7991            .await
7992            .unwrap();
7993
7994        // Test with zone map index
7995        let with_index = dataset
7996            .scan()
7997            .filter("name LIKE 'alpha%'")
7998            .unwrap()
7999            .try_into_batch()
8000            .await
8001            .unwrap();
8002
8003        // Test without index (for comparison)
8004        let without_index = dataset
8005            .scan()
8006            .use_scalar_index(false)
8007            .filter("name LIKE 'alpha%'")
8008            .unwrap()
8009            .try_into_batch()
8010            .await
8011            .unwrap();
8012
8013        // Both should return same results: alpha, alphabet
8014        assert_eq!(with_index.num_rows(), without_index.num_rows());
8015        assert_eq!(with_index.num_rows(), 2);
8016
8017        let with_index_names: BTreeSet<String> = with_index
8018            .column_by_name("name")
8019            .unwrap()
8020            .as_any()
8021            .downcast_ref::<StringArray>()
8022            .unwrap()
8023            .iter()
8024            .map(|s| s.unwrap().to_string())
8025            .collect();
8026
8027        let without_index_names: BTreeSet<String> = without_index
8028            .column_by_name("name")
8029            .unwrap()
8030            .as_any()
8031            .downcast_ref::<StringArray>()
8032            .unwrap()
8033            .iter()
8034            .map(|s| s.unwrap().to_string())
8035            .collect();
8036
8037        assert_eq!(with_index_names, without_index_names);
8038        assert_eq!(
8039            with_index_names,
8040            BTreeSet::from(["alpha".to_string(), "alphabet".to_string()])
8041        );
8042
8043        // Test starts_with correctness with zone map
8044        let starts_with_result = dataset
8045            .scan()
8046            .filter("starts_with(name, 'e')")
8047            .unwrap()
8048            .try_into_batch()
8049            .await
8050            .unwrap();
8051
8052        let starts_with_names: BTreeSet<String> = starts_with_result
8053            .column_by_name("name")
8054            .unwrap()
8055            .as_any()
8056            .downcast_ref::<StringArray>()
8057            .unwrap()
8058            .iter()
8059            .map(|s| s.unwrap().to_string())
8060            .collect();
8061
8062        // Should match: epsilon, eta
8063        assert_eq!(
8064            starts_with_names,
8065            BTreeSet::from(["epsilon".to_string(), "eta".to_string()])
8066        );
8067    }
8068
8069    #[rstest]
8070    #[tokio::test]
8071    async fn test_late_materialization(
8072        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8073        data_storage_version: LanceFileVersion,
8074    ) {
8075        use lance_io::assert_io_lt;
8076        // Create a large dataset with a scalar indexed column and a sorted but not scalar
8077        // indexed column
8078        use lance_table::io::commit::RenameCommitHandler;
8079        let data = gen_batch()
8080            .col(
8081                "vector",
8082                array::rand_vec::<Float32Type>(Dimension::from(32)),
8083            )
8084            .col("indexed", array::step::<Int32Type>())
8085            .col("not_indexed", array::step::<Int32Type>())
8086            .into_reader_rows(RowCount::from(1000), BatchCount::from(20));
8087
8088        let mut dataset = Dataset::write(
8089            data,
8090            "memory://test",
8091            Some(WriteParams {
8092                commit_handler: Some(Arc::new(RenameCommitHandler)),
8093                data_storage_version: Some(data_storage_version),
8094                ..Default::default()
8095            }),
8096        )
8097        .await
8098        .unwrap();
8099        dataset
8100            .create_index(
8101                &["indexed"],
8102                IndexType::Scalar,
8103                None,
8104                &ScalarIndexParams::default(),
8105                false,
8106            )
8107            .await
8108            .unwrap();
8109
8110        // First run a full scan to get a baseline
8111        let _ = dataset.object_store().io_stats_incremental(); // reset
8112        dataset.scan().try_into_batch().await.unwrap();
8113        let io_stats = dataset.object_store().io_stats_incremental();
8114        let full_scan_bytes = io_stats.read_bytes;
8115
8116        // Next do a scan without pushdown, we should still see a benefit from late materialization
8117        dataset
8118            .scan()
8119            .use_stats(false)
8120            .filter("not_indexed = 50")
8121            .unwrap()
8122            .try_into_batch()
8123            .await
8124            .unwrap();
8125        let io_stats = dataset.object_store().io_stats_incremental();
8126        assert_io_lt!(io_stats, read_bytes, full_scan_bytes);
8127        let filtered_scan_bytes = io_stats.read_bytes;
8128
8129        // Now do a scan with pushdown, the benefit should be even greater
8130        // Pushdown only works with the legacy format for now.
8131        if data_storage_version == LanceFileVersion::Legacy {
8132            dataset
8133                .scan()
8134                .filter("not_indexed = 50")
8135                .unwrap()
8136                .try_into_batch()
8137                .await
8138                .unwrap();
8139            let io_stats = dataset.object_store().io_stats_incremental();
8140            assert_io_lt!(io_stats, read_bytes, filtered_scan_bytes);
8141        }
8142
8143        // Now do a scalar index scan, this should be better than a
8144        // full scan but since we have to load the index might be more
8145        // expensive than late / pushdown scan
8146        dataset
8147            .scan()
8148            .filter("indexed = 50")
8149            .unwrap()
8150            .try_into_batch()
8151            .await
8152            .unwrap();
8153        let io_stats = dataset.object_store().io_stats_incremental();
8154        assert_io_lt!(io_stats, read_bytes, full_scan_bytes);
8155        let index_scan_bytes = io_stats.read_bytes;
8156
8157        // A second scalar index scan should be cheaper than the first
8158        // since we should have the index in cache
8159        dataset
8160            .scan()
8161            .filter("indexed = 50")
8162            .unwrap()
8163            .try_into_batch()
8164            .await
8165            .unwrap();
8166        let io_stats = dataset.object_store().io_stats_incremental();
8167        assert_io_lt!(io_stats, read_bytes, index_scan_bytes);
8168    }
8169
8170    #[rstest]
8171    #[tokio::test]
8172    async fn test_project_nested(
8173        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8174        data_storage_version: LanceFileVersion,
8175    ) -> Result<()> {
8176        let struct_i_field = ArrowField::new("i", DataType::Int32, true);
8177        let struct_o_field = ArrowField::new("o", DataType::Utf8, true);
8178        let schema = Arc::new(ArrowSchema::new(vec![
8179            ArrowField::new(
8180                "struct",
8181                DataType::Struct(vec![struct_i_field.clone(), struct_o_field.clone()].into()),
8182                true,
8183            ),
8184            ArrowField::new("s", DataType::Utf8, true),
8185        ]));
8186
8187        let input_batches: Vec<RecordBatch> = (0..5)
8188            .map(|i| {
8189                let struct_i_arr: Arc<Int32Array> =
8190                    Arc::new(Int32Array::from_iter_values(i * 20..(i + 1) * 20));
8191                let struct_o_arr: Arc<StringArray> = Arc::new(StringArray::from_iter_values(
8192                    (i * 20..(i + 1) * 20).map(|v| format!("o-{:02}", v)),
8193                ));
8194                RecordBatch::try_new(
8195                    schema.clone(),
8196                    vec![
8197                        Arc::new(StructArray::from(vec![
8198                            (Arc::new(struct_i_field.clone()), struct_i_arr as ArrayRef),
8199                            (Arc::new(struct_o_field.clone()), struct_o_arr as ArrayRef),
8200                        ])),
8201                        Arc::new(StringArray::from_iter_values(
8202                            (i * 20..(i + 1) * 20).map(|v| format!("s-{}", v)),
8203                        )),
8204                    ],
8205                )
8206                .unwrap()
8207            })
8208            .collect();
8209        let batches =
8210            RecordBatchIterator::new(input_batches.clone().into_iter().map(Ok), schema.clone());
8211        let test_dir = TempStrDir::default();
8212        let test_uri = &test_dir;
8213        let write_params = WriteParams {
8214            max_rows_per_file: 40,
8215            max_rows_per_group: 10,
8216            data_storage_version: Some(data_storage_version),
8217            ..Default::default()
8218        };
8219        Dataset::write(batches, test_uri, Some(write_params))
8220            .await
8221            .unwrap();
8222
8223        let dataset = Dataset::open(test_uri).await.unwrap();
8224
8225        let batches = dataset
8226            .scan()
8227            .project(&["struct.i"])
8228            .unwrap()
8229            .try_into_stream()
8230            .await
8231            .unwrap()
8232            .try_collect::<Vec<_>>()
8233            .await
8234            .unwrap();
8235        let batch = concat_batches(&batches[0].schema(), &batches).unwrap();
8236        assert!(batch.column_by_name("struct.i").is_some());
8237        Ok(())
8238    }
8239
8240    #[rstest]
8241    #[tokio::test]
8242    async fn test_plans(
8243        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
8244        data_storage_version: LanceFileVersion,
8245        #[values(false, true)] stable_row_id: bool,
8246    ) -> Result<()> {
8247        // Create a vector dataset
8248
8249        use lance_index::scalar::inverted::query::BoostQuery;
8250        let dim = 256;
8251        let mut dataset =
8252            TestVectorDataset::new_with_dimension(data_storage_version, stable_row_id, dim).await?;
8253        let lance_schema = dataset.dataset.schema();
8254
8255        // Scans
8256        // ---------------------------------------------------------------------
8257        // V2 writer does not use LancePushdownScan
8258        if data_storage_version == LanceFileVersion::Legacy {
8259            log::info!("Test case: Pushdown scan");
8260            assert_plan_equals(
8261                &dataset.dataset,
8262                |scan| scan.project(&["s"])?.filter("i > 10 and i < 20"),
8263                "LancePushdownScan: uri=..., projection=[s], predicate=i > Int32(10) AND i < Int32(20), row_id=false, row_addr=false, ordered=true"
8264            ).await?;
8265        }
8266
8267        log::info!("Test case: Project and filter");
8268        let expected = if data_storage_version == LanceFileVersion::Legacy {
8269            "ProjectionExec: expr=[s@2 as s]
8270  Take: columns=\"i, _rowid, (s)\"
8271    CoalesceBatchesExec: target_batch_size=8192
8272      FilterExec: i@0 > 10 AND i@0 < 20
8273        LanceScan: uri..., projection=[i], row_id=true, row_addr=false, ordered=true, range=None"
8274        } else {
8275            "ProjectionExec: expr=[s@2 as s]
8276  Take: columns=\"i, _rowid, (s)\"
8277    CoalesceBatchesExec: target_batch_size=8192
8278      LanceRead: ..., projection=[i], num_fragments=2, range_before=None, range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10) AND i < Int32(20), refine_filter=i > Int32(10) AND i < Int32(20)"
8279        };
8280        assert_plan_equals(
8281            &dataset.dataset,
8282            |scan| {
8283                scan.use_stats(false)
8284                    .project(&["s"])?
8285                    .filter("i > 10 and i < 20")
8286            },
8287            expected,
8288        )
8289        .await?;
8290
8291        // Integer fields will be eagerly materialized while string/vec fields
8292        // are not.
8293        log::info!("Test case: Late materialization");
8294        let expected = if data_storage_version == LanceFileVersion::Legacy {
8295            "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@3 as vec]
8296            Take: columns=\"i, s, _rowid, (vec)\"
8297              CoalesceBatchesExec: target_batch_size=8192
8298                FilterExec: s@1 IS NOT NULL
8299                  LanceScan: uri..., projection=[i, s], row_id=true, row_addr=false, ordered=true, range=None"
8300        } else {
8301            "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@3 as vec]
8302  Take: columns=\"i, s, _rowid, (vec)\"
8303    CoalesceBatchesExec: target_batch_size=8192
8304      LanceRead: uri=..., projection=[i, s], num_fragments=2, range_before=None, range_after=None, \
8305      row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8306        };
8307        assert_plan_equals(
8308            &dataset.dataset,
8309            |scan| scan.use_stats(false).filter("s IS NOT NULL"),
8310            expected,
8311        )
8312        .await?;
8313
8314        // Custom materialization
8315        log::info!("Test case: Custom materialization (all early)");
8316        let expected = if data_storage_version == LanceFileVersion::Legacy {
8317            "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@2 as vec]
8318  FilterExec: s@1 IS NOT NULL
8319    LanceScan: uri..., projection=[i, s, vec], row_id=true, row_addr=false, ordered=true, range=None"
8320        } else {
8321            "ProjectionExec: expr=[i@0 as i, s@1 as s, vec@2 as vec]
8322  LanceRead: uri=..., projection=[i, s, vec], num_fragments=2, range_before=None, \
8323  range_after=None, row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8324        };
8325        assert_plan_equals(
8326            &dataset.dataset,
8327            |scan| {
8328                scan.use_stats(false)
8329                    .materialization_style(MaterializationStyle::AllEarly)
8330                    .filter("s IS NOT NULL")
8331            },
8332            expected,
8333        )
8334        .await?;
8335
8336        log::info!("Test case: Custom materialization 2 (all late)");
8337        let expected = if data_storage_version == LanceFileVersion::Legacy {
8338            "ProjectionExec: expr=[i@2 as i, s@0 as s, vec@3 as vec]
8339  Take: columns=\"s, _rowid, (i), (vec)\"
8340    CoalesceBatchesExec: target_batch_size=8192
8341      FilterExec: s@0 IS NOT NULL
8342        LanceScan: uri..., projection=[s], row_id=true, row_addr=false, ordered=true, range=None"
8343        } else {
8344            "ProjectionExec: expr=[i@2 as i, s@0 as s, vec@3 as vec]
8345  Take: columns=\"s, _rowid, (i), (vec)\"
8346    CoalesceBatchesExec: target_batch_size=8192
8347      LanceRead: uri=..., projection=[s], num_fragments=2, range_before=None, \
8348      range_after=None, row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8349        };
8350        assert_plan_equals(
8351            &dataset.dataset,
8352            |scan| {
8353                scan.use_stats(false)
8354                    .materialization_style(MaterializationStyle::AllLate)
8355                    .filter("s IS NOT NULL")
8356            },
8357            expected,
8358        )
8359        .await?;
8360
8361        log::info!("Test case: Custom materialization 3 (mixed)");
8362        let expected = if data_storage_version == LanceFileVersion::Legacy {
8363            "ProjectionExec: expr=[i@3 as i, s@0 as s, vec@1 as vec]
8364  Take: columns=\"s, vec, _rowid, (i)\"
8365    CoalesceBatchesExec: target_batch_size=8192
8366      FilterExec: s@0 IS NOT NULL
8367        LanceScan: uri..., projection=[s, vec], row_id=true, row_addr=false, ordered=true, range=None"
8368        } else {
8369            "ProjectionExec: expr=[i@3 as i, s@0 as s, vec@1 as vec]
8370  Take: columns=\"s, vec, _rowid, (i)\"
8371    CoalesceBatchesExec: target_batch_size=8192
8372      LanceRead: uri=..., projection=[s, vec], num_fragments=2, range_before=None, range_after=None, \
8373      row_id=true, row_addr=false, full_filter=s IS NOT NULL, refine_filter=s IS NOT NULL"
8374        };
8375        assert_plan_equals(
8376            &dataset.dataset,
8377            |scan| {
8378                scan.use_stats(false)
8379                    .materialization_style(
8380                        MaterializationStyle::all_early_except(&["i"], lance_schema).unwrap(),
8381                    )
8382                    .filter("s IS NOT NULL")
8383            },
8384            expected,
8385        )
8386        .await?;
8387
8388        log::info!("Test case: Scan out of order");
8389        let expected = if data_storage_version == LanceFileVersion::Legacy {
8390            "LanceScan: uri=..., projection=[s], row_id=true, row_addr=false, ordered=false, range=None"
8391        } else {
8392            "LanceRead: uri=..., projection=[s], num_fragments=2, range_before=None, range_after=None, row_id=true, \
8393            row_addr=false, full_filter=--, refine_filter=--"
8394        };
8395        assert_plan_equals(
8396            &dataset.dataset,
8397            |scan| Ok(scan.project(&["s"])?.with_row_id().scan_in_order(false)),
8398            expected,
8399        )
8400        .await?;
8401
8402        // KNN
8403        // ---------------------------------------------------------------------
8404        let q: Float32Array = (32..32 + dim).map(|v| v as f32).collect();
8405        log::info!("Test case: Basic KNN");
8406        let expected = if data_storage_version == LanceFileVersion::Legacy {
8407            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8408  Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8409    CoalesceBatchesExec: target_batch_size=8192
8410      FilterExec: _distance@2 IS NOT NULL
8411        SortExec: TopK(fetch=5), expr=...
8412          KNNVectorDistance: metric=l2
8413            LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8414        } else {
8415            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8416  Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8417    CoalesceBatchesExec: target_batch_size=8192
8418      FilterExec: _distance@2 IS NOT NULL
8419        SortExec: TopK(fetch=5), expr=...
8420          KNNVectorDistance: metric=l2
8421            LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8422            row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8423        };
8424        assert_plan_equals(
8425            &dataset.dataset,
8426            |scan| scan.nearest("vec", &q, 5),
8427            expected,
8428        )
8429        .await?;
8430
8431        // KNN + Limit (arguably the user, or us, should fold the limit into the KNN but we don't today)
8432        // ---------------------------------------------------------------------
8433        let q: Float32Array = (32..32 + dim).map(|v| v as f32).collect();
8434        log::info!("Test case: KNN with extraneous limit");
8435        let expected = if data_storage_version == LanceFileVersion::Legacy {
8436            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8437  Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8438    CoalesceBatchesExec: target_batch_size=8192
8439      GlobalLimitExec: skip=0, fetch=1
8440        FilterExec: _distance@2 IS NOT NULL
8441          SortExec: TopK(fetch=5), expr=...
8442            KNNVectorDistance: metric=l2
8443              LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8444        } else {
8445            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8446  Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8447    CoalesceBatchesExec: target_batch_size=8192
8448      GlobalLimitExec: skip=0, fetch=1
8449        FilterExec: _distance@2 IS NOT NULL
8450          SortExec: TopK(fetch=5), expr=...
8451            KNNVectorDistance: metric=l2
8452              LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8453              row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8454        };
8455        assert_plan_equals(
8456            &dataset.dataset,
8457            |scan| scan.nearest("vec", &q, 5)?.limit(Some(1), None),
8458            expected,
8459        )
8460        .await?;
8461
8462        // ANN
8463        // ---------------------------------------------------------------------
8464        dataset.make_vector_index().await?;
8465        log::info!("Test case: Basic ANN");
8466        let expected =
8467            "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8468  Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8469    CoalesceBatchesExec: target_batch_size=8192
8470      SortExec: TopK(fetch=42), expr=...
8471        ANNSubIndex: name=..., k=42, deltas=1, metric=L2
8472          ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8473        assert_plan_equals(
8474            &dataset.dataset,
8475            |scan| scan.nearest("vec", &q, 42),
8476            expected,
8477        )
8478        .await?;
8479
8480        log::info!("Test case: ANN with refine");
8481        let expected =
8482            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8483  Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8484    CoalesceBatchesExec: target_batch_size=8192
8485      FilterExec: _distance@... IS NOT NULL
8486        SortExec: TopK(fetch=10), expr=...
8487          KNNVectorDistance: metric=l2
8488            Take: columns=\"_distance, _rowid, (vec)\"
8489              CoalesceBatchesExec: target_batch_size=8192
8490                SortExec: TopK(fetch=40), expr=...
8491                  ANNSubIndex: name=..., k=40, deltas=1, metric=L2
8492                    ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8493        assert_plan_equals(
8494            &dataset.dataset,
8495            |scan| Ok(scan.nearest("vec", &q, 10)?.refine(4)),
8496            expected,
8497        )
8498        .await?;
8499
8500        // use_index = False -> same plan as KNN
8501        log::info!("Test case: ANN with index disabled");
8502        let expected = if data_storage_version == LanceFileVersion::Legacy {
8503            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8504  Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8505    CoalesceBatchesExec: target_batch_size=8192
8506      FilterExec: _distance@... IS NOT NULL
8507        SortExec: TopK(fetch=13), expr=...
8508          KNNVectorDistance: metric=l2
8509            LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None"
8510        } else {
8511            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@0 as vec, _distance@2 as _distance]
8512  Take: columns=\"vec, _rowid, _distance, (i), (s)\"
8513    CoalesceBatchesExec: target_batch_size=8192
8514      FilterExec: _distance@... IS NOT NULL
8515        SortExec: TopK(fetch=13), expr=...
8516          KNNVectorDistance: metric=l2
8517            LanceRead: uri=..., projection=[vec], num_fragments=2, range_before=None, range_after=None, \
8518            row_id=true, row_addr=false, full_filter=--, refine_filter=--"
8519        };
8520        assert_plan_equals(
8521            &dataset.dataset,
8522            |scan| Ok(scan.nearest("vec", &q, 13)?.use_index(false)),
8523            expected,
8524        )
8525        .await?;
8526
8527        log::info!("Test case: ANN with postfilter");
8528        let expected = "ProjectionExec: expr=[s@3 as s, vec@4 as vec, _distance@0 as _distance, _rowid@1 as _rowid]
8529  Take: columns=\"_distance, _rowid, i, (s), (vec)\"
8530    CoalesceBatchesExec: target_batch_size=8192
8531      FilterExec: i@2 > 10
8532        Take: columns=\"_distance, _rowid, (i)\"
8533          CoalesceBatchesExec: target_batch_size=8192
8534            SortExec: TopK(fetch=17), expr=...
8535              ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8536                ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8537        assert_plan_equals(
8538            &dataset.dataset,
8539            |scan| {
8540                Ok(scan
8541                    .nearest("vec", &q, 17)?
8542                    .filter("i > 10")?
8543                    .project(&["s", "vec"])?
8544                    .with_row_id())
8545            },
8546            expected,
8547        )
8548        .await?;
8549
8550        log::info!("Test case: ANN with prefilter");
8551        let expected = if data_storage_version == LanceFileVersion::Legacy {
8552            "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8553  Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8554    CoalesceBatchesExec: target_batch_size=8192
8555      SortExec: TopK(fetch=17), expr=...
8556        ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8557          ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8558          FilterExec: i@0 > 10
8559            LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
8560        } else {
8561            "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8562  Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8563    CoalesceBatchesExec: target_batch_size=8192
8564      SortExec: TopK(fetch=17), expr=...
8565        ANNSubIndex: name=..., k=17, deltas=1, metric=L2
8566          ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8567          LanceRead: uri=..., projection=[], num_fragments=2, range_before=None, range_after=None, \
8568          row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)
8569"
8570        };
8571        assert_plan_equals(
8572            &dataset.dataset,
8573            |scan| {
8574                Ok(scan
8575                    .nearest("vec", &q, 17)?
8576                    .filter("i > 10")?
8577                    .prefilter(true))
8578            },
8579            expected,
8580        )
8581        .await?;
8582
8583        dataset.append_new_data().await?;
8584        log::info!("Test case: Combined KNN/ANN");
8585        let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8586  Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8587    CoalesceBatchesExec: target_batch_size=8192
8588      FilterExec: _distance@... IS NOT NULL
8589        SortExec: TopK(fetch=6), expr=...
8590          KNNVectorDistance: metric=l2
8591            RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8592              UnionExec
8593                ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
8594                  FilterExec: _distance@... IS NOT NULL
8595                    SortExec: TopK(fetch=6), expr=...
8596                      KNNVectorDistance: metric=l2
8597                        LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
8598                Take: columns=\"_distance, _rowid, (vec)\"
8599                  CoalesceBatchesExec: target_batch_size=8192
8600                    SortExec: TopK(fetch=6), expr=...
8601                      ANNSubIndex: name=..., k=6, deltas=1, metric=L2
8602                        ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8603        assert_plan_equals(
8604            &dataset.dataset,
8605            |scan| scan.nearest("vec", &q, 6),
8606            // TODO: we could write an optimizer rule to eliminate the last Projection
8607            // by doing it as part of the last Take. This would likely have minimal impact though.
8608            expected,
8609        )
8610        .await?;
8611
8612        // new data and with filter
8613        log::info!("Test case: Combined KNN/ANN with postfilter");
8614        let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8615  Take: columns=\"_rowid, vec, _distance, i, (s)\"
8616    CoalesceBatchesExec: target_batch_size=8192
8617      FilterExec: i@3 > 10
8618        Take: columns=\"_rowid, vec, _distance, (i)\"
8619          CoalesceBatchesExec: target_batch_size=8192
8620            FilterExec: _distance@... IS NOT NULL
8621              SortExec: TopK(fetch=15), expr=...
8622                KNNVectorDistance: metric=l2
8623                  RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8624                    UnionExec
8625                      ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
8626                        FilterExec: _distance@... IS NOT NULL
8627                          SortExec: TopK(fetch=15), expr=...
8628                            KNNVectorDistance: metric=l2
8629                              LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
8630                      Take: columns=\"_distance, _rowid, (vec)\"
8631                        CoalesceBatchesExec: target_batch_size=8192
8632                          SortExec: TopK(fetch=15), expr=...
8633                            ANNSubIndex: name=..., k=15, deltas=1, metric=L2
8634                              ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1";
8635        assert_plan_equals(
8636            &dataset.dataset,
8637            |scan| scan.nearest("vec", &q, 15)?.filter("i > 10"),
8638            expected,
8639        )
8640        .await?;
8641
8642        // new data and with prefilter
8643        log::info!("Test case: Combined KNN/ANN with prefilter");
8644        let expected = if data_storage_version == LanceFileVersion::Legacy {
8645            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8646  Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8647    CoalesceBatchesExec: target_batch_size=8192
8648      FilterExec: _distance@... IS NOT NULL
8649        SortExec: TopK(fetch=5), expr=...
8650          KNNVectorDistance: metric=l2
8651            RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8652              UnionExec
8653                ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
8654                  FilterExec: _distance@... IS NOT NULL
8655                    SortExec: TopK(fetch=5), expr=...
8656                      KNNVectorDistance: metric=l2
8657                        FilterExec: i@1 > 10
8658                          LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
8659                Take: columns=\"_distance, _rowid, (vec)\"
8660                  CoalesceBatchesExec: target_batch_size=8192
8661                    SortExec: TopK(fetch=5), expr=...
8662                      ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8663                        ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8664                        FilterExec: i@0 > 10
8665                          LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
8666        } else {
8667            "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8668  Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8669    CoalesceBatchesExec: target_batch_size=8192
8670      FilterExec: _distance@... IS NOT NULL
8671        SortExec: TopK(fetch=5), expr=...
8672          KNNVectorDistance: metric=l2
8673            RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8674              UnionExec
8675                ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
8676                  FilterExec: _distance@... IS NOT NULL
8677                    SortExec: TopK(fetch=5), expr=...
8678                      KNNVectorDistance: metric=l2
8679                        FilterExec: i@1 > 10
8680                          LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
8681                Take: columns=\"_distance, _rowid, (vec)\"
8682                  CoalesceBatchesExec: target_batch_size=8192
8683                    SortExec: TopK(fetch=5), expr=...
8684                      ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8685                        ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8686                        LanceRead: uri=..., projection=[], num_fragments=2, range_before=None, range_after=None, \
8687                          row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)"
8688        };
8689        assert_plan_equals(
8690            &dataset.dataset,
8691            |scan| {
8692                Ok(scan
8693                    .nearest("vec", &q, 5)?
8694                    .filter("i > 10")?
8695                    .prefilter(true))
8696            },
8697            // TODO: i is scanned on both sides but is projected away mid-plan
8698            // only to be taken again later. We should fix this.
8699            expected,
8700        )
8701        .await?;
8702
8703        // ANN with scalar index
8704        // ---------------------------------------------------------------------
8705        // Make sure both indices are up-to-date to start
8706        dataset.make_vector_index().await?;
8707        dataset.make_scalar_index().await?;
8708
8709        log::info!("Test case: ANN with scalar index");
8710        let expected =
8711            "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8712  Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8713    CoalesceBatchesExec: target_batch_size=8192
8714      SortExec: TopK(fetch=5), expr=...
8715        ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8716          ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8717          ScalarIndexQuery: query=[i > 10]@i_idx";
8718        assert_plan_equals(
8719            &dataset.dataset,
8720            |scan| {
8721                Ok(scan
8722                    .nearest("vec", &q, 5)?
8723                    .filter("i > 10")?
8724                    .prefilter(true))
8725            },
8726            expected,
8727        )
8728        .await?;
8729
8730        log::info!("Test case: ANN with scalar index disabled");
8731        let expected = if data_storage_version == LanceFileVersion::Legacy {
8732            "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8733  Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8734    CoalesceBatchesExec: target_batch_size=8192
8735      SortExec: TopK(fetch=5), expr=...
8736        ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8737          ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8738          FilterExec: i@0 > 10
8739            LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"
8740        } else {
8741            "ProjectionExec: expr=[i@2 as i, s@3 as s, vec@4 as vec, _distance@0 as _distance]
8742  Take: columns=\"_distance, _rowid, (i), (s), (vec)\"
8743    CoalesceBatchesExec: target_batch_size=8192
8744      SortExec: TopK(fetch=5), expr=...
8745        ANNSubIndex: name=..., k=5, deltas=1, metric=L2
8746          ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8747          LanceRead: uri=..., projection=[], num_fragments=3, range_before=None, \
8748          range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)"
8749        };
8750        assert_plan_equals(
8751            &dataset.dataset,
8752            |scan| {
8753                Ok(scan
8754                    .nearest("vec", &q, 5)?
8755                    .use_scalar_index(false)
8756                    .filter("i > 10")?
8757                    .prefilter(true))
8758            },
8759            expected,
8760        )
8761        .await?;
8762
8763        dataset.append_new_data().await?;
8764
8765        log::info!("Test case: Combined KNN/ANN with scalar index");
8766        let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8767  Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8768    CoalesceBatchesExec: target_batch_size=8192
8769      FilterExec: _distance@... IS NOT NULL
8770        SortExec: TopK(fetch=8), expr=...
8771          KNNVectorDistance: metric=l2
8772            RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8773              UnionExec
8774                ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
8775                  FilterExec: _distance@... IS NOT NULL
8776                    SortExec: TopK(fetch=8), expr=...
8777                      KNNVectorDistance: metric=l2
8778                        FilterExec: i@1 > 10
8779                          LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
8780                Take: columns=\"_distance, _rowid, (vec)\"
8781                  CoalesceBatchesExec: target_batch_size=8192
8782                    SortExec: TopK(fetch=8), expr=...
8783                      ANNSubIndex: name=..., k=8, deltas=1, metric=L2
8784                        ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8785                        ScalarIndexQuery: query=[i > 10]@i_idx";
8786        assert_plan_equals(
8787            &dataset.dataset,
8788            |scan| {
8789                Ok(scan
8790                    .nearest("vec", &q, 8)?
8791                    .filter("i > 10")?
8792                    .prefilter(true))
8793            },
8794            expected,
8795        )
8796        .await?;
8797
8798        // Update scalar index but not vector index
8799        log::info!(
8800            "Test case: Combined KNN/ANN with updated scalar index and outdated vector index"
8801        );
8802        let expected = "ProjectionExec: expr=[i@3 as i, s@4 as s, vec@1 as vec, _distance@2 as _distance]
8803  Take: columns=\"_rowid, vec, _distance, (i), (s)\"
8804    CoalesceBatchesExec: target_batch_size=8192
8805      FilterExec: _distance@... IS NOT NULL
8806        SortExec: TopK(fetch=11), expr=...
8807          KNNVectorDistance: metric=l2
8808            RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8809              UnionExec
8810                ProjectionExec: expr=[_distance@3 as _distance, _rowid@2 as _rowid, vec@0 as vec]
8811                  FilterExec: _distance@... IS NOT NULL
8812                    SortExec: TopK(fetch=11), expr=...
8813                      KNNVectorDistance: metric=l2
8814                        FilterExec: i@1 > 10
8815                          LanceScan: uri=..., projection=[vec, i], row_id=true, row_addr=false, ordered=false, range=None
8816                Take: columns=\"_distance, _rowid, (vec)\"
8817                  CoalesceBatchesExec: target_batch_size=8192
8818                    SortExec: TopK(fetch=11), expr=...
8819                      ANNSubIndex: name=..., k=11, deltas=1, metric=L2
8820                        ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1
8821                        ScalarIndexQuery: query=[i > 10]@i_idx";
8822        dataset.make_scalar_index().await?;
8823        assert_plan_equals(
8824            &dataset.dataset,
8825            |scan| {
8826                Ok(scan
8827                    .nearest("vec", &q, 11)?
8828                    .filter("i > 10")?
8829                    .prefilter(true))
8830            },
8831            expected,
8832        )
8833        .await?;
8834
8835        // Scans with scalar index
8836        // ---------------------------------------------------------------------
8837        log::info!("Test case: Filtered read with scalar index");
8838        let expected = if data_storage_version == LanceFileVersion::Legacy {
8839            "ProjectionExec: expr=[s@1 as s]
8840  Take: columns=\"_rowid, (s)\"
8841    CoalesceBatchesExec: target_batch_size=8192
8842      MaterializeIndex: query=[i > 10]@i_idx"
8843        } else {
8844            "LanceRead: uri=..., projection=[s], num_fragments=4, range_before=None, \
8845            range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
8846              ScalarIndexQuery: query=[i > 10]@i_idx"
8847        };
8848        assert_plan_equals(
8849            &dataset.dataset,
8850            |scan| scan.project(&["s"])?.filter("i > 10"),
8851            expected,
8852        )
8853        .await?;
8854
8855        if data_storage_version != LanceFileVersion::Legacy {
8856            log::info!(
8857                "Test case: Filtered read with scalar index disabled (late materialization)"
8858            );
8859            assert_plan_equals(
8860                &dataset.dataset,
8861                |scan| {
8862                    scan.project(&["s"])?
8863                        .use_scalar_index(false)
8864                        .filter("i > 10")
8865                },
8866                "ProjectionExec: expr=[s@2 as s]
8867  Take: columns=\"i, _rowid, (s)\"
8868    CoalesceBatchesExec: target_batch_size=8192
8869      LanceRead: uri=..., projection=[i], num_fragments=4, range_before=None, \
8870      range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=i > Int32(10)",
8871            )
8872            .await?;
8873        }
8874
8875        log::info!("Test case: Empty projection");
8876        let expected = if data_storage_version == LanceFileVersion::Legacy {
8877            "ProjectionExec: expr=[_rowaddr@0 as _rowaddr]
8878  AddRowAddrExec
8879    MaterializeIndex: query=[i > 10]@i_idx"
8880        } else {
8881            "LanceRead: uri=..., projection=[], num_fragments=4, range_before=None, \
8882            range_after=None, row_id=false, row_addr=true, full_filter=i > Int32(10), refine_filter=--
8883              ScalarIndexQuery: query=[i > 10]@i_idx"
8884        };
8885        assert_plan_equals(
8886            &dataset.dataset,
8887            |scan| {
8888                scan.filter("i > 10")
8889                    .unwrap()
8890                    .with_row_address()
8891                    .project::<&str>(&[])
8892            },
8893            expected,
8894        )
8895        .await?;
8896
8897        dataset.append_new_data().await?;
8898        log::info!("Test case: Combined Scalar/non-scalar filtered read");
8899        let expected = if data_storage_version == LanceFileVersion::Legacy {
8900            "ProjectionExec: expr=[s@1 as s]
8901  RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8902    UnionExec
8903      Take: columns=\"_rowid, (s)\"
8904        CoalesceBatchesExec: target_batch_size=8192
8905          MaterializeIndex: query=[i > 10]@i_idx
8906      ProjectionExec: expr=[_rowid@2 as _rowid, s@1 as s]
8907        FilterExec: i@0 > 10
8908          LanceScan: uri=..., projection=[i, s], row_id=true, row_addr=false, ordered=false, range=None"
8909        } else {
8910            "LanceRead: uri=..., projection=[s], num_fragments=5, range_before=None, \
8911            range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
8912              ScalarIndexQuery: query=[i > 10]@i_idx"
8913        };
8914        assert_plan_equals(
8915            &dataset.dataset,
8916            |scan| scan.project(&["s"])?.filter("i > 10"),
8917            expected,
8918        )
8919        .await?;
8920
8921        log::info!("Test case: Combined Scalar/non-scalar filtered read with empty projection");
8922        let expected = if data_storage_version == LanceFileVersion::Legacy {
8923            "ProjectionExec: expr=[_rowaddr@0 as _rowaddr]
8924  RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8925    UnionExec
8926      AddRowAddrExec
8927        MaterializeIndex: query=[i > 10]@i_idx
8928      ProjectionExec: expr=[_rowaddr@2 as _rowaddr, _rowid@1 as _rowid]
8929        FilterExec: i@0 > 10
8930          LanceScan: uri=..., projection=[i], row_id=true, row_addr=true, ordered=false, range=None"
8931        } else {
8932            "LanceRead: uri=..., projection=[], num_fragments=5, range_before=None, \
8933            range_after=None, row_id=false, row_addr=true, full_filter=i > Int32(10), refine_filter=--
8934              ScalarIndexQuery: query=[i > 10]@i_idx"
8935        };
8936        assert_plan_equals(
8937            &dataset.dataset,
8938            |scan| {
8939                scan.filter("i > 10")
8940                    .unwrap()
8941                    .with_row_address()
8942                    .project::<&str>(&[])
8943            },
8944            expected,
8945        )
8946        .await?;
8947
8948        // Scans with dynamic projection
8949        // When an expression is specified in the projection, the plan should include a ProjectionExec
8950        log::info!("Test case: Dynamic projection");
8951        let expected = if data_storage_version == LanceFileVersion::Legacy {
8952            "ProjectionExec: expr=[regexp_match(s@1, .*) as matches]
8953  RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
8954    UnionExec
8955      Take: columns=\"_rowid, (s)\"
8956        CoalesceBatchesExec: target_batch_size=8192
8957          MaterializeIndex: query=[i > 10]@i_idx
8958      ProjectionExec: expr=[_rowid@2 as _rowid, s@1 as s]
8959        FilterExec: i@0 > 10
8960          LanceScan: uri=..., row_id=true, row_addr=false, ordered=false, range=None"
8961        } else {
8962            "ProjectionExec: expr=[regexp_match(s@0, .*) as matches]
8963  LanceRead: uri=..., projection=[s], num_fragments=5, range_before=None, \
8964  range_after=None, row_id=false, row_addr=false, full_filter=i > Int32(10), refine_filter=--
8965    ScalarIndexQuery: query=[i > 10]@i_idx"
8966        };
8967        assert_plan_equals(
8968            &dataset.dataset,
8969            |scan| {
8970                scan.project_with_transform(&[("matches", "regexp_match(s, \".*\")")])?
8971                    .filter("i > 10")
8972            },
8973            expected,
8974        )
8975        .await?;
8976
8977        // FTS
8978        // ---------------------------------------------------------------------
8979        // All rows are indexed
8980        dataset.make_fts_index().await?;
8981        log::info!("Test case: Full text search (match query)");
8982        let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
8983  Take: columns="_rowid, _score, (s)"
8984    CoalesceBatchesExec: target_batch_size=8192
8985      MatchQuery: column=s, query=hello"#;
8986        assert_plan_equals(
8987            &dataset.dataset,
8988            |scan| {
8989                scan.project(&["s"])?
8990                    .with_row_id()
8991                    .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
8992            },
8993            expected,
8994        )
8995        .await?;
8996
8997        log::info!("Test case: Full text search (phrase query)");
8998        let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
8999  Take: columns="_rowid, _score, (s)"
9000    CoalesceBatchesExec: target_batch_size=8192
9001      PhraseQuery: column=s, query=hello world"#;
9002        assert_plan_equals(
9003            &dataset.dataset,
9004            |scan| {
9005                let query = PhraseQuery::new("hello world".to_owned());
9006                scan.project(&["s"])?
9007                    .with_row_id()
9008                    .full_text_search(FullTextSearchQuery::new_query(query.into()))
9009            },
9010            expected,
9011        )
9012        .await?;
9013
9014        log::info!("Test case: Full text search (boost query)");
9015        let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9016  Take: columns="_rowid, _score, (s)"
9017    CoalesceBatchesExec: target_batch_size=8192
9018      BoostQuery: negative_boost=1
9019        MatchQuery: column=s, query=hello
9020        MatchQuery: column=s, query=world"#;
9021        assert_plan_equals(
9022            &dataset.dataset,
9023            |scan| {
9024                let positive =
9025                    MatchQuery::new("hello".to_owned()).with_column(Some("s".to_owned()));
9026                let negative =
9027                    MatchQuery::new("world".to_owned()).with_column(Some("s".to_owned()));
9028                let query = BoostQuery::new(positive.into(), negative.into(), Some(1.0));
9029                scan.project(&["s"])?
9030                    .with_row_id()
9031                    .full_text_search(FullTextSearchQuery::new_query(query.into()))
9032            },
9033            expected,
9034        )
9035        .await?;
9036
9037        log::info!("Test case: Full text search with prefilter");
9038        let expected = if data_storage_version == LanceFileVersion::Legacy {
9039            r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9040  Take: columns="_rowid, _score, (s)"
9041    CoalesceBatchesExec: target_batch_size=8192
9042      MatchQuery: column=s, query=hello
9043        RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9044          UnionExec
9045            MaterializeIndex: query=[i > 10]@i_idx
9046            ProjectionExec: expr=[_rowid@1 as _rowid]
9047              FilterExec: i@0 > 10
9048                LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None"#
9049        } else {
9050            r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9051  Take: columns="_rowid, _score, (s)"
9052    CoalesceBatchesExec: target_batch_size=8192
9053      MatchQuery: column=s, query=hello
9054        LanceRead: uri=..., projection=[], num_fragments=5, range_before=None, range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=--
9055          ScalarIndexQuery: query=[i > 10]@i_idx"#
9056        };
9057        assert_plan_equals(
9058            &dataset.dataset,
9059            |scan| {
9060                scan.project(&["s"])?
9061                    .with_row_id()
9062                    .filter("i > 10")?
9063                    .prefilter(true)
9064                    .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9065            },
9066            expected,
9067        )
9068        .await?;
9069
9070        log::info!("Test case: Full text search with unindexed rows");
9071        let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9072  Take: columns="_rowid, _score, (s)"
9073    CoalesceBatchesExec: target_batch_size=8192
9074      SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9075        RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9076          UnionExec
9077            MatchQuery: column=s, query=hello
9078            FlatMatchQuery: column=s, query=hello
9079              LanceScan: uri=..., projection=[s], row_id=true, row_addr=false, ordered=false, range=None"#;
9080        dataset.append_new_data().await?;
9081        assert_plan_equals(
9082            &dataset.dataset,
9083            |scan| {
9084                scan.project(&["s"])?
9085                    .with_row_id()
9086                    .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9087            },
9088            expected,
9089        )
9090        .await?;
9091
9092        log::info!("Test case: Full text search with unindexed rows and fast_search");
9093        let expected = r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9094  Take: columns="_rowid, _score, (s)"
9095    CoalesceBatchesExec: target_batch_size=8192
9096      MatchQuery: column=s, query=hello"#;
9097        assert_plan_equals(
9098            &dataset.dataset,
9099            |scan| {
9100                let scan = scan
9101                    .project(&["s"])?
9102                    .with_row_id()
9103                    .full_text_search(FullTextSearchQuery::new("hello".to_owned()))?;
9104                scan.fast_search();
9105                Ok(scan)
9106            },
9107            expected,
9108        )
9109        .await?;
9110
9111        log::info!("Test case: Full text search with unindexed rows and prefilter");
9112        let expected = if data_storage_version == LanceFileVersion::Legacy {
9113            r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9114  Take: columns="_rowid, _score, (s)"
9115    CoalesceBatchesExec: target_batch_size=8192
9116      SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9117        RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9118          UnionExec
9119            MatchQuery: column=s, query=hello
9120              RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9121                UnionExec
9122                  MaterializeIndex: query=[i > 10]@i_idx
9123                  ProjectionExec: expr=[_rowid@1 as _rowid]
9124                    FilterExec: i@0 > 10
9125                      LanceScan: uri=..., projection=[i], row_id=true, row_addr=false, ordered=false, range=None
9126            FlatMatchQuery: column=s, query=hello
9127              FilterExec: i@1 > 10
9128                LanceScan: uri=..., projection=[s, i], row_id=true, row_addr=false, ordered=false, range=None"#
9129        } else {
9130            r#"ProjectionExec: expr=[s@2 as s, _score@1 as _score, _rowid@0 as _rowid]
9131  Take: columns="_rowid, _score, (s)"
9132    CoalesceBatchesExec: target_batch_size=8192
9133      SortExec: expr=[_score@1 DESC NULLS LAST], preserve_partitioning=[false]
9134        RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9135          UnionExec
9136            MatchQuery: column=s, query=hello
9137              LanceRead: uri=..., projection=[], num_fragments=5, range_before=None, range_after=None, row_id=true, row_addr=false, full_filter=i > Int32(10), refine_filter=--
9138                ScalarIndexQuery: query=[i > 10]@i_idx
9139            FlatMatchQuery: column=s, query=hello
9140              FilterExec: i@1 > 10
9141                LanceScan: uri=..., projection=[s, i], row_id=true, row_addr=false, ordered=false, range=None"#
9142        };
9143        assert_plan_equals(
9144            &dataset.dataset,
9145            |scan| {
9146                scan.project(&["s"])?
9147                    .with_row_id()
9148                    .filter("i > 10")?
9149                    .prefilter(true)
9150                    .full_text_search(FullTextSearchQuery::new("hello".to_owned()))
9151            },
9152            expected,
9153        )
9154        .await?;
9155
9156        Ok(())
9157    }
9158
9159    #[tokio::test]
9160    async fn test_fast_search_plan() {
9161        // Create a vector dataset
9162        let mut dataset = TestVectorDataset::new(LanceFileVersion::Stable, true)
9163            .await
9164            .unwrap();
9165        dataset.make_vector_index().await.unwrap();
9166        dataset.append_new_data().await.unwrap();
9167
9168        let q: Float32Array = (32..64).map(|v| v as f32).collect();
9169
9170        assert_plan_equals(
9171            &dataset.dataset,
9172            |scan| {
9173                scan.nearest("vec", &q, 32)?
9174                    .fast_search()
9175                    .project(&["_distance", "_rowid"])
9176            },
9177            "SortExec: TopK(fetch=32), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9178    ANNSubIndex: name=idx, k=32, deltas=1, metric=L2
9179      ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9180        )
9181        .await
9182        .unwrap();
9183
9184        assert_plan_equals(
9185            &dataset.dataset,
9186            |scan| {
9187                scan.nearest("vec", &q, 33)?
9188                    .fast_search()
9189                    .with_row_id()
9190                    .project(&["_distance", "_rowid"])
9191            },
9192            "SortExec: TopK(fetch=33), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9193    ANNSubIndex: name=idx, k=33, deltas=1, metric=L2
9194      ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9195        )
9196        .await
9197        .unwrap();
9198
9199        // Not `fast_scan` case
9200        assert_plan_equals(
9201            &dataset.dataset,
9202            |scan| {
9203                scan.nearest("vec", &q, 34)?
9204                    .with_row_id()
9205                    .project(&["_distance", "_rowid"])
9206            },
9207            "ProjectionExec: expr=[_distance@2 as _distance, _rowid@0 as _rowid]
9208  FilterExec: _distance@2 IS NOT NULL
9209    SortExec: TopK(fetch=34), expr=[_distance@2 ASC NULLS LAST, _rowid@0 ASC NULLS LAST]...
9210      KNNVectorDistance: metric=l2
9211        RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=2
9212          UnionExec
9213            ProjectionExec: expr=[_distance@2 as _distance, _rowid@1 as _rowid, vec@0 as vec]
9214              FilterExec: _distance@2 IS NOT NULL
9215                SortExec: TopK(fetch=34), expr=[_distance@2 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9216                  KNNVectorDistance: metric=l2
9217                    LanceScan: uri=..., projection=[vec], row_id=true, row_addr=false, ordered=false, range=None
9218            Take: columns=\"_distance, _rowid, (vec)\"
9219              CoalesceBatchesExec: target_batch_size=8192
9220                SortExec: TopK(fetch=34), expr=[_distance@0 ASC NULLS LAST, _rowid@1 ASC NULLS LAST]...
9221                  ANNSubIndex: name=idx, k=34, deltas=1, metric=L2
9222                    ANNIvfPartition: uuid=..., minimum_nprobes=1, maximum_nprobes=None, deltas=1",
9223        )
9224        .await
9225        .unwrap();
9226    }
9227
9228    #[tokio::test]
9229    async fn test_fast_search_without_vector_index_returns_empty() {
9230        let dataset = TestVectorDataset::new(LanceFileVersion::Stable, true)
9231            .await
9232            .unwrap();
9233        let q: Float32Array = (32..64).map(|v| v as f32).collect();
9234
9235        let mut scanner = dataset.dataset.scan();
9236        scanner.nearest("vec", &q, 10).unwrap();
9237        let normal_rows = scanner.try_into_batch().await.unwrap().num_rows();
9238
9239        let mut scanner = dataset.dataset.scan();
9240        scanner.nearest("vec", &q, 10).unwrap().fast_search();
9241        let fast_rows = scanner.try_into_batch().await.unwrap().num_rows();
9242
9243        assert_eq!(normal_rows, 10);
9244        assert_eq!(fast_rows, 0);
9245    }
9246
9247    #[rstest]
9248    #[tokio::test]
9249    pub async fn test_scan_planning_io(
9250        #[values(LanceFileVersion::Legacy, LanceFileVersion::Stable)]
9251        data_storage_version: LanceFileVersion,
9252    ) {
9253        // Create a large dataset with a scalar indexed column and a sorted but not scalar
9254        // indexed column
9255
9256        use lance_index::scalar::inverted::tokenizer::InvertedIndexParams;
9257        use lance_io::assert_io_eq;
9258        let data = gen_batch()
9259            .col(
9260                "vector",
9261                array::rand_vec::<Float32Type>(Dimension::from(32)),
9262            )
9263            .col("text", array::rand_utf8(ByteCount::from(4), false))
9264            .col("indexed", array::step::<Int32Type>())
9265            .col("not_indexed", array::step::<Int32Type>())
9266            .into_reader_rows(RowCount::from(100), BatchCount::from(5));
9267
9268        let mut dataset = Dataset::write(
9269            data,
9270            "memory://test",
9271            Some(WriteParams {
9272                data_storage_version: Some(data_storage_version),
9273                ..Default::default()
9274            }),
9275        )
9276        .await
9277        .unwrap();
9278        dataset
9279            .create_index(
9280                &["indexed"],
9281                IndexType::Scalar,
9282                None,
9283                &ScalarIndexParams::default(),
9284                false,
9285            )
9286            .await
9287            .unwrap();
9288        dataset
9289            .create_index(
9290                &["text"],
9291                IndexType::Inverted,
9292                None,
9293                &InvertedIndexParams::default(),
9294                false,
9295            )
9296            .await
9297            .unwrap();
9298        dataset
9299            .create_index(
9300                &["vector"],
9301                IndexType::Vector,
9302                None,
9303                &VectorIndexParams {
9304                    metric_type: DistanceType::L2,
9305                    stages: vec![
9306                        StageParams::Ivf(IvfBuildParams {
9307                            max_iters: 2,
9308                            num_partitions: Some(2),
9309                            sample_rate: 2,
9310                            ..Default::default()
9311                        }),
9312                        StageParams::PQ(PQBuildParams {
9313                            max_iters: 2,
9314                            num_sub_vectors: 2,
9315                            ..Default::default()
9316                        }),
9317                    ],
9318                    version: crate::index::vector::IndexFileVersion::Legacy,
9319                    skip_transpose: false,
9320                },
9321                false,
9322            )
9323            .await
9324            .unwrap();
9325
9326        // First planning cycle needs to do some I/O to determine what scalar indices are available
9327        dataset
9328            .scan()
9329            .prefilter(true)
9330            .filter("indexed > 10")
9331            .unwrap()
9332            .explain_plan(true)
9333            .await
9334            .unwrap();
9335
9336        // First pass will need to perform some IOPs to determine what scalar indices are available
9337        let io_stats = dataset.object_store().io_stats_incremental();
9338        assert_io_gt!(io_stats, read_iops, 0);
9339
9340        // Second planning cycle should not perform any I/O
9341        dataset
9342            .scan()
9343            .prefilter(true)
9344            .filter("indexed > 10")
9345            .unwrap()
9346            .explain_plan(true)
9347            .await
9348            .unwrap();
9349
9350        let io_stats = dataset.object_store().io_stats_incremental();
9351        assert_io_eq!(io_stats, read_iops, 0);
9352
9353        dataset
9354            .scan()
9355            .prefilter(true)
9356            .filter("true")
9357            .unwrap()
9358            .explain_plan(true)
9359            .await
9360            .unwrap();
9361
9362        let io_stats = dataset.object_store().io_stats_incremental();
9363        assert_io_eq!(io_stats, read_iops, 0);
9364
9365        dataset
9366            .scan()
9367            .prefilter(true)
9368            .materialization_style(MaterializationStyle::AllEarly)
9369            .filter("true")
9370            .unwrap()
9371            .explain_plan(true)
9372            .await
9373            .unwrap();
9374
9375        let io_stats = dataset.object_store().io_stats_incremental();
9376        assert_io_eq!(io_stats, read_iops, 0);
9377
9378        dataset
9379            .scan()
9380            .prefilter(true)
9381            .materialization_style(MaterializationStyle::AllLate)
9382            .filter("true")
9383            .unwrap()
9384            .explain_plan(true)
9385            .await
9386            .unwrap();
9387
9388        let io_stats = dataset.object_store().io_stats_incremental();
9389        assert_io_eq!(io_stats, read_iops, 0);
9390    }
9391
9392    #[rstest]
9393    #[tokio::test]
9394    pub async fn test_row_meta_columns(
9395        #[values(
9396            (true, false),  // Test row_id only
9397            (false, true),  // Test row_address only
9398            (true, true)    // Test both
9399        )]
9400        columns: (bool, bool),
9401    ) {
9402        let (with_row_id, with_row_address) = columns;
9403        let test_dir = TempStrDir::default();
9404        let uri = &test_dir;
9405
9406        let schema = Arc::new(arrow_schema::Schema::new(vec![
9407            arrow_schema::Field::new("data_item_id", arrow_schema::DataType::Int32, false),
9408            arrow_schema::Field::new("a", arrow_schema::DataType::Int32, false),
9409        ]));
9410
9411        let data = RecordBatch::try_new(
9412            schema.clone(),
9413            vec![
9414                Arc::new(Int32Array::from(vec![1001, 1002, 1003])),
9415                Arc::new(Int32Array::from(vec![1, 2, 3])),
9416            ],
9417        )
9418        .unwrap();
9419
9420        let dataset = Dataset::write(
9421            RecordBatchIterator::new(vec![Ok(data)], schema.clone()),
9422            uri,
9423            None,
9424        )
9425        .await
9426        .unwrap();
9427
9428        // Test explicit projection
9429        let mut scanner = dataset.scan();
9430
9431        let mut projection = vec!["data_item_id".to_string()];
9432        if with_row_id {
9433            scanner.with_row_id();
9434            projection.push(ROW_ID.to_string());
9435        }
9436        if with_row_address {
9437            scanner.with_row_address();
9438            projection.push(ROW_ADDR.to_string());
9439        }
9440
9441        scanner.project(&projection).unwrap();
9442        let stream = scanner.try_into_stream().await.unwrap();
9443        let batch = stream.try_collect::<Vec<_>>().await.unwrap().pop().unwrap();
9444
9445        // Verify column existence and data type
9446        if with_row_id {
9447            let column = batch.column_by_name(ROW_ID).unwrap();
9448            assert_eq!(column.data_type(), &DataType::UInt64);
9449        }
9450        if with_row_address {
9451            let column = batch.column_by_name(ROW_ADDR).unwrap();
9452            assert_eq!(column.data_type(), &DataType::UInt64);
9453        }
9454
9455        // Test implicit inclusion
9456        let mut scanner = dataset.scan();
9457        if with_row_id {
9458            scanner.with_row_id();
9459        }
9460        if with_row_address {
9461            scanner.with_row_address();
9462        }
9463        scanner.project(&["data_item_id"]).unwrap();
9464        let stream = scanner.try_into_stream().await.unwrap();
9465        let batch = stream.try_collect::<Vec<_>>().await.unwrap().pop().unwrap();
9466        let meta_column = batch.column_by_name(if with_row_id { ROW_ID } else { ROW_ADDR });
9467        assert!(meta_column.is_some());
9468
9469        // Test error case
9470        let mut scanner = dataset.scan();
9471        if with_row_id {
9472            scanner.project(&[ROW_ID]).unwrap();
9473        } else {
9474            scanner.project(&[ROW_ADDR]).unwrap();
9475        };
9476        let stream = scanner.try_into_stream().await.unwrap();
9477        assert_eq!(stream.schema().fields().len(), 1);
9478        if with_row_id {
9479            assert!(stream.schema().field_with_name(ROW_ID).is_ok());
9480        } else {
9481            assert!(stream.schema().field_with_name(ROW_ADDR).is_ok());
9482        }
9483    }
9484
9485    async fn limit_offset_equivalency_test(scanner: &Scanner) {
9486        async fn test_one(
9487            scanner: &Scanner,
9488            full_result: &RecordBatch,
9489            limit: Option<i64>,
9490            offset: Option<i64>,
9491        ) {
9492            let mut new_scanner = scanner.clone();
9493            new_scanner.limit(limit, offset).unwrap();
9494            if let Some(nearest) = new_scanner.nearest_mut() {
9495                nearest.k = offset.unwrap_or(0).saturating_add(limit.unwrap_or(10_000)) as usize;
9496            }
9497            let result = new_scanner.try_into_batch().await.unwrap();
9498
9499            let resolved_offset = offset.unwrap_or(0).min(full_result.num_rows() as i64);
9500            let resolved_length = limit
9501                .unwrap_or(i64::MAX)
9502                .min(full_result.num_rows() as i64 - resolved_offset);
9503
9504            let expected = full_result.slice(resolved_offset as usize, resolved_length as usize);
9505
9506            if expected != result {
9507                let plan = new_scanner.analyze_plan().await.unwrap();
9508                assert_eq!(
9509                    &expected, &result,
9510                    "Limit: {:?}, Offset: {:?}, Plan: \n{}",
9511                    limit, offset, plan
9512                );
9513            }
9514        }
9515
9516        let mut scanner_full = scanner.clone();
9517        if let Some(nearest) = scanner_full.nearest_mut() {
9518            nearest.k = 500;
9519        }
9520        let full_results = scanner_full.try_into_batch().await.unwrap();
9521
9522        test_one(scanner, &full_results, Some(1), None).await;
9523        test_one(scanner, &full_results, Some(1), Some(1)).await;
9524        test_one(scanner, &full_results, Some(1), Some(2)).await;
9525        test_one(scanner, &full_results, Some(1), Some(10)).await;
9526
9527        test_one(scanner, &full_results, Some(3), None).await;
9528        test_one(scanner, &full_results, Some(3), Some(2)).await;
9529        test_one(scanner, &full_results, Some(3), Some(4)).await;
9530
9531        test_one(scanner, &full_results, None, Some(3)).await;
9532        test_one(scanner, &full_results, None, Some(10)).await;
9533    }
9534
9535    #[tokio::test]
9536    async fn test_scan_limit_offset() {
9537        let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9538            .await
9539            .unwrap();
9540        let scanner = test_ds.dataset.scan();
9541        limit_offset_equivalency_test(&scanner).await;
9542    }
9543
9544    #[tokio::test]
9545    async fn test_knn_limit_offset() {
9546        let test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9547            .await
9548            .unwrap();
9549        let query_vector = Float32Array::from(vec![0.0; 32]);
9550        let mut scanner = test_ds.dataset.scan();
9551        scanner
9552            .nearest("vec", &query_vector, 5)
9553            .unwrap()
9554            .project(&["i"])
9555            .unwrap();
9556        limit_offset_equivalency_test(&scanner).await;
9557    }
9558
9559    #[tokio::test]
9560    async fn test_ivf_pq_limit_offset() {
9561        let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9562            .await
9563            .unwrap();
9564        test_ds.make_vector_index().await.unwrap();
9565        test_ds.append_new_data().await.unwrap();
9566        let query_vector = Float32Array::from(vec![0.0; 32]);
9567        let mut scanner = test_ds.dataset.scan();
9568        scanner.nearest("vec", &query_vector, 500).unwrap();
9569        limit_offset_equivalency_test(&scanner).await;
9570    }
9571
9572    #[tokio::test]
9573    async fn test_fts_limit_offset() {
9574        let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9575            .await
9576            .unwrap();
9577        test_ds.make_fts_index().await.unwrap();
9578        test_ds.append_new_data().await.unwrap();
9579        let mut scanner = test_ds.dataset.scan();
9580        scanner
9581            .full_text_search(FullTextSearchQuery::new("4".into()))
9582            .unwrap();
9583        limit_offset_equivalency_test(&scanner).await;
9584    }
9585
9586    #[tokio::test]
9587    async fn test_fts_fast_search_excludes_unindexed_rows() {
9588        let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
9589            .await
9590            .unwrap();
9591        test_ds.make_fts_index().await.unwrap();
9592        // Append rows after index build so they stay unindexed.
9593        test_ds.append_data_with_range(10, 20).await.unwrap();
9594
9595        let mut scanner = test_ds.dataset.scan();
9596        scanner
9597            .full_text_search(FullTextSearchQuery::new_query(
9598                MatchQuery::new("15".to_owned())
9599                    .with_column(Some("s".to_owned()))
9600                    .into(),
9601            ))
9602            .unwrap();
9603        let normal_rows = scanner.try_into_batch().await.unwrap().num_rows();
9604
9605        let mut scanner = test_ds.dataset.scan();
9606        scanner
9607            .full_text_search(FullTextSearchQuery::new_query(
9608                MatchQuery::new("15".to_owned())
9609                    .with_column(Some("s".to_owned()))
9610                    .into(),
9611            ))
9612            .unwrap()
9613            .fast_search();
9614        let fast_rows = scanner.try_into_batch().await.unwrap().num_rows();
9615
9616        assert_eq!(normal_rows, 2);
9617        assert_eq!(fast_rows, 1);
9618    }
9619
9620    async fn test_row_offset_read_helper(
9621        ds: &Dataset,
9622        scan_builder: impl FnOnce(&mut Scanner) -> &mut Scanner,
9623        expected_cols: &[&str],
9624        expected_row_offsets: &[u64],
9625    ) {
9626        let mut scanner = ds.scan();
9627        let scanner = scan_builder(&mut scanner);
9628        let stream = scanner.try_into_stream().await.unwrap();
9629
9630        let schema = stream.schema();
9631        let actual_cols = schema
9632            .fields()
9633            .iter()
9634            .map(|f| f.name().as_str())
9635            .collect::<Vec<_>>();
9636        assert_eq!(&actual_cols, expected_cols);
9637
9638        let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9639        let batch = arrow_select::concat::concat_batches(&schema, &batches).unwrap();
9640
9641        let row_offsets = batch
9642            .column_by_name(ROW_OFFSET)
9643            .unwrap()
9644            .as_primitive::<UInt64Type>()
9645            .values();
9646        assert_eq!(row_offsets.as_ref(), expected_row_offsets);
9647    }
9648
9649    #[tokio::test]
9650    async fn test_row_offset_read() {
9651        let mut ds = lance_datagen::gen_batch()
9652            .col("idx", array::step::<Int32Type>())
9653            .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(3))
9654            .await
9655            .unwrap();
9656        // [0, 1, 2], [3, 4, 5], [6, 7, 8]
9657
9658        // Delete [2, 3, 4, 5, 6]
9659        ds.delete("idx >= 2 AND idx <= 6").await.unwrap();
9660
9661        // Normal read, all columns plus row offset
9662        test_row_offset_read_helper(
9663            &ds,
9664            |scanner| scanner.project(&["idx", ROW_OFFSET]).unwrap(),
9665            &["idx", ROW_OFFSET],
9666            &[0, 1, 2, 3],
9667        )
9668        .await;
9669
9670        // Read with row offset only
9671        test_row_offset_read_helper(
9672            &ds,
9673            |scanner| scanner.project(&[ROW_OFFSET]).unwrap(),
9674            &[ROW_OFFSET],
9675            &[0, 1, 2, 3],
9676        )
9677        .await;
9678
9679        // Filtered read of row offset
9680        test_row_offset_read_helper(
9681            &ds,
9682            |scanner| {
9683                scanner
9684                    .filter("idx > 1")
9685                    .unwrap()
9686                    .project(&[ROW_OFFSET])
9687                    .unwrap()
9688            },
9689            &[ROW_OFFSET],
9690            &[2, 3],
9691        )
9692        .await;
9693    }
9694
9695    #[tokio::test]
9696    async fn test_filter_to_take() {
9697        let mut ds = lance_datagen::gen_batch()
9698            .col("idx", array::step::<Int32Type>())
9699            .into_ram_dataset(FragmentCount::from(3), FragmentRowCount::from(100))
9700            .await
9701            .unwrap();
9702
9703        let row_ids = ds
9704            .scan()
9705            .project(&Vec::<&str>::default())
9706            .unwrap()
9707            .with_row_id()
9708            .try_into_stream()
9709            .await
9710            .unwrap()
9711            .try_collect::<Vec<_>>()
9712            .await
9713            .unwrap();
9714        let schema = row_ids[0].schema();
9715        let row_ids = concat_batches(&schema, row_ids.iter()).unwrap();
9716        let row_ids = row_ids.column(0).as_primitive::<UInt64Type>().clone();
9717
9718        let row_addrs = ds
9719            .scan()
9720            .project(&Vec::<&str>::default())
9721            .unwrap()
9722            .with_row_address()
9723            .try_into_stream()
9724            .await
9725            .unwrap()
9726            .try_collect::<Vec<_>>()
9727            .await
9728            .unwrap();
9729        let schema = row_addrs[0].schema();
9730        let row_addrs = concat_batches(&schema, row_addrs.iter()).unwrap();
9731        let row_addrs = row_addrs.column(0).as_primitive::<UInt64Type>().clone();
9732
9733        ds.delete("idx >= 190 AND idx < 210").await.unwrap();
9734
9735        let ds_copy = ds.clone();
9736        let do_check = async move |filt: &str, expected_idx: &[i32], applies_optimization: bool| {
9737            let mut scanner = ds_copy.scan();
9738            scanner.filter(filt).unwrap();
9739            // Verify the optimization is applied
9740            let plan = scanner.explain_plan(true).await.unwrap();
9741            if applies_optimization {
9742                assert!(
9743                    plan.contains("OneShotStream"),
9744                    "expected take optimization to be applied. Filter: '{}'.  Plan:\n{}",
9745                    filt,
9746                    plan
9747                );
9748            } else {
9749                assert!(
9750                    !plan.contains("OneShotStream"),
9751                    "expected take optimization to not be applied. Filter: '{}'.  Plan:\n{}",
9752                    filt,
9753                    plan
9754                );
9755            }
9756
9757            // Verify the results
9758            let stream = scanner.try_into_stream().await.unwrap();
9759            let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9760            let idx = batches
9761                .iter()
9762                .map(|b| b.column_by_name("idx").unwrap().as_ref())
9763                .collect::<Vec<_>>();
9764
9765            if idx.is_empty() {
9766                assert!(expected_idx.is_empty());
9767                return;
9768            }
9769
9770            let idx = arrow::compute::concat(&idx).unwrap();
9771            assert_eq!(idx.as_primitive::<Int32Type>().values(), expected_idx);
9772        };
9773        let check =
9774            async |filt: &str, expected_idx: &[i32]| do_check(filt, expected_idx, true).await;
9775        let check_no_opt = async |filt: &str, expected_idx: &[i32]| {
9776            do_check(filt, expected_idx, false).await;
9777        };
9778
9779        // Simple case, no deletions yet
9780        check("_rowid = 50", &[50]).await;
9781        check("_rowaddr = 50", &[50]).await;
9782        check("_rowoffset = 50", &[50]).await;
9783
9784        check(
9785            "_rowid = 50 OR _rowid = 51 OR _rowid = 52 OR _rowid = 49",
9786            &[49, 50, 51, 52],
9787        )
9788        .await;
9789        check(
9790            "_rowaddr = 50 OR _rowaddr = 51 OR _rowaddr = 52 OR _rowaddr = 49",
9791            &[49, 50, 51, 52],
9792        )
9793        .await;
9794        check(
9795            "_rowoffset = 50 OR _rowoffset = 51 OR _rowoffset = 52 OR _rowoffset = 49",
9796            &[49, 50, 51, 52],
9797        )
9798        .await;
9799
9800        check("_rowid IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
9801        check("_rowaddr IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
9802        check("_rowoffset IN (52, 51, 50, 17)", &[17, 50, 51, 52]).await;
9803
9804        // Taking _rowid / _rowaddr of deleted row
9805
9806        // When using rowid / rowaddr we get an empty
9807        check(&format!("_rowid = {}", row_ids.value(190)), &[]).await;
9808        check(&format!("_rowaddr = {}", row_addrs.value(190)), &[]).await;
9809        // When using rowoffset it just skips the deleted rows (impossible to create an offset
9810        // into a deleted row)
9811        check("_rowoffset = 190", &[210]).await;
9812
9813        // Grabbing after the deleted rows
9814        check(&format!("_rowid = {}", row_ids.value(250)), &[250]).await;
9815        check(&format!("_rowaddr = {}", row_addrs.value(250)), &[250]).await;
9816        check("_rowoffset = 250", &[270]).await;
9817
9818        // Grabbing past the end
9819        check("_rowoffset = 1000", &[]).await;
9820
9821        // Combine take and filter
9822        check("_rowid IN (5, 10, 15) AND idx > 10", &[15]).await;
9823        check("_rowaddr IN (5, 10, 15) AND idx > 10", &[15]).await;
9824        check("_rowoffset IN (5, 10, 15) AND idx > 10", &[15]).await;
9825        check("idx > 10 AND _rowid IN (5, 10, 15)", &[15]).await;
9826        check("idx > 10 AND _rowaddr IN (5, 10, 15)", &[15]).await;
9827        check("idx > 10 AND _rowoffset IN (5, 10, 15)", &[15]).await;
9828        // Get's simplified into _rowid = 50 and so we catch it
9829        check("_rowid = 50 AND _rowid = 50", &[50]).await;
9830
9831        // Filters that cannot be converted into a take
9832        check_no_opt("_rowid = 50 AND _rowid = 51", &[]).await;
9833        check_no_opt("(_rowid = 50 AND idx < 100) OR _rowid = 51", &[50, 51]).await;
9834
9835        // Dynamic projection
9836        let mut scanner = ds.scan();
9837        scanner.filter("_rowoffset = 77").unwrap();
9838        scanner
9839            .project_with_transform(&[("foo", "idx * 2")])
9840            .unwrap();
9841        let stream = scanner.try_into_stream().await.unwrap();
9842        let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9843        assert_eq!(batches[0].schema().field(0).name(), "foo");
9844        let val = batches[0].column(0).as_primitive::<Int32Type>().values()[0];
9845        assert_eq!(val, 154);
9846    }
9847
9848    #[tokio::test]
9849    async fn test_nested_field_ordering() {
9850        use arrow_array::StructArray;
9851
9852        // Create test data with nested structs
9853        let id_array = Int32Array::from(vec![3, 1, 2]);
9854        let nested_values = Int32Array::from(vec![30, 10, 20]);
9855        let nested_struct = StructArray::from(vec![(
9856            Arc::new(ArrowField::new("value", DataType::Int32, false)),
9857            Arc::new(nested_values) as ArrayRef,
9858        )]);
9859
9860        let schema = Arc::new(ArrowSchema::new(vec![
9861            ArrowField::new("id", DataType::Int32, false),
9862            ArrowField::new(
9863                "nested",
9864                DataType::Struct(vec![ArrowField::new("value", DataType::Int32, false)].into()),
9865                false,
9866            ),
9867        ]));
9868
9869        let batch = RecordBatch::try_new(
9870            schema.clone(),
9871            vec![Arc::new(id_array), Arc::new(nested_struct)],
9872        )
9873        .unwrap();
9874
9875        let test_dir = TempStrDir::default();
9876        let test_uri = &test_dir;
9877        let reader = RecordBatchIterator::new(vec![Ok(batch)].into_iter(), schema.clone());
9878
9879        let dataset = Dataset::write(reader, test_uri, None).await.unwrap();
9880
9881        // Test ordering by nested field
9882        let mut scanner = dataset.scan();
9883        scanner
9884            .order_by(Some(vec![ColumnOrdering {
9885                column_name: "nested.value".to_string(),
9886                ascending: true,
9887                nulls_first: true,
9888            }]))
9889            .unwrap(); // ascending order
9890
9891        let stream = scanner.try_into_stream().await.unwrap();
9892        let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9893
9894        // Check that results are sorted by nested.value
9895        let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
9896        assert_eq!(sorted_ids[0], 1); // id=1 has nested.value=10
9897        assert_eq!(sorted_ids[1], 2); // id=2 has nested.value=20
9898        assert_eq!(sorted_ids[2], 3); // id=3 has nested.value=30
9899    }
9900
9901    #[tokio::test]
9902    async fn test_limit_with_ordering_not_pushed_down() {
9903        // This test verifies the fix for a bug where limit/offset could be pushed down
9904        // even when ordering was specified. When ordering is present, we need to load
9905        // all data first to sort it before applying limits.
9906
9907        // Create test data with specific ordering
9908        let id_array = Int32Array::from(vec![5, 2, 8, 1, 3, 7, 4, 6]);
9909        let value_array = Int32Array::from(vec![50, 20, 80, 10, 30, 70, 40, 60]);
9910
9911        let schema = Arc::new(ArrowSchema::new(vec![
9912            ArrowField::new("id", DataType::Int32, false),
9913            ArrowField::new("value", DataType::Int32, false),
9914        ]));
9915
9916        let batch = RecordBatch::try_new(
9917            schema.clone(),
9918            vec![Arc::new(id_array), Arc::new(value_array)],
9919        )
9920        .unwrap();
9921
9922        let test_dir = TempStrDir::default();
9923        let test_uri = &test_dir;
9924        let reader = RecordBatchIterator::new(vec![Ok(batch)].into_iter(), schema.clone());
9925
9926        let dataset = Dataset::write(reader, test_uri, None).await.unwrap();
9927
9928        // Test 1: limit with ordering should return top N after sorting
9929        let mut scanner = dataset.scan();
9930        scanner
9931            .order_by(Some(vec![ColumnOrdering {
9932                column_name: "value".to_string(),
9933                ascending: true,
9934                nulls_first: true,
9935            }]))
9936            .unwrap();
9937        scanner.limit(Some(3), None).unwrap();
9938
9939        let stream = scanner.try_into_stream().await.unwrap();
9940        let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9941
9942        // Results should be sorted by value and limited to 3
9943        let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
9944        let sorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
9945        assert_eq!(batches[0].num_rows(), 3);
9946        assert_eq!(sorted_ids[0], 1); // value=10
9947        assert_eq!(sorted_ids[1], 2); // value=20
9948        assert_eq!(sorted_ids[2], 3); // value=30
9949        assert_eq!(sorted_values[0], 10);
9950        assert_eq!(sorted_values[1], 20);
9951        assert_eq!(sorted_values[2], 30);
9952
9953        // Test 2: offset with ordering should skip first N after sorting
9954        let mut scanner = dataset.scan();
9955        scanner
9956            .order_by(Some(vec![ColumnOrdering {
9957                column_name: "value".to_string(),
9958                ascending: true,
9959                nulls_first: true,
9960            }]))
9961            .unwrap();
9962        scanner.limit(Some(3), Some(2)).unwrap(); // Skip first 2, take next 3
9963
9964        let stream = scanner.try_into_stream().await.unwrap();
9965        let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9966
9967        let sorted_ids = batches[0].column(0).as_primitive::<Int32Type>().values();
9968        let sorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
9969        assert_eq!(batches[0].num_rows(), 3);
9970        assert_eq!(sorted_ids[0], 3); // value=30 (skipped 10, 20)
9971        assert_eq!(sorted_ids[1], 4); // value=40
9972        assert_eq!(sorted_ids[2], 5); // value=50
9973        assert_eq!(sorted_values[0], 30);
9974        assert_eq!(sorted_values[1], 40);
9975        assert_eq!(sorted_values[2], 50);
9976
9977        // Test 3: without ordering, limit can be pushed down (different behavior)
9978        let mut scanner = dataset.scan();
9979        scanner.limit(Some(3), None).unwrap();
9980
9981        let stream = scanner.try_into_stream().await.unwrap();
9982        let batches = stream.try_collect::<Vec<_>>().await.unwrap();
9983
9984        // Should get first 3 rows in storage order (not sorted)
9985        assert_eq!(batches[0].num_rows(), 3);
9986        let unsorted_values = batches[0].column(1).as_primitive::<Int32Type>().values();
9987        // These will be in original insertion order, not sorted
9988        assert_eq!(unsorted_values[0], 50);
9989        assert_eq!(unsorted_values[1], 20);
9990        assert_eq!(unsorted_values[2], 80);
9991    }
9992
9993    #[tokio::test]
9994    async fn test_scan_with_version_columns() {
9995        use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator};
9996        use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
9997
9998        // Create a simple dataset
9999        let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
10000            "id",
10001            DataType::Int32,
10002            false,
10003        )]));
10004
10005        let batch = RecordBatch::try_new(
10006            schema.clone(),
10007            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
10008        )
10009        .unwrap();
10010
10011        let test_dir = lance_core::utils::tempfile::TempStrDir::default();
10012        let test_uri = test_dir.as_str();
10013
10014        let reader = RecordBatchIterator::new(vec![Ok(batch)], schema);
10015        let write_params = WriteParams {
10016            enable_stable_row_ids: true,
10017            ..Default::default()
10018        };
10019        Dataset::write(reader, test_uri, Some(write_params))
10020            .await
10021            .unwrap();
10022
10023        let dataset = Dataset::open(test_uri).await.unwrap();
10024        let mut scanner = dataset.scan();
10025
10026        scanner
10027            .project(&[ROW_CREATED_AT_VERSION, ROW_LAST_UPDATED_AT_VERSION])
10028            .unwrap();
10029
10030        // Check that the schema includes version columns
10031        let output_schema = scanner.schema().await.unwrap();
10032        assert!(
10033            output_schema
10034                .column_with_name("_row_last_updated_at_version")
10035                .is_some(),
10036            "Schema should include _row_last_updated_at_version"
10037        );
10038        assert!(
10039            output_schema
10040                .column_with_name("_row_created_at_version")
10041                .is_some(),
10042            "Schema should include _row_created_at_version"
10043        );
10044
10045        // Actually read the data to ensure version columns are materialized
10046        let batches = scanner
10047            .try_into_stream()
10048            .await
10049            .unwrap()
10050            .try_collect::<Vec<_>>()
10051            .await
10052            .unwrap();
10053
10054        assert_eq!(batches.len(), 1);
10055        let batch = &batches[0];
10056
10057        // Verify version columns exist in the output
10058        let last_updated = batch
10059            .column_by_name("_row_last_updated_at_version")
10060            .expect("Should have _row_last_updated_at_version column");
10061        let created_at = batch
10062            .column_by_name("_row_created_at_version")
10063            .expect("Should have _row_created_at_version column");
10064
10065        // Verify they have the correct values (all rows created in version 1)
10066        let last_updated_array = last_updated
10067            .as_any()
10068            .downcast_ref::<arrow_array::UInt64Array>()
10069            .unwrap();
10070        let created_at_array = created_at
10071            .as_any()
10072            .downcast_ref::<arrow_array::UInt64Array>()
10073            .unwrap();
10074
10075        for i in 0..batch.num_rows() {
10076            assert_eq!(
10077                last_updated_array.value(i),
10078                1,
10079                "All rows last updated at version 1"
10080            );
10081            assert_eq!(
10082                created_at_array.value(i),
10083                1,
10084                "All rows created at version 1"
10085            );
10086        }
10087    }
10088
10089    #[test_log::test(test)]
10090    fn test_scan_finishes_all_tasks() {
10091        // Need to use multi-threaded runtime otherwise tasks don't run unless someone is polling somewhere
10092        let runtime = tokio::runtime::Builder::new_multi_thread()
10093            .enable_time()
10094            .build()
10095            .unwrap();
10096
10097        runtime.block_on(async move {
10098            let ds = lance_datagen::gen_batch()
10099                .col("id", lance_datagen::array::step::<Int32Type>())
10100                .into_ram_dataset(FragmentCount::from(1000), FragmentRowCount::from(10))
10101                .await
10102                .unwrap();
10103
10104            // This scan with has a small I/O buffer size and batch size to mimic a real-world situation
10105            // that required a lot of data.  Many fragments will be scheduled at low priority and the data
10106            // buffer will fill up with data reads.  When the scan is abandoned, the tasks to read the fragment
10107            // metadata were left behind and would never finish because the data was never decoded to drain the
10108            // backpressure queue.
10109            //
10110            // The fix (that this test verifies) is to ensure we close the I/O scheduler when the scan is abandoned.
10111            let mut stream = ds
10112                .scan()
10113                .fragment_readahead(1000)
10114                .batch_size(1)
10115                .io_buffer_size(1)
10116                .batch_readahead(1)
10117                .try_into_stream()
10118                .await
10119                .unwrap();
10120            stream.next().await.unwrap().unwrap();
10121        });
10122
10123        let start = Instant::now();
10124        while start.elapsed() < Duration::from_secs(10) {
10125            if runtime.handle().metrics().num_alive_tasks() == 0 {
10126                break;
10127            }
10128            std::thread::sleep(Duration::from_millis(100));
10129        }
10130
10131        assert!(
10132            runtime.handle().metrics().num_alive_tasks() == 0,
10133            "Tasks should have finished within 10 seconds but there are still {} tasks running",
10134            runtime.handle().metrics().num_alive_tasks()
10135        );
10136    }
10137
10138    fn assert_values_in_range(array: &Int32Array, range: std::ops::Range<i32>, msg: &str) {
10139        assert!(!array.is_empty(), "Expected some results but got none");
10140        assert!(
10141            array
10142                .iter()
10143                .all(|v| v.is_some_and(|val| range.contains(&val))),
10144            "{msg} (expected range {range:?})"
10145        );
10146    }
10147
10148    // Helper to assert that results exist from all fragment ranges
10149    fn assert_has_all_fragments(array: &Int32Array) {
10150        assert!(
10151            array
10152                .iter()
10153                .any(|v| v.is_some_and(|val| (0..200).contains(&val)))
10154                && array
10155                    .iter()
10156                    .any(|v| v.is_some_and(|val| (200..400).contains(&val)))
10157                && array
10158                    .iter()
10159                    .any(|v| v.is_some_and(|val| (400..410).contains(&val)))
10160                && array
10161                    .iter()
10162                    .any(|v| v.is_some_and(|val| (410..420).contains(&val))),
10163            "Expected results from all fragments"
10164        );
10165    }
10166
10167    // Common test function for fragment list filtering (unindexed + indexed fragments)
10168    async fn test_fragment_list_filtering(
10169        test_ds: &TestVectorDataset,
10170        fragments: &[Fragment],
10171        mut build_scanner: impl FnMut(&Dataset) -> Scanner,
10172    ) {
10173        // Test 1: Query without fragment filter - should get results from all fragments
10174        let batch = build_scanner(&test_ds.dataset)
10175            .try_into_batch()
10176            .await
10177            .unwrap();
10178        let i_array = batch
10179            .column_by_name("i")
10180            .unwrap()
10181            .as_any()
10182            .downcast_ref::<Int32Array>()
10183            .unwrap();
10184        assert_has_all_fragments(i_array);
10185
10186        // Test 2: Query only one unindexed fragment (fragment 2), excluding fragment 3
10187        let mut scanner = build_scanner(&test_ds.dataset);
10188        scanner.with_fragments(vec![fragments[2].clone()]);
10189        let batch = scanner.try_into_batch().await.unwrap();
10190        let i_array = batch
10191            .column_by_name("i")
10192            .unwrap()
10193            .as_any()
10194            .downcast_ref::<Int32Array>()
10195            .unwrap();
10196        assert_values_in_range(i_array, 400..410, "Should only get results from fragment 2");
10197
10198        // Test 3: Query a single indexed fragment (fragment 0 only)
10199        let mut scanner = build_scanner(&test_ds.dataset);
10200        scanner.with_fragments(vec![fragments[0].clone()]);
10201        let batch = scanner.try_into_batch().await.unwrap();
10202        let i_array = batch
10203            .column_by_name("i")
10204            .unwrap()
10205            .as_any()
10206            .downcast_ref::<Int32Array>()
10207            .unwrap();
10208        assert_values_in_range(i_array, 0..200, "Should only get results from fragment 0");
10209
10210        // Test 4: Query all indexed fragments (0, 1) plus one unindexed fragment (2), excluding fragment 3
10211        let mut scanner = build_scanner(&test_ds.dataset);
10212        scanner.with_fragments(vec![
10213            fragments[0].clone(),
10214            fragments[1].clone(),
10215            fragments[2].clone(),
10216        ]);
10217        let batch = scanner.try_into_batch().await.unwrap();
10218        let i_array = batch
10219            .column_by_name("i")
10220            .unwrap()
10221            .as_any()
10222            .downcast_ref::<Int32Array>()
10223            .unwrap();
10224        assert_values_in_range(
10225            i_array,
10226            0..410,
10227            "Should get results from fragments 0, 1, and 2, excluding fragment 3",
10228        );
10229
10230        // Test 5: One indexed fragment (0) + one unindexed fragment (2), skipping indexed fragment 1 and unindexed fragment 3
10231        let mut scanner = build_scanner(&test_ds.dataset);
10232        scanner.with_fragments(vec![fragments[0].clone(), fragments[2].clone()]);
10233        let batch = scanner.try_into_batch().await.unwrap();
10234        let i_array = batch
10235            .column_by_name("i")
10236            .unwrap()
10237            .as_any()
10238            .downcast_ref::<Int32Array>()
10239            .unwrap();
10240        assert!(
10241            i_array
10242                .iter()
10243                .all(|v| v.is_some_and(|val| (0..200).contains(&val) || (400..410).contains(&val)))
10244                && i_array
10245                    .iter()
10246                    .any(|v| v.is_some_and(|val| (0..200).contains(&val)))
10247                && i_array
10248                    .iter()
10249                    .any(|v| v.is_some_and(|val| (400..410).contains(&val))),
10250            "Should only get results from fragment 0 (indexed) and fragment 2 (unindexed)"
10251        );
10252    }
10253
10254    #[tokio::test]
10255    async fn test_vector_search_respects_fragment_list() {
10256        let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10257            .await
10258            .unwrap();
10259
10260        // Create index on first 2 fragments
10261        test_ds.make_vector_index().await.unwrap();
10262
10263        let query: Float32Array = (0..32).map(|v| v as f32).collect();
10264
10265        // Append two more unindexed fragments
10266        test_ds.append_data_with_range(400, 410).await.unwrap();
10267        test_ds.append_data_with_range(410, 420).await.unwrap();
10268
10269        // Fragment 0: i=0..200 (indexed), Fragment 1: i=200..400 (indexed)
10270        // Fragment 2: i=400..410 (unindexed), Fragment 3: i=410..420 (unindexed)
10271        let fragments = test_ds.dataset.fragments();
10272        assert_eq!(fragments.len(), 4);
10273
10274        test_fragment_list_filtering(&test_ds, fragments, |dataset| {
10275            let mut scanner = dataset.scan();
10276            scanner.nearest("vec", &query, 420).unwrap();
10277            scanner
10278        })
10279        .await;
10280    }
10281
10282    #[tokio::test]
10283    async fn test_fts_respects_fragment_list() {
10284        let mut test_ds = TestVectorDataset::new(LanceFileVersion::Stable, false)
10285            .await
10286            .unwrap();
10287
10288        // Create FTS index on first 2 fragments
10289        test_ds.make_fts_index().await.unwrap();
10290
10291        // Append two more unindexed fragments
10292        test_ds.append_data_with_range(400, 410).await.unwrap();
10293        test_ds.append_data_with_range(410, 420).await.unwrap();
10294
10295        // Fragment 0: i=0..200 (indexed), Fragment 1: i=200..400 (indexed)
10296        // Fragment 2: i=400..410 (unindexed), Fragment 3: i=410..420 (unindexed)
10297        let fragments = test_ds.dataset.fragments();
10298        assert_eq!(fragments.len(), 4);
10299
10300        // "s-5" matches: s-5, s-50..s-59, s-150..s-159 (frag 0), s-250..s-259, s-350..s-359 (frag 1), s-405 (frag 2), s-415 (frag 3)
10301        test_fragment_list_filtering(&test_ds, fragments, |dataset| {
10302            let mut scanner = dataset.scan();
10303            scanner
10304                .full_text_search(FullTextSearchQuery::new("s-5".into()))
10305                .unwrap();
10306            scanner
10307        })
10308        .await;
10309    }
10310}