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}