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