Skip to main content

lance_index_core/
scalar.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Abstract scalar index traits and types for Lance index plugins
5
6use arrow_array::{BooleanArray, RecordBatch, UInt64Array};
7use arrow_schema::{DataType, Schema};
8use async_trait::async_trait;
9use bytes::Bytes;
10use datafusion::physical_plan::SendableRecordBatchStream;
11use datafusion_common::scalar::ScalarValue;
12use datafusion_expr::Expr;
13use lance_core::deepsize::DeepSizeOf;
14use lance_core::utils::row_addr_remap::RowAddrRemap;
15use lance_core::{Error, Result};
16use lance_io::stream::{RecordBatchStream, RecordBatchStreamAdapter};
17use lance_select::{NullableRowAddrSet, RowAddrTreeMap, RowSetOps};
18use roaring::{RoaringBitmap, RoaringTreemap};
19use serde::Serialize;
20use std::collections::HashMap;
21use std::fmt::Debug;
22use std::pin::Pin;
23use std::{any::Any, sync::Arc};
24
25use crate::metrics::MetricsCollector;
26use crate::{Index, IndexParams, IndexType};
27
28/// Metadata about a single file within an index segment.
29#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
30pub struct IndexFile {
31    /// Path relative to the index directory (e.g., "index.idx", "auxiliary.idx")
32    pub path: String,
33    /// Size of the file in bytes
34    pub size_bytes: u64,
35}
36
37pub const LANCE_SCALAR_INDEX: &str = "__lance_scalar_index";
38
39/// Builtin index types supported by the Lance library
40///
41/// This is primarily for convenience to avoid a bunch of string
42/// constants and provide some auto-complete.  This type should not
43/// be used in the manifest as plugins cannot add new entries.
44#[derive(Debug, Clone, PartialEq, Eq, DeepSizeOf)]
45pub enum BuiltinIndexType {
46    BTree,
47    Bitmap,
48    LabelList,
49    NGram,
50    ZoneMap,
51    BloomFilter,
52    RTree,
53    Inverted,
54    Fm,
55}
56
57impl BuiltinIndexType {
58    pub fn as_str(&self) -> &str {
59        match self {
60            Self::BTree => "btree",
61            Self::Bitmap => "bitmap",
62            Self::LabelList => "labellist",
63            Self::NGram => "ngram",
64            Self::ZoneMap => "zonemap",
65            Self::Inverted => "inverted",
66            Self::BloomFilter => "bloomfilter",
67            Self::RTree => "rtree",
68            Self::Fm => "fm",
69        }
70    }
71}
72
73impl TryFrom<IndexType> for BuiltinIndexType {
74    type Error = Error;
75
76    fn try_from(value: IndexType) -> Result<Self> {
77        match value {
78            IndexType::BTree => Ok(Self::BTree),
79            IndexType::Bitmap => Ok(Self::Bitmap),
80            IndexType::LabelList => Ok(Self::LabelList),
81            IndexType::NGram => Ok(Self::NGram),
82            IndexType::ZoneMap => Ok(Self::ZoneMap),
83            IndexType::Inverted => Ok(Self::Inverted),
84            IndexType::BloomFilter => Ok(Self::BloomFilter),
85            IndexType::RTree => Ok(Self::RTree),
86            IndexType::Fm => Ok(Self::Fm),
87            _ => Err(Error::index("Invalid index type".to_string())),
88        }
89    }
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub struct ScalarIndexParams {
94    /// The type of index to create
95    ///
96    /// Plugins may add additional index types.  Index type lookup is case-insensitive.
97    pub index_type: String,
98    /// The parameters to train the index
99    ///
100    /// This should be a JSON string.  The contents of the JSON string will be specific to the
101    /// index type.  If not set, then default parameters will be used for the index type.
102    pub params: Option<String>,
103}
104
105impl Default for ScalarIndexParams {
106    fn default() -> Self {
107        Self {
108            index_type: BuiltinIndexType::BTree.as_str().to_string(),
109            params: None,
110        }
111    }
112}
113
114impl ScalarIndexParams {
115    /// Creates a new ScalarIndexParams from one of the builtin index types
116    pub fn for_builtin(index_type: BuiltinIndexType) -> Self {
117        Self {
118            index_type: index_type.as_str().to_string(),
119            params: None,
120        }
121    }
122
123    /// Create a new ScalarIndexParams with the given index type
124    pub fn new(index_type: String) -> Self {
125        Self {
126            index_type,
127            params: None,
128        }
129    }
130
131    /// Set the parameters for the index
132    pub fn with_params<ParamsType: Serialize>(mut self, params: &ParamsType) -> Self {
133        self.params = Some(serde_json::to_string(params).unwrap());
134        self
135    }
136}
137
138impl IndexParams for ScalarIndexParams {
139    fn as_any(&self) -> &dyn std::any::Any {
140        self
141    }
142
143    fn index_name(&self) -> &str {
144        LANCE_SCALAR_INDEX
145    }
146}
147
148/// Trait for storing an index (or parts of an index) into storage
149#[async_trait]
150pub trait IndexWriter: Send {
151    /// Writes a record batch into the file, returning the 0-based index of the batch in the file
152    ///
153    /// E.g. if this is the third time this is called this method will return 2
154    async fn write_record_batch(&mut self, batch: RecordBatch) -> Result<u64>;
155    /// Adds a global buffer and returns its index.
156    async fn add_global_buffer(&mut self, _data: Bytes) -> Result<u32> {
157        Err(Error::not_supported(
158            "global buffers are not supported by this index writer",
159        ))
160    }
161    /// Finishes writing the file and closes the file
162    async fn finish(&mut self) -> Result<IndexFile>;
163    /// Finishes writing the file and closes the file with additional metadata
164    async fn finish_with_metadata(
165        &mut self,
166        metadata: HashMap<String, String>,
167    ) -> Result<IndexFile>;
168}
169
170/// Trait for reading an index (or parts of an index) from storage
171#[async_trait]
172pub trait IndexReader: Send + Sync {
173    /// Read the n-th record batch from the file
174    async fn read_record_batch(&self, n: u64, batch_size: u64) -> Result<RecordBatch>;
175    /// Reads a global buffer by index.
176    async fn read_global_buffer(&self, _index: u32) -> Result<Bytes> {
177        Err(Error::not_supported(
178            "global buffers are not supported by this index reader",
179        ))
180    }
181    /// Read the range of rows from the file.
182    /// If projection is Some, only return the columns in the projection,
183    /// nested columns like Some(&["x.y"]) are not supported.
184    /// If projection is None, return all columns.
185    async fn read_range(
186        &self,
187        range: std::ops::Range<usize>,
188        projection: Option<&[&str]>,
189    ) -> Result<RecordBatch>;
190    /// Read multiple ranges and concatenate into a single batch.
191    /// Default impl runs `read_range`s in parallel via `try_join_all`.
192    async fn read_ranges(
193        &self,
194        ranges: &[std::ops::Range<usize>],
195        projection: Option<&[&str]>,
196    ) -> Result<RecordBatch> {
197        if ranges.is_empty() {
198            return self.read_range(0..0, projection).await;
199        }
200        let futures = ranges
201            .iter()
202            .map(|r| self.read_range(r.clone(), projection));
203        let batches = futures::future::try_join_all(futures).await?;
204        let schema = batches[0].schema();
205        Ok(arrow_select::concat::concat_batches(&schema, &batches)?)
206    }
207    /// Read a range of rows as a stream of record batches.
208    ///
209    /// This allows the caller to process rows incrementally without loading the
210    /// entire range into memory at once.
211    ///
212    /// The default implementation falls back to [`Self::read_range`] and wraps
213    /// the result in a single-item stream.
214    async fn read_range_stream(
215        &self,
216        range: std::ops::Range<usize>,
217        projection: Option<&[&str]>,
218    ) -> Result<Pin<Box<dyn RecordBatchStream>>> {
219        let batch = self.read_range(range, projection).await?;
220        let schema = batch.schema();
221        Ok(Box::pin(RecordBatchStreamAdapter::new(
222            schema,
223            futures::stream::once(async move { Ok(batch) }),
224        )))
225    }
226    /// Return the number of batches in the file
227    async fn num_batches(&self, batch_size: u64) -> u32;
228    /// Return the number of rows in the file
229    fn num_rows(&self) -> usize;
230    /// Return the metadata of the file
231    fn schema(&self) -> &lance_core::datatypes::Schema;
232    /// Best-effort on-disk byte size of the file when the reader already knows it
233    /// without extra I/O, else `None`. Used to size prewarm chunks.
234    fn file_size_bytes(&self) -> Option<u64> {
235        None
236    }
237}
238
239/// Trait abstracting I/O away from index logic
240///
241/// Scalar indices are currently serialized as indexable arrow record batches stored in
242/// named "files".  The index store is responsible for serializing and deserializing
243/// these batches into file data (e.g. as .lance files or .parquet files, etc.)
244#[async_trait]
245pub trait IndexStore: std::fmt::Debug + Send + Sync + DeepSizeOf {
246    fn as_any(&self) -> &dyn Any;
247    fn clone_arc(&self) -> Arc<dyn IndexStore>;
248
249    /// Return whether this store has the same live storage binding as `other`.
250    ///
251    /// `true` means cached indices holding [`IndexReader`]s created through `other` are safe to
252    /// reuse through this store. `false` means they must be reopened through
253    /// [`IndexStore::open_index_file`]. Implementations should return `true` only when readers,
254    /// credentials, and connection handles are interchangeable between both stores. The
255    /// conservative default prevents custom stores from accidentally reusing a store-bound
256    /// index.
257    ///
258    /// ```
259    /// use lance_index_core::scalar::IndexStore;
260    ///
261    /// fn can_reuse_cached_index(
262    ///     current_store: &dyn IndexStore,
263    ///     cached_store: &dyn IndexStore,
264    /// ) -> bool {
265    ///     current_store.is_same_storage_binding(cached_store)
266    /// }
267    /// ```
268    fn is_same_storage_binding(&self, _other: &dyn IndexStore) -> bool {
269        false
270    }
271
272    /// Suggested I/O parallelism for the store
273    fn io_parallelism(&self) -> usize;
274
275    /// Create a new file and return a writer to store data in the file
276    async fn new_index_file(&self, name: &str, schema: Arc<Schema>)
277    -> Result<Box<dyn IndexWriter>>;
278
279    /// Open an existing file for retrieval
280    async fn open_index_file(&self, name: &str) -> Result<Arc<dyn IndexReader>>;
281
282    /// Return a store that submits its I/O at the given base priority.
283    fn with_io_priority(&self, io_priority: u64) -> Arc<dyn IndexStore>;
284
285    /// Copy a range of batches from an index file from this store to another
286    ///
287    /// This is often useful when remapping or updating
288    async fn copy_index_file(&self, name: &str, dest_store: &dyn IndexStore) -> Result<IndexFile>;
289
290    /// Copy an index file from this store to a new name in another store, leaving the source intact
291    async fn copy_index_file_to(
292        &self,
293        name: &str,
294        new_name: &str,
295        dest_store: &dyn IndexStore,
296    ) -> Result<IndexFile> {
297        if name == new_name {
298            self.copy_index_file(name, dest_store).await
299        } else {
300            Err(Error::not_supported(format!(
301                "copying index file {name} to {new_name} is not supported by this index store"
302            )))
303        }
304    }
305
306    /// Rename an index file
307    async fn rename_index_file(&self, name: &str, new_name: &str) -> Result<IndexFile>;
308
309    /// Delete an index file (used in the tmp spill store to keep tmp size down)
310    async fn delete_index_file(&self, name: &str) -> Result<()>;
311
312    /// List all files in the index directory with their sizes.
313    ///
314    /// Returns a list of (relative_path, size_bytes) tuples.
315    /// Used to capture file metadata after index creation/modification.
316    async fn list_files_with_sizes(&self) -> Result<Vec<IndexFile>>;
317}
318
319/// Different scalar indices may support different kinds of queries
320///
321/// For example, a btree index can support a wide range of queries (e.g. x > 7)
322/// while an index based on FTS only supports queries like "x LIKE 'foo'"
323///
324/// This trait is used when we need an object that can represent any kind of query
325///
326/// Note: if you are implementing this trait for a query type then you probably also
327/// need to implement the scalar query parser trait to create instances of your query at parse time.
328pub trait AnyQuery: std::fmt::Debug + Any + Send + Sync {
329    /// Cast the query as Any to allow for downcasting
330    fn as_any(&self) -> &dyn Any;
331    /// Format the query as a string for display purposes
332    fn format(&self, col: &str) -> String;
333    /// Convert the query to a datafusion expression
334    fn to_expr(&self, col: String) -> Expr;
335    /// Compare this query to another query
336    fn dyn_eq(&self, other: &dyn AnyQuery) -> bool;
337}
338
339impl PartialEq for dyn AnyQuery {
340    fn eq(&self, other: &Self) -> bool {
341        self.dyn_eq(other)
342    }
343}
344
345/// The result of a search operation against a scalar index
346#[derive(Debug, PartialEq)]
347pub enum SearchResult {
348    /// The exact row ids that satisfy the query
349    Exact(NullableRowAddrSet),
350    /// Any row id satisfying the query will be in this set but not every
351    /// row id in this set will satisfy the query, a further recheck step
352    /// is needed
353    AtMost(NullableRowAddrSet),
354    /// All of the given row ids satisfy the query but there may be more
355    ///
356    /// No scalar index actually returns this today but it can arise from
357    /// boolean operations (e.g. NOT(AtMost(x)) == AtLeast(NOT(x)))
358    AtLeast(NullableRowAddrSet),
359}
360
361impl SearchResult {
362    pub fn exact(row_ids: impl Into<RowAddrTreeMap>) -> Self {
363        Self::Exact(NullableRowAddrSet::new(row_ids.into(), Default::default()))
364    }
365
366    pub fn at_most(row_ids: impl Into<RowAddrTreeMap>) -> Self {
367        Self::AtMost(NullableRowAddrSet::new(row_ids.into(), Default::default()))
368    }
369
370    pub fn at_least(row_ids: impl Into<RowAddrTreeMap>) -> Self {
371        Self::AtLeast(NullableRowAddrSet::new(row_ids.into(), Default::default()))
372    }
373
374    pub fn with_nulls(self, nulls: impl Into<RowAddrTreeMap>) -> Self {
375        match self {
376            Self::Exact(row_ids) => Self::Exact(row_ids.with_nulls(nulls.into())),
377            Self::AtMost(row_ids) => Self::AtMost(row_ids.with_nulls(nulls.into())),
378            Self::AtLeast(row_ids) => Self::AtLeast(row_ids.with_nulls(nulls.into())),
379        }
380    }
381
382    pub fn row_addrs(&self) -> &NullableRowAddrSet {
383        match self {
384            Self::Exact(row_addrs) => row_addrs,
385            Self::AtMost(row_addrs) => row_addrs,
386            Self::AtLeast(row_addrs) => row_addrs,
387        }
388    }
389
390    pub fn is_exact(&self) -> bool {
391        matches!(self, Self::Exact(_))
392    }
393}
394
395/// Brief information about an index that was created
396pub struct CreatedIndex {
397    /// The details of the index that was created
398    ///
399    /// These should be stored somewhere as they will be needed to
400    /// load the index later.
401    pub index_details: prost_types::Any,
402    /// The version of the index that was created
403    ///
404    /// This can be used to determine if a reader is able to load the index.
405    pub index_version: u32,
406    /// List of files and their sizes for this index
407    ///
408    /// This enables skipping HEAD calls when opening indices and provides
409    /// visibility into index storage size via describe_indices().
410    pub files: Vec<IndexFile>,
411}
412
413/// The ordering that training data must satisfy
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub enum TrainingOrdering {
416    /// The input will arrive sorted by the value column in ascending order
417    Values,
418    /// The input will arrive sorted by the address column in ascending order
419    Addresses,
420    /// The input will arrive in an arbitrary order
421    None,
422}
423
424#[derive(Debug, Clone)]
425pub struct TrainingCriteria {
426    pub ordering: TrainingOrdering,
427    pub needs_row_ids: bool,
428    pub needs_row_addrs: bool,
429}
430
431impl TrainingCriteria {
432    pub fn new(ordering: TrainingOrdering) -> Self {
433        Self {
434            ordering,
435            needs_row_ids: false,
436            needs_row_addrs: false,
437        }
438    }
439
440    pub fn with_row_id(mut self) -> Self {
441        self.needs_row_ids = true;
442        self
443    }
444
445    pub fn with_row_addr(mut self) -> Self {
446        self.needs_row_addrs = true;
447        self
448    }
449}
450
451/// The criteria that specifies how to update an index
452pub struct UpdateCriteria {
453    /// If true, then we need to read the old data to update the index
454    ///
455    /// This should be avoided if possible but is left in for some legacy paths
456    pub requires_old_data: bool,
457    /// The criteria required for data (both old and new)
458    pub data_criteria: TrainingCriteria,
459}
460
461/// Filter used when merging existing scalar-index rows during update.
462///
463/// The caller must pick a filter mode that matches the row-id semantics of the
464/// dataset:
465/// - address-style row IDs: fragment filtering is valid
466/// - stable row IDs: use exact row-id membership instead
467#[derive(Debug, Clone)]
468pub enum OldIndexDataFilter {
469    /// Keeps track of which fragments are still valid and which are no longer valid.
470    ///
471    /// This is valid for address-style row IDs.
472    Fragments {
473        to_keep: RoaringBitmap,
474        to_remove: RoaringBitmap,
475    },
476    /// Keep old rows whose row IDs are in this exact allow-list.
477    ///
478    /// This is required for stable row IDs, where row IDs are opaque and
479    /// should not be interpreted as encoded row addresses.
480    RowIds(RowAddrTreeMap),
481}
482
483impl OldIndexDataFilter {
484    /// Build a boolean mask that keeps only row IDs selected by this filter.
485    pub fn filter_row_ids(&self, row_ids: &UInt64Array) -> BooleanArray {
486        match self {
487            Self::Fragments { to_keep, .. } => row_ids
488                .iter()
489                .map(|id| id.map(|id| to_keep.contains((id >> 32) as u32)))
490                .collect(),
491            Self::RowIds(valid_row_ids) => row_ids
492                .iter()
493                .map(|id| id.map(|id| valid_row_ids.contains(id)))
494                .collect(),
495        }
496    }
497
498    /// Apply this filter in place to a set of existing (old) row ids/addresses,
499    /// retaining only the rows the filter selects to keep. Used by index types
500    /// that merge old postings directly (e.g. bitmap) instead of re-scanning a
501    /// row-id array through [`Self::filter_row_ids`].
502    pub fn retain_old_rows(&self, rows: &mut RowAddrTreeMap) {
503        match self {
504            Self::Fragments { to_keep, .. } => rows.retain_fragments(to_keep.iter()),
505            Self::RowIds(valid_row_ids) => *rows &= valid_row_ids,
506        }
507    }
508}
509
510impl UpdateCriteria {
511    pub fn requires_old_data(data_criteria: TrainingCriteria) -> Self {
512        Self {
513            requires_old_data: true,
514            data_criteria,
515        }
516    }
517
518    pub fn only_new_data(data_criteria: TrainingCriteria) -> Self {
519        Self {
520            requires_old_data: false,
521            data_criteria,
522        }
523    }
524}
525
526/// Execution-time options for scalar index searches.
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528pub struct SearchOptions {
529    /// Preserve rows where the query evaluates to NULL.
530    ///
531    /// Callers may disable this only when NULL rows cannot affect the final
532    /// result, such as a top-level filter whose NULL results will be discarded.
533    track_nulls: bool,
534}
535
536impl Default for SearchOptions {
537    fn default() -> Self {
538        Self { track_nulls: true }
539    }
540}
541
542impl SearchOptions {
543    /// Configure whether searches preserve rows where the query evaluates to
544    /// NULL. When disabled, implementations return only TRUE rows.
545    pub fn with_track_nulls(mut self, track_nulls: bool) -> Self {
546        self.track_nulls = track_nulls;
547        self
548    }
549
550    /// Whether searches preserve rows where the query evaluates to NULL.
551    pub fn track_nulls(&self) -> bool {
552        self.track_nulls
553    }
554}
555
556/// A trait for a scalar index, a structure that can determine row ids that satisfy scalar queries
557#[async_trait]
558pub trait ScalarIndex: Send + Sync + std::fmt::Debug + Index + DeepSizeOf {
559    /// Search the scalar index
560    ///
561    /// Returns all row ids that satisfy the query, these row ids are not necessarily ordered
562    async fn search(
563        &self,
564        query: &dyn AnyQuery,
565        metrics: &dyn MetricsCollector,
566    ) -> Result<SearchResult>;
567
568    /// Search the scalar index with execution-time options.
569    ///
570    /// Index implementations that do not need the options can rely on this
571    /// default implementation. The default preserves the behavior of
572    /// [`Self::search`].
573    async fn search_with_options(
574        &self,
575        query: &dyn AnyQuery,
576        _options: SearchOptions,
577        metrics: &dyn MetricsCollector,
578    ) -> Result<SearchResult> {
579        self.search(query, metrics).await
580    }
581
582    /// Returns true if this index reports matches as physical row addresses
583    /// (`fragment_id << 32 | offset`) rather than row ids
584    ///
585    /// Address-domain indices (e.g. zone map, bloom filter) are built over the
586    /// `_rowaddr` column. On a dataset with stable row ids the address and
587    /// row-id domains diverge, so these results must be translated back to row
588    /// ids (via the per-fragment row-id sequences, known only at the dataset
589    /// layer) before they are combined with row-id results or handed to the
590    /// scan. The default (row-id domain) needs no translation.
591    fn results_are_row_addresses(&self) -> bool {
592        false
593    }
594
595    /// Returns true if the remap operation is supported
596    fn can_remap(&self) -> bool;
597
598    /// Remap the row ids, creating a new remapped version of this index in `dest_store`
599    async fn remap(
600        &self,
601        mapping: &RowAddrRemap,
602        dest_store: &dyn IndexStore,
603    ) -> Result<CreatedIndex>;
604
605    /// Add the new data into the index, creating an updated version of the index in `dest_store`
606    ///
607    /// If `old_data_filter` is provided, old index data will be filtered before
608    /// merge according to the chosen filter mode.
609    async fn update(
610        &self,
611        new_data: SendableRecordBatchStream,
612        dest_store: &dyn IndexStore,
613        old_data_filter: Option<OldIndexDataFilter>,
614    ) -> Result<CreatedIndex>;
615
616    /// Returns the criteria that will be used to update the index
617    fn update_criteria(&self) -> UpdateCriteria;
618
619    /// Derive the index parameters from the current index
620    ///
621    /// This returns a ScalarIndexParams that can be used to recreate an index
622    /// with the same configuration on another dataset.
623    fn derive_index_params(&self) -> Result<ScalarIndexParams>;
624
625    /// Returns the value type expected by [`Self::update`], when the index has
626    /// a durable type contract for its training data.
627    ///
628    /// Wrapper indices use this to transform new data to the same type as the
629    /// loaded index instead of inferring a potentially different type from an
630    /// update batch. Index types without such a contract may return `None`.
631    fn training_data_type(&self) -> Option<DataType> {
632        None
633    }
634
635    /// Global `[min, max]` of the indexed column from index metadata, without a
636    /// scan, or `None` if this index type cannot supply a sound bound. When
637    /// `Some`, the range is a superset of live values (conservative under
638    /// deletes): safe to prune with, not guaranteed tight.
639    fn value_range(&self) -> Option<(ScalarValue, ScalarValue)> {
640        None
641    }
642}
643
644/// Abstraction over any type that can remap row IDs during index loading.
645///
646/// This decouples scalar index plugins from the table-level frag reuse index type.
647/// The frag reuse index implements this trait, but callers may also supply custom
648/// implementations for testing or other remapping strategies.
649pub trait RowIdRemapper: Send + Sync + std::fmt::Debug {
650    /// Remap a single row id.  Returns `None` if the row was deleted.
651    fn remap_row_id(&self, row_id: u64) -> Option<u64>;
652    /// Remap all addresses in a [`RowAddrTreeMap`], dropping deleted rows.
653    fn remap_row_addrs_tree_map(&self, row_addrs: &RowAddrTreeMap) -> RowAddrTreeMap;
654    /// Remap all row ids in a [`RoaringTreemap`], dropping deleted rows.
655    fn remap_row_ids_roaring_tree_map(&self, row_ids: &RoaringTreemap) -> RoaringTreemap;
656    /// Remap the row-id column at `row_id_idx` inside `batch`, dropping deleted rows.
657    fn remap_row_ids_record_batch(
658        &self,
659        batch: RecordBatch,
660        row_id_idx: usize,
661    ) -> Result<RecordBatch>;
662}