Skip to main content

store_api/
sst_entry.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::Arc;
16
17use bytes::Bytes;
18use common_recordbatch::DfRecordBatch;
19use common_time::Timestamp;
20use common_time::timestamp::TimeUnit;
21use datafusion_common::DataFusionError;
22use datafusion_expr::{LogicalPlan, LogicalPlanBuilder, LogicalTableSource};
23use datatypes::arrow::array::{
24    ArrayRef, BinaryArray, BooleanArray, TimestampMillisecondArray, TimestampNanosecondArray,
25    UInt8Array, UInt32Array, UInt64Array,
26};
27use datatypes::arrow::datatypes::SchemaRef as ArrowSchemaRef;
28use datatypes::arrow::error::ArrowError;
29use datatypes::arrow_array::StringArray;
30use datatypes::schema::{ColumnSchema, Schema, SchemaRef};
31use serde::{Deserialize, Serialize};
32
33use crate::storage::{RegionGroup, RegionId, RegionNumber, RegionSeq, ScanRequest, TableId};
34
35// Manifest SST column names.
36pub const TABLE_DIR: &str = "table_dir";
37pub const REGION_ID: &str = "region_id";
38pub const TABLE_ID: &str = "table_id";
39pub const REGION_NUMBER: &str = "region_number";
40pub const REGION_GROUP: &str = "region_group";
41pub const REGION_SEQUENCE: &str = "region_sequence";
42pub const FILE_ID: &str = "file_id";
43pub const INDEX_VERSION: &str = "index_version";
44pub const LEVEL: &str = "level";
45pub const FILE_PATH: &str = "file_path";
46pub const FILE_SIZE: &str = "file_size";
47pub const INDEX_FILE_PATH: &str = "index_file_path";
48pub const INDEX_FILE_SIZE: &str = "index_file_size";
49pub const NUM_ROWS: &str = "num_rows";
50pub const NUM_ROW_GROUPS: &str = "num_row_groups";
51pub const NUM_SERIES: &str = "num_series";
52pub const MIN_TS: &str = "min_ts";
53pub const MAX_TS: &str = "max_ts";
54pub const SEQUENCE: &str = "sequence";
55pub const ORIGIN_REGION_ID: &str = "origin_region_id";
56pub const NODE_ID: &str = "node_id";
57pub const VISIBLE: &str = "visible";
58pub const PRIMARY_KEY_MIN: &str = "primary_key_min";
59pub const PRIMARY_KEY_MAX: &str = "primary_key_max";
60pub const MAX_ROW_GROUP_UNCOMPRESSED_SIZE: &str = "max_row_group_uncompressed_size";
61pub const PARTITION_EXPR: &str = "partition_expr";
62
63/// Puffin index type for bloom filter indexes.
64pub const PUFFIN_INDEX_TYPE_BLOOM_FILTER: &str = "bloom_filter";
65/// Puffin index type for bloom-backed full-text indexes.
66pub const PUFFIN_INDEX_TYPE_FULLTEXT_BLOOM: &str = "fulltext_bloom";
67/// Puffin index type for Tantivy-backed full-text indexes.
68pub const PUFFIN_INDEX_TYPE_FULLTEXT_TANTIVY: &str = "fulltext_tantivy";
69/// Puffin index type for inverted indexes.
70pub const PUFFIN_INDEX_TYPE_INVERTED: &str = "inverted";
71
72/// An entry describing a SST file known by the engine's manifest.
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74pub struct ManifestSstEntry {
75    /// The table directory this file belongs to.
76    pub table_dir: String,
77    /// The region id of region that refers to the file.
78    pub region_id: RegionId,
79    /// The table id this file belongs to.
80    pub table_id: TableId,
81    /// The region number this file belongs to.
82    pub region_number: RegionNumber,
83    /// The region group this file belongs to.
84    pub region_group: RegionGroup,
85    /// The region sequence this file belongs to.
86    pub region_sequence: RegionSeq,
87    /// Engine-specific file identifier (string form).
88    pub file_id: String,
89    /// Index version, increment when the index file is rebuilt.
90    pub index_version: u64,
91    /// SST level.
92    pub level: u8,
93    /// Full path of the SST file in object store.
94    pub file_path: String,
95    /// File size in bytes.
96    pub file_size: u64,
97    /// Maximum uncompressed row group size in bytes. Zero means unknown.
98    pub max_row_group_uncompressed_size: u64,
99    /// Full path of the index file in object store.
100    pub index_file_path: Option<String>,
101    /// File size of the index file in object store.
102    pub index_file_size: Option<u64>,
103    /// Number of rows in the SST.
104    pub num_rows: u64,
105    /// Number of row groups in the SST.
106    pub num_row_groups: u64,
107    /// Number of series in the SST.
108    pub num_series: Option<u64>,
109    /// Min timestamp.
110    pub min_ts: Timestamp,
111    /// Max timestamp.
112    pub max_ts: Timestamp,
113    /// The sequence number associated with this file.
114    pub sequence: Option<u64>,
115    /// Human-readable partition expression associated with this file.
116    pub partition_expr: Option<String>,
117    /// The region id of region that creates the file.
118    pub origin_region_id: RegionId,
119    /// The node id fetched from the manifest.
120    pub node_id: Option<u64>,
121    /// Whether this file is visible in current version.
122    pub visible: bool,
123    /// Minimum encoded primary key in the SST.
124    pub primary_key_min: Option<Bytes>,
125    /// Maximum encoded primary key in the SST.
126    pub primary_key_max: Option<Bytes>,
127}
128
129impl ManifestSstEntry {
130    /// Returns the schema of the manifest sst entry.
131    pub fn schema() -> SchemaRef {
132        use datatypes::prelude::ConcreteDataType as Ty;
133        Arc::new(Schema::new(vec![
134            ColumnSchema::new(TABLE_DIR, Ty::string_datatype(), false),
135            ColumnSchema::new(REGION_ID, Ty::uint64_datatype(), false),
136            ColumnSchema::new(TABLE_ID, Ty::uint32_datatype(), false),
137            ColumnSchema::new(REGION_NUMBER, Ty::uint32_datatype(), false),
138            ColumnSchema::new(REGION_GROUP, Ty::uint8_datatype(), false),
139            ColumnSchema::new(REGION_SEQUENCE, Ty::uint32_datatype(), false),
140            ColumnSchema::new(FILE_ID, Ty::string_datatype(), false),
141            ColumnSchema::new(INDEX_VERSION, Ty::uint64_datatype(), false),
142            ColumnSchema::new(LEVEL, Ty::uint8_datatype(), false),
143            ColumnSchema::new(FILE_PATH, Ty::string_datatype(), false),
144            ColumnSchema::new(FILE_SIZE, Ty::uint64_datatype(), false),
145            ColumnSchema::new(INDEX_FILE_PATH, Ty::string_datatype(), true),
146            ColumnSchema::new(INDEX_FILE_SIZE, Ty::uint64_datatype(), true),
147            ColumnSchema::new(NUM_ROWS, Ty::uint64_datatype(), false),
148            ColumnSchema::new(NUM_ROW_GROUPS, Ty::uint64_datatype(), false),
149            ColumnSchema::new(NUM_SERIES, Ty::uint64_datatype(), true),
150            ColumnSchema::new(MIN_TS, Ty::timestamp_nanosecond_datatype(), true),
151            ColumnSchema::new(MAX_TS, Ty::timestamp_nanosecond_datatype(), true),
152            ColumnSchema::new(SEQUENCE, Ty::uint64_datatype(), true),
153            ColumnSchema::new(ORIGIN_REGION_ID, Ty::uint64_datatype(), false),
154            ColumnSchema::new(NODE_ID, Ty::uint64_datatype(), true),
155            ColumnSchema::new(VISIBLE, Ty::boolean_datatype(), false),
156            ColumnSchema::new(PRIMARY_KEY_MIN, Ty::binary_datatype(), true),
157            ColumnSchema::new(PRIMARY_KEY_MAX, Ty::binary_datatype(), true),
158            ColumnSchema::new(
159                MAX_ROW_GROUP_UNCOMPRESSED_SIZE,
160                Ty::uint64_datatype(),
161                false,
162            ),
163            ColumnSchema::new(PARTITION_EXPR, Ty::string_datatype(), true),
164        ]))
165    }
166
167    /// Converts a list of manifest sst entries to a record batch.
168    pub fn to_record_batch(entries: &[Self]) -> std::result::Result<DfRecordBatch, ArrowError> {
169        Self::to_projected_record_batch(entries, None)
170    }
171
172    /// Builds only the requested columns, including empty and repeated projections.
173    fn to_projected_record_batch(
174        entries: &[Self],
175        projection: Option<&[usize]>,
176    ) -> std::result::Result<DfRecordBatch, ArrowError> {
177        let schema = Self::schema().arrow_schema().clone();
178        let schema = match projection {
179            Some(projection) => Arc::new(schema.project(projection)?),
180            None => schema,
181        };
182        ManifestSstBatchBuilder::new(schema).build(entries)
183    }
184
185    /// Reserved internal inspect table name.
186    ///
187    /// This table name is used only for building logical plans on the
188    /// frontend -> datanode path. It is not user-visible and cannot be
189    /// referenced by user queries.
190    pub fn reserved_table_name_for_inspection() -> &'static str {
191        "__inspect/__mito/__sst_manifest"
192    }
193
194    /// Builds a logical plan for scanning the manifest sst entries.
195    pub fn build_plan(scan_request: ScanRequest) -> Result<LogicalPlan, DataFusionError> {
196        build_plan_helper(
197            scan_request,
198            Self::reserved_table_name_for_inspection(),
199            Self::schema(),
200        )
201    }
202}
203
204/// Converts manifest SST batches using a schema prepared once per scan.
205/// Repeated projected columns share the same Arrow array.
206pub struct ManifestSstBatchBuilder {
207    schema: ArrowSchemaRef,
208    duplicates: Vec<Option<usize>>,
209}
210
211impl ManifestSstBatchBuilder {
212    /// Creates a builder for a projected manifest SST schema.
213    pub fn new(schema: ArrowSchemaRef) -> Self {
214        let duplicates = schema
215            .fields()
216            .iter()
217            .enumerate()
218            .map(|(index, field)| {
219                schema.fields()[..index]
220                    .iter()
221                    .position(|previous| previous == field)
222            })
223            .collect();
224        Self { schema, duplicates }
225    }
226
227    /// Builds the projected columns, copying string contents only into Arrow buffers.
228    pub fn build(
229        &self,
230        entries: &[ManifestSstEntry],
231    ) -> std::result::Result<DfRecordBatch, ArrowError> {
232        let mut columns: Vec<ArrayRef> = Vec::with_capacity(self.schema.fields().len());
233        for (index, field) in self.schema.fields().iter().enumerate() {
234            if let Some(previous) = self.duplicates[index] {
235                columns.push(columns[previous].clone());
236                continue;
237            }
238            let array: ArrayRef = match field.name().as_str() {
239                TABLE_DIR => Arc::new(StringArray::from_iter_values(
240                    entries.iter().map(|e| e.table_dir.as_str()),
241                )),
242                REGION_ID => Arc::new(UInt64Array::from_iter_values(
243                    entries.iter().map(|e| e.region_id.as_u64()),
244                )),
245                TABLE_ID => Arc::new(UInt32Array::from_iter_values(
246                    entries.iter().map(|e| e.table_id),
247                )),
248                REGION_NUMBER => Arc::new(UInt32Array::from_iter_values(
249                    entries.iter().map(|e| e.region_number),
250                )),
251                REGION_GROUP => Arc::new(UInt8Array::from_iter_values(
252                    entries.iter().map(|e| e.region_group),
253                )),
254                REGION_SEQUENCE => Arc::new(UInt32Array::from_iter_values(
255                    entries.iter().map(|e| e.region_sequence),
256                )),
257                FILE_ID => Arc::new(StringArray::from_iter_values(
258                    entries.iter().map(|e| e.file_id.as_str()),
259                )),
260                INDEX_VERSION => Arc::new(UInt64Array::from_iter(
261                    entries.iter().map(|e| e.index_version),
262                )),
263                LEVEL => Arc::new(UInt8Array::from_iter_values(
264                    entries.iter().map(|e| e.level),
265                )),
266                FILE_PATH => Arc::new(StringArray::from_iter_values(
267                    entries.iter().map(|e| e.file_path.as_str()),
268                )),
269                FILE_SIZE => Arc::new(UInt64Array::from_iter_values(
270                    entries.iter().map(|e| e.file_size),
271                )),
272                INDEX_FILE_PATH => Arc::new(StringArray::from_iter(
273                    entries.iter().map(|e| e.index_file_path.as_ref()),
274                )),
275                INDEX_FILE_SIZE => Arc::new(UInt64Array::from_iter(
276                    entries.iter().map(|e| e.index_file_size),
277                )),
278                NUM_ROWS => Arc::new(UInt64Array::from_iter_values(
279                    entries.iter().map(|e| e.num_rows),
280                )),
281                NUM_ROW_GROUPS => Arc::new(UInt64Array::from_iter_values(
282                    entries.iter().map(|e| e.num_row_groups),
283                )),
284                NUM_SERIES => {
285                    Arc::new(UInt64Array::from_iter(entries.iter().map(|e| e.num_series)))
286                }
287                MIN_TS => Arc::new(TimestampNanosecondArray::from_iter(entries.iter().map(
288                    |e| {
289                        e.min_ts
290                            .convert_to(TimeUnit::Nanosecond)
291                            .map(|ts| ts.value())
292                    },
293                ))),
294                MAX_TS => Arc::new(TimestampNanosecondArray::from_iter(entries.iter().map(
295                    |e| {
296                        e.max_ts
297                            .convert_to(TimeUnit::Nanosecond)
298                            .map(|ts| ts.value())
299                    },
300                ))),
301                SEQUENCE => Arc::new(UInt64Array::from_iter(entries.iter().map(|e| e.sequence))),
302                ORIGIN_REGION_ID => Arc::new(UInt64Array::from_iter_values(
303                    entries.iter().map(|e| e.origin_region_id.as_u64()),
304                )),
305                NODE_ID => Arc::new(UInt64Array::from_iter(entries.iter().map(|e| e.node_id))),
306                VISIBLE => Arc::new(BooleanArray::from_iter(
307                    entries.iter().map(|e| Some(e.visible)),
308                )),
309                PRIMARY_KEY_MIN => Arc::new(BinaryArray::from_iter(
310                    entries.iter().map(|e| e.primary_key_min.as_deref()),
311                )),
312                PRIMARY_KEY_MAX => Arc::new(BinaryArray::from_iter(
313                    entries.iter().map(|e| e.primary_key_max.as_deref()),
314                )),
315                MAX_ROW_GROUP_UNCOMPRESSED_SIZE => Arc::new(UInt64Array::from_iter_values(
316                    entries.iter().map(|e| e.max_row_group_uncompressed_size),
317                )),
318                PARTITION_EXPR => Arc::new(StringArray::from_iter(
319                    entries.iter().map(|e| e.partition_expr.as_ref()),
320                )),
321                name => {
322                    return Err(ArrowError::SchemaError(format!(
323                        "Unknown manifest SST column: {name}"
324                    )));
325                }
326            };
327            columns.push(array);
328        }
329        DfRecordBatch::try_new_with_options(
330            self.schema.clone(),
331            columns,
332            &datatypes::arrow::record_batch::RecordBatchOptions::new()
333                .with_row_count(Some(entries.len())),
334        )
335    }
336}
337
338/// An entry describing a SST file listed from storage layer directly.
339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
340pub struct StorageSstEntry {
341    /// Full path of the SST file in object store.
342    pub file_path: String,
343    /// File size in bytes.
344    pub file_size: Option<u64>,
345    /// Last modified time in milliseconds since epoch, if available from storage.
346    pub last_modified_ms: Option<Timestamp>,
347    /// The node id fetched from the manifest.
348    pub node_id: Option<u64>,
349}
350
351impl StorageSstEntry {
352    /// Returns the schema of the storage sst entry.
353    pub fn schema() -> SchemaRef {
354        use datatypes::prelude::ConcreteDataType as Ty;
355        Arc::new(Schema::new(vec![
356            ColumnSchema::new("file_path", Ty::string_datatype(), false),
357            ColumnSchema::new("file_size", Ty::uint64_datatype(), true),
358            ColumnSchema::new(
359                "last_modified_ms",
360                Ty::timestamp_millisecond_datatype(),
361                true,
362            ),
363            ColumnSchema::new("node_id", Ty::uint64_datatype(), true),
364        ]))
365    }
366
367    /// Converts a list of storage sst entries to a record batch.
368    pub fn to_record_batch(entries: &[Self]) -> std::result::Result<DfRecordBatch, ArrowError> {
369        let schema = Self::schema();
370        let file_paths = entries.iter().map(|e| e.file_path.as_str());
371        let file_sizes = entries.iter().map(|e| e.file_size);
372        let last_modified_ms = entries.iter().map(|e| {
373            e.last_modified_ms
374                .and_then(|ts| ts.convert_to(TimeUnit::Millisecond).map(|ts| ts.value()))
375        });
376        let node_ids = entries.iter().map(|e| e.node_id);
377
378        let columns: Vec<ArrayRef> = vec![
379            Arc::new(StringArray::from_iter_values(file_paths)),
380            Arc::new(UInt64Array::from_iter(file_sizes)),
381            Arc::new(TimestampMillisecondArray::from_iter(last_modified_ms)),
382            Arc::new(UInt64Array::from_iter(node_ids)),
383        ];
384
385        DfRecordBatch::try_new(schema.arrow_schema().clone(), columns)
386    }
387
388    /// Reserved internal inspect table name.
389    ///
390    /// This table name is used only for building logical plans on the
391    /// frontend -> datanode path. It is not user-visible and cannot be
392    /// referenced by user queries.
393    pub fn reserved_table_name_for_inspection() -> &'static str {
394        "__inspect/__mito/__sst_storage"
395    }
396
397    /// Builds a logical plan for scanning the storage sst entries.
398    pub fn build_plan(scan_request: ScanRequest) -> Result<LogicalPlan, DataFusionError> {
399        build_plan_helper(
400            scan_request,
401            Self::reserved_table_name_for_inspection(),
402            Self::schema(),
403        )
404    }
405}
406
407/// An entry describing puffin index metadata for inspection.
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
409pub struct PuffinIndexMetaEntry {
410    /// The table directory this index belongs to.
411    pub table_dir: String,
412    /// The full path of the index file in object store.
413    pub index_file_path: String,
414    /// The region id referencing the index file.
415    pub region_id: RegionId,
416    /// The table id referencing the index file.
417    pub table_id: TableId,
418    /// The region number referencing the index file.
419    pub region_number: RegionNumber,
420    /// The region group referencing the index file.
421    pub region_group: RegionGroup,
422    /// The region sequence referencing the index file.
423    pub region_sequence: RegionSeq,
424    /// Engine-specific file identifier (string form).
425    pub file_id: String,
426    /// Size of the index file in object store (if available).
427    pub index_file_size: Option<u64>,
428    /// Logical index type (`bloom_filter`, `fulltext_bloom`, `fulltext_tantivy`, `inverted`).
429    pub index_type: String,
430    /// Target type (`column`, ...).
431    pub target_type: String,
432    /// Encoded target key string.
433    pub target_key: String,
434    /// Structured JSON describing the target.
435    pub target_json: String,
436    /// Size of the blob storing this target.
437    pub blob_size: u64,
438    /// Structured JSON describing index-specific metadata (if available).
439    pub meta_json: Option<String>,
440    /// Node id associated with the index file (if known).
441    pub node_id: Option<u64>,
442}
443
444impl PuffinIndexMetaEntry {
445    /// Returns the schema describing puffin index metadata entries.
446    pub fn schema() -> SchemaRef {
447        use datatypes::prelude::ConcreteDataType as Ty;
448        Arc::new(Schema::new(vec![
449            ColumnSchema::new("table_dir", Ty::string_datatype(), false),
450            ColumnSchema::new("index_file_path", Ty::string_datatype(), false),
451            ColumnSchema::new("region_id", Ty::uint64_datatype(), false),
452            ColumnSchema::new("table_id", Ty::uint32_datatype(), false),
453            ColumnSchema::new("region_number", Ty::uint32_datatype(), false),
454            ColumnSchema::new("region_group", Ty::uint8_datatype(), false),
455            ColumnSchema::new("region_sequence", Ty::uint32_datatype(), false),
456            ColumnSchema::new("file_id", Ty::string_datatype(), false),
457            ColumnSchema::new("index_file_size", Ty::uint64_datatype(), true),
458            ColumnSchema::new("index_type", Ty::string_datatype(), false),
459            ColumnSchema::new("target_type", Ty::string_datatype(), false),
460            ColumnSchema::new("target_key", Ty::string_datatype(), false),
461            ColumnSchema::new("target_json", Ty::string_datatype(), false),
462            ColumnSchema::new("blob_size", Ty::uint64_datatype(), false),
463            ColumnSchema::new("meta_json", Ty::string_datatype(), true),
464            ColumnSchema::new("node_id", Ty::uint64_datatype(), true),
465        ]))
466    }
467
468    /// Converts a list of puffin index metadata entries to a record batch.
469    pub fn to_record_batch(entries: &[Self]) -> std::result::Result<DfRecordBatch, ArrowError> {
470        let schema = Self::schema();
471        let table_dirs = entries.iter().map(|e| e.table_dir.as_str());
472        let index_file_paths = entries.iter().map(|e| e.index_file_path.as_str());
473        let region_ids = entries.iter().map(|e| e.region_id.as_u64());
474        let table_ids = entries.iter().map(|e| e.table_id);
475        let region_numbers = entries.iter().map(|e| e.region_number);
476        let region_groups = entries.iter().map(|e| e.region_group);
477        let region_sequences = entries.iter().map(|e| e.region_sequence);
478        let file_ids = entries.iter().map(|e| e.file_id.as_str());
479        let index_file_sizes = entries.iter().map(|e| e.index_file_size);
480        let index_types = entries.iter().map(|e| e.index_type.as_str());
481        let target_types = entries.iter().map(|e| e.target_type.as_str());
482        let target_keys = entries.iter().map(|e| e.target_key.as_str());
483        let target_jsons = entries.iter().map(|e| e.target_json.as_str());
484        let blob_sizes = entries.iter().map(|e| e.blob_size);
485        let meta_jsons = entries.iter().map(|e| e.meta_json.as_deref());
486        let node_ids = entries.iter().map(|e| e.node_id);
487
488        let columns: Vec<ArrayRef> = vec![
489            Arc::new(StringArray::from_iter_values(table_dirs)),
490            Arc::new(StringArray::from_iter_values(index_file_paths)),
491            Arc::new(UInt64Array::from_iter_values(region_ids)),
492            Arc::new(UInt32Array::from_iter_values(table_ids)),
493            Arc::new(UInt32Array::from_iter_values(region_numbers)),
494            Arc::new(UInt8Array::from_iter_values(region_groups)),
495            Arc::new(UInt32Array::from_iter_values(region_sequences)),
496            Arc::new(StringArray::from_iter_values(file_ids)),
497            Arc::new(UInt64Array::from_iter(index_file_sizes)),
498            Arc::new(StringArray::from_iter_values(index_types)),
499            Arc::new(StringArray::from_iter_values(target_types)),
500            Arc::new(StringArray::from_iter_values(target_keys)),
501            Arc::new(StringArray::from_iter_values(target_jsons)),
502            Arc::new(UInt64Array::from_iter_values(blob_sizes)),
503            Arc::new(StringArray::from_iter(meta_jsons)),
504            Arc::new(UInt64Array::from_iter(node_ids)),
505        ];
506
507        DfRecordBatch::try_new(schema.arrow_schema().clone(), columns)
508    }
509
510    /// Reserved internal inspect table name for puffin index metadata.
511    pub fn reserved_table_name_for_inspection() -> &'static str {
512        "__inspect/__mito/__puffin_index_meta"
513    }
514
515    /// Builds a logical plan for scanning puffin index metadata entries.
516    pub fn build_plan(scan_request: ScanRequest) -> Result<LogicalPlan, DataFusionError> {
517        build_plan_helper(
518            scan_request,
519            Self::reserved_table_name_for_inspection(),
520            Self::schema(),
521        )
522    }
523}
524
525fn build_plan_helper(
526    scan_request: ScanRequest,
527    table_name: &str,
528    schema: SchemaRef,
529) -> Result<LogicalPlan, DataFusionError> {
530    let table_source = LogicalTableSource::new(schema.arrow_schema().clone());
531
532    // Filters may reference columns omitted from the requested output. Apply
533    // them to the full schema and let the optimizer push the minimal projection.
534    let projection = scan_request.projection;
535    let mut builder = LogicalPlanBuilder::scan(table_name, Arc::new(table_source), None)?;
536
537    for filter in scan_request.filters {
538        builder = builder.filter(filter)?;
539    }
540
541    if let Some(projection) = projection {
542        let projected = schema.arrow_schema().project(&projection)?;
543        builder = builder.project(
544            projected
545                .fields()
546                .iter()
547                .map(|field| datafusion_expr::col(field.name())),
548        )?;
549    }
550
551    if let Some(limit) = scan_request.limit {
552        builder = builder.limit(0, Some(limit))?;
553    }
554
555    builder.build()
556}
557
558#[cfg(test)]
559mod tests {
560    use datafusion_common::TableReference;
561    use datafusion_expr::{Expr, LogicalPlan, Operator, binary_expr, col, lit};
562    use datatypes::arrow::array::{
563        Array, BinaryArray, TimestampMillisecondArray, TimestampNanosecondArray, UInt8Array,
564        UInt32Array, UInt64Array,
565    };
566    use datatypes::arrow_array::StringArray;
567
568    use super::*;
569
570    #[test]
571    fn test_sst_entry_manifest_to_record_batch() {
572        // Prepare entries
573        let table_id1: TableId = 1;
574        let region_group1: RegionGroup = 2;
575        let region_seq1: RegionSeq = 3;
576        let region_number1: RegionNumber = ((region_group1 as u32) << 24) | region_seq1;
577        let region_id1 = RegionId::with_group_and_seq(table_id1, region_group1, region_seq1);
578
579        let table_id2: TableId = 5;
580        let region_group2: RegionGroup = 1;
581        let region_seq2: RegionSeq = 42;
582        let region_number2: RegionNumber = ((region_group2 as u32) << 24) | region_seq2;
583        let region_id2 = RegionId::with_group_and_seq(table_id2, region_group2, region_seq2);
584
585        let entries = vec![
586            ManifestSstEntry {
587                table_dir: "tdir1".to_string(),
588                region_id: region_id1,
589                table_id: table_id1,
590                region_number: region_number1,
591                region_group: region_group1,
592                region_sequence: region_seq1,
593                file_id: "f1".to_string(),
594                index_version: 0,
595                level: 1,
596                file_path: "/p1".to_string(),
597                file_size: 100,
598                max_row_group_uncompressed_size: 80,
599                index_file_path: None,
600                index_file_size: None,
601                num_rows: 10,
602                num_row_groups: 2,
603                num_series: Some(5),
604                min_ts: Timestamp::new_millisecond(1000), // 1s -> 1_000_000_000ns
605                max_ts: Timestamp::new_second(2),         // 2s -> 2_000_000_000ns
606                sequence: None,
607                partition_expr: Some("a < 10".to_string()),
608                origin_region_id: region_id1,
609                node_id: Some(1),
610                visible: false,
611                primary_key_min: Some(Bytes::from_static(b"aaa")),
612                primary_key_max: Some(Bytes::from_static(b"zzz")),
613            },
614            ManifestSstEntry {
615                table_dir: "tdir2".to_string(),
616                region_id: region_id2,
617                table_id: table_id2,
618                region_number: region_number2,
619                region_group: region_group2,
620                region_sequence: region_seq2,
621                file_id: "f2".to_string(),
622                index_version: 1,
623                level: 3,
624                file_path: "/p2".to_string(),
625                file_size: 200,
626                max_row_group_uncompressed_size: 160,
627                index_file_path: Some("idx".to_string()),
628                index_file_size: Some(11),
629                num_rows: 20,
630                num_row_groups: 4,
631                num_series: None,
632                min_ts: Timestamp::new_nanosecond(5),     // 5ns
633                max_ts: Timestamp::new_microsecond(2000), // 2ms -> 2_000_000ns
634                sequence: Some(9),
635                partition_expr: None,
636                origin_region_id: region_id2,
637                node_id: None,
638                visible: true,
639                primary_key_min: None,
640                primary_key_max: None,
641            },
642        ];
643
644        let schema = ManifestSstEntry::schema();
645        let batch = ManifestSstEntry::to_record_batch(&entries).unwrap();
646        for projection in [vec![], vec![16, 6, 10], vec![1, 1], (0..26).rev().collect()] {
647            let projected =
648                ManifestSstEntry::to_projected_record_batch(&entries, Some(&projection)).unwrap();
649            assert_eq!(projected, batch.project(&projection).unwrap());
650            assert_eq!(projected.num_rows(), entries.len());
651        }
652        assert!(ManifestSstEntry::to_projected_record_batch(&entries, Some(&[26])).is_err());
653
654        let projected_schema = Arc::new(batch.schema().project(&[6, 9, 6, 11, 25, 9]).unwrap());
655        let builder = ManifestSstBatchBuilder::new(projected_schema.clone());
656        for rows in [entries.as_slice(), &entries[..1], &entries[..0]] {
657            let projected = builder.build(rows).unwrap();
658            assert_eq!(
659                projected,
660                ManifestSstEntry::to_record_batch(rows)
661                    .unwrap()
662                    .project(&[6, 9, 6, 11, 25, 9])
663                    .unwrap()
664            );
665            assert!(Arc::ptr_eq(&projected.schema(), &projected_schema));
666            assert!(Arc::ptr_eq(projected.column(0), projected.column(2)));
667            assert!(Arc::ptr_eq(projected.column(1), projected.column(5)));
668        }
669
670        // Schema checks
671        assert_eq!(schema.arrow_schema().fields().len(), batch.num_columns());
672        assert_eq!(2, batch.num_rows());
673        let expected_columns = [
674            "table_dir",
675            "region_id",
676            "table_id",
677            "region_number",
678            "region_group",
679            "region_sequence",
680            "file_id",
681            "index_version",
682            "level",
683            "file_path",
684            "file_size",
685            "index_file_path",
686            "index_file_size",
687            "num_rows",
688            "num_row_groups",
689            "num_series",
690            "min_ts",
691            "max_ts",
692            "sequence",
693            "origin_region_id",
694            "node_id",
695            "visible",
696            "primary_key_min",
697            "primary_key_max",
698            "max_row_group_uncompressed_size",
699            "partition_expr",
700        ];
701        assert_eq!(
702            expected_columns,
703            schema
704                .arrow_schema()
705                .fields()
706                .iter()
707                .map(|field| field.name().as_str())
708                .collect::<Vec<_>>()
709                .as_slice()
710        );
711        for (i, f) in schema.arrow_schema().fields().iter().enumerate() {
712            assert_eq!(f.name(), batch.schema().field(i).name());
713            assert_eq!(f.is_nullable(), batch.schema().field(i).is_nullable());
714            assert_eq!(f.data_type(), batch.schema().field(i).data_type());
715        }
716
717        // Column asserts
718        let table_dirs = batch
719            .column(0)
720            .as_any()
721            .downcast_ref::<StringArray>()
722            .unwrap();
723        assert_eq!("tdir1", table_dirs.value(0));
724        assert_eq!("tdir2", table_dirs.value(1));
725
726        let region_ids = batch
727            .column(1)
728            .as_any()
729            .downcast_ref::<UInt64Array>()
730            .unwrap();
731        assert_eq!(region_id1.as_u64(), region_ids.value(0));
732        assert_eq!(region_id2.as_u64(), region_ids.value(1));
733
734        let table_ids = batch
735            .column(2)
736            .as_any()
737            .downcast_ref::<UInt32Array>()
738            .unwrap();
739        assert_eq!(table_id1, table_ids.value(0));
740        assert_eq!(table_id2, table_ids.value(1));
741
742        let region_numbers = batch
743            .column(3)
744            .as_any()
745            .downcast_ref::<UInt32Array>()
746            .unwrap();
747        assert_eq!(region_number1, region_numbers.value(0));
748        assert_eq!(region_number2, region_numbers.value(1));
749
750        let region_groups = batch
751            .column(4)
752            .as_any()
753            .downcast_ref::<UInt8Array>()
754            .unwrap();
755        assert_eq!(region_group1, region_groups.value(0));
756        assert_eq!(region_group2, region_groups.value(1));
757
758        let region_sequences = batch
759            .column(5)
760            .as_any()
761            .downcast_ref::<UInt32Array>()
762            .unwrap();
763        assert_eq!(region_seq1, region_sequences.value(0));
764        assert_eq!(region_seq2, region_sequences.value(1));
765
766        let file_ids = batch
767            .column(6)
768            .as_any()
769            .downcast_ref::<StringArray>()
770            .unwrap();
771        assert_eq!("f1", file_ids.value(0));
772        assert_eq!("f2", file_ids.value(1));
773
774        let index_versions = batch
775            .column(7)
776            .as_any()
777            .downcast_ref::<UInt64Array>()
778            .unwrap();
779        assert_eq!(0, index_versions.value(0));
780        assert_eq!(1, index_versions.value(1));
781
782        let levels = batch
783            .column(8)
784            .as_any()
785            .downcast_ref::<UInt8Array>()
786            .unwrap();
787        assert_eq!(1, levels.value(0));
788        assert_eq!(3, levels.value(1));
789
790        let file_paths = batch
791            .column(9)
792            .as_any()
793            .downcast_ref::<StringArray>()
794            .unwrap();
795        assert_eq!("/p1", file_paths.value(0));
796        assert_eq!("/p2", file_paths.value(1));
797
798        let file_sizes = batch
799            .column(10)
800            .as_any()
801            .downcast_ref::<UInt64Array>()
802            .unwrap();
803        assert_eq!(100, file_sizes.value(0));
804        assert_eq!(200, file_sizes.value(1));
805
806        let index_file_paths = batch
807            .column(11)
808            .as_any()
809            .downcast_ref::<StringArray>()
810            .unwrap();
811        assert!(index_file_paths.is_null(0));
812        assert_eq!("idx", index_file_paths.value(1));
813
814        let index_file_sizes = batch
815            .column(12)
816            .as_any()
817            .downcast_ref::<UInt64Array>()
818            .unwrap();
819        assert!(index_file_sizes.is_null(0));
820        assert_eq!(11, index_file_sizes.value(1));
821
822        let num_rows = batch
823            .column(13)
824            .as_any()
825            .downcast_ref::<UInt64Array>()
826            .unwrap();
827        assert_eq!(10, num_rows.value(0));
828        assert_eq!(20, num_rows.value(1));
829
830        let num_row_groups = batch
831            .column(14)
832            .as_any()
833            .downcast_ref::<UInt64Array>()
834            .unwrap();
835        assert_eq!(2, num_row_groups.value(0));
836        assert_eq!(4, num_row_groups.value(1));
837
838        let num_series = batch
839            .column(15)
840            .as_any()
841            .downcast_ref::<UInt64Array>()
842            .unwrap();
843        assert_eq!(5, num_series.value(0));
844        assert!(num_series.is_null(1));
845
846        let min_ts = batch
847            .column(16)
848            .as_any()
849            .downcast_ref::<TimestampNanosecondArray>()
850            .unwrap();
851        assert_eq!(1_000_000_000, min_ts.value(0));
852        assert_eq!(5, min_ts.value(1));
853
854        let max_ts = batch
855            .column(17)
856            .as_any()
857            .downcast_ref::<TimestampNanosecondArray>()
858            .unwrap();
859        assert_eq!(2_000_000_000, max_ts.value(0));
860        assert_eq!(2_000_000, max_ts.value(1));
861
862        let sequences = batch
863            .column(18)
864            .as_any()
865            .downcast_ref::<UInt64Array>()
866            .unwrap();
867        assert!(sequences.is_null(0));
868        assert_eq!(9, sequences.value(1));
869
870        let origin_region_ids = batch
871            .column(19)
872            .as_any()
873            .downcast_ref::<UInt64Array>()
874            .unwrap();
875        assert_eq!(region_id1.as_u64(), origin_region_ids.value(0));
876        assert_eq!(region_id2.as_u64(), origin_region_ids.value(1));
877
878        let node_ids = batch
879            .column(20)
880            .as_any()
881            .downcast_ref::<UInt64Array>()
882            .unwrap();
883        assert_eq!(1, node_ids.value(0));
884        assert!(node_ids.is_null(1));
885
886        let visible = batch
887            .column(21)
888            .as_any()
889            .downcast_ref::<BooleanArray>()
890            .unwrap();
891        assert!(!visible.value(0));
892        assert!(visible.value(1));
893
894        let primary_key_min = batch
895            .column(22)
896            .as_any()
897            .downcast_ref::<BinaryArray>()
898            .unwrap();
899        assert_eq!(b"aaa", primary_key_min.value(0));
900        assert!(primary_key_min.is_null(1));
901
902        let primary_key_max = batch
903            .column(23)
904            .as_any()
905            .downcast_ref::<BinaryArray>()
906            .unwrap();
907        assert_eq!(b"zzz", primary_key_max.value(0));
908        assert!(primary_key_max.is_null(1));
909
910        let max_row_group_uncompressed_sizes = batch
911            .column(24)
912            .as_any()
913            .downcast_ref::<UInt64Array>()
914            .unwrap();
915        assert_eq!(80, max_row_group_uncompressed_sizes.value(0));
916        assert_eq!(160, max_row_group_uncompressed_sizes.value(1));
917
918        let partition_exprs = batch
919            .column(25)
920            .as_any()
921            .downcast_ref::<StringArray>()
922            .unwrap();
923        assert_eq!("a < 10", partition_exprs.value(0));
924        assert!(partition_exprs.is_null(1));
925    }
926
927    #[test]
928    fn test_sst_entry_storage_to_record_batch() {
929        let entries = vec![
930            StorageSstEntry {
931                file_path: "/s1".to_string(),
932                file_size: None,
933                last_modified_ms: None,
934                node_id: Some(1),
935            },
936            StorageSstEntry {
937                file_path: "/s2".to_string(),
938                file_size: Some(123),
939                last_modified_ms: Some(Timestamp::new_millisecond(456)),
940                node_id: None,
941            },
942        ];
943
944        let schema = StorageSstEntry::schema();
945        let batch = StorageSstEntry::to_record_batch(&entries).unwrap();
946
947        assert_eq!(schema.arrow_schema().fields().len(), batch.num_columns());
948        assert_eq!(2, batch.num_rows());
949
950        let file_paths = batch
951            .column(0)
952            .as_any()
953            .downcast_ref::<StringArray>()
954            .unwrap();
955        assert_eq!("/s1", file_paths.value(0));
956        assert_eq!("/s2", file_paths.value(1));
957
958        let file_sizes = batch
959            .column(1)
960            .as_any()
961            .downcast_ref::<UInt64Array>()
962            .unwrap();
963        assert!(file_sizes.is_null(0));
964        assert_eq!(123, file_sizes.value(1));
965
966        let last_modified = batch
967            .column(2)
968            .as_any()
969            .downcast_ref::<TimestampMillisecondArray>()
970            .unwrap();
971        assert!(last_modified.is_null(0));
972        assert_eq!(456, last_modified.value(1));
973
974        let node_ids = batch
975            .column(3)
976            .as_any()
977            .downcast_ref::<UInt64Array>()
978            .unwrap();
979        assert_eq!(1, node_ids.value(0));
980        assert!(node_ids.is_null(1));
981    }
982
983    #[test]
984    fn test_puffin_index_meta_to_record_batch() {
985        let entries = vec![
986            PuffinIndexMetaEntry {
987                table_dir: "table1".to_string(),
988                index_file_path: "index1".to_string(),
989                region_id: RegionId::with_group_and_seq(10, 0, 20),
990                table_id: 10,
991                region_number: 20,
992                region_group: 0,
993                region_sequence: 20,
994                file_id: "file1".to_string(),
995                index_file_size: Some(1024),
996                index_type: "bloom_filter".to_string(),
997                target_type: "column".to_string(),
998                target_key: "1".to_string(),
999                target_json: "{\"column\":1}".to_string(),
1000                blob_size: 256,
1001                meta_json: Some("{\"bloom\":{}}".to_string()),
1002                node_id: Some(42),
1003            },
1004            PuffinIndexMetaEntry {
1005                table_dir: "table2".to_string(),
1006                index_file_path: "index2".to_string(),
1007                region_id: RegionId::with_group_and_seq(11, 0, 21),
1008                table_id: 11,
1009                region_number: 21,
1010                region_group: 0,
1011                region_sequence: 21,
1012                file_id: "file2".to_string(),
1013                index_file_size: None,
1014                index_type: "inverted".to_string(),
1015                target_type: "unknown".to_string(),
1016                target_key: "legacy".to_string(),
1017                target_json: "{}".to_string(),
1018                blob_size: 0,
1019                meta_json: None,
1020                node_id: None,
1021            },
1022        ];
1023
1024        let schema = PuffinIndexMetaEntry::schema();
1025        let batch = PuffinIndexMetaEntry::to_record_batch(&entries).unwrap();
1026
1027        assert_eq!(schema.arrow_schema().fields().len(), batch.num_columns());
1028        assert_eq!(2, batch.num_rows());
1029
1030        let table_dirs = batch
1031            .column(0)
1032            .as_any()
1033            .downcast_ref::<StringArray>()
1034            .unwrap();
1035        assert_eq!("table1", table_dirs.value(0));
1036        assert_eq!("table2", table_dirs.value(1));
1037
1038        let index_file_paths = batch
1039            .column(1)
1040            .as_any()
1041            .downcast_ref::<StringArray>()
1042            .unwrap();
1043        assert_eq!("index1", index_file_paths.value(0));
1044        assert_eq!("index2", index_file_paths.value(1));
1045
1046        let region_ids = batch
1047            .column(2)
1048            .as_any()
1049            .downcast_ref::<UInt64Array>()
1050            .unwrap();
1051        assert_eq!(
1052            RegionId::with_group_and_seq(10, 0, 20).as_u64(),
1053            region_ids.value(0)
1054        );
1055        assert_eq!(
1056            RegionId::with_group_and_seq(11, 0, 21).as_u64(),
1057            region_ids.value(1)
1058        );
1059
1060        let table_ids = batch
1061            .column(3)
1062            .as_any()
1063            .downcast_ref::<UInt32Array>()
1064            .unwrap();
1065        assert_eq!(10, table_ids.value(0));
1066        assert_eq!(11, table_ids.value(1));
1067
1068        let region_numbers = batch
1069            .column(4)
1070            .as_any()
1071            .downcast_ref::<UInt32Array>()
1072            .unwrap();
1073        assert_eq!(20, region_numbers.value(0));
1074        assert_eq!(21, region_numbers.value(1));
1075
1076        let region_groups = batch
1077            .column(5)
1078            .as_any()
1079            .downcast_ref::<UInt8Array>()
1080            .unwrap();
1081        assert_eq!(0, region_groups.value(0));
1082        assert_eq!(0, region_groups.value(1));
1083
1084        let region_sequences = batch
1085            .column(6)
1086            .as_any()
1087            .downcast_ref::<UInt32Array>()
1088            .unwrap();
1089        assert_eq!(20, region_sequences.value(0));
1090        assert_eq!(21, region_sequences.value(1));
1091
1092        let file_ids = batch
1093            .column(7)
1094            .as_any()
1095            .downcast_ref::<StringArray>()
1096            .unwrap();
1097        assert_eq!("file1", file_ids.value(0));
1098        assert_eq!("file2", file_ids.value(1));
1099
1100        let index_file_sizes = batch
1101            .column(8)
1102            .as_any()
1103            .downcast_ref::<UInt64Array>()
1104            .unwrap();
1105        assert_eq!(1024, index_file_sizes.value(0));
1106        assert!(index_file_sizes.is_null(1));
1107
1108        let index_types = batch
1109            .column(9)
1110            .as_any()
1111            .downcast_ref::<StringArray>()
1112            .unwrap();
1113        assert_eq!("bloom_filter", index_types.value(0));
1114        assert_eq!("inverted", index_types.value(1));
1115
1116        let target_types = batch
1117            .column(10)
1118            .as_any()
1119            .downcast_ref::<StringArray>()
1120            .unwrap();
1121        assert_eq!("column", target_types.value(0));
1122        assert_eq!("unknown", target_types.value(1));
1123
1124        let target_keys = batch
1125            .column(11)
1126            .as_any()
1127            .downcast_ref::<StringArray>()
1128            .unwrap();
1129        assert_eq!("1", target_keys.value(0));
1130        assert_eq!("legacy", target_keys.value(1));
1131
1132        let target_json = batch
1133            .column(12)
1134            .as_any()
1135            .downcast_ref::<StringArray>()
1136            .unwrap();
1137        assert_eq!("{\"column\":1}", target_json.value(0));
1138        assert_eq!("{}", target_json.value(1));
1139
1140        let blob_sizes = batch
1141            .column(13)
1142            .as_any()
1143            .downcast_ref::<UInt64Array>()
1144            .unwrap();
1145        assert_eq!(256, blob_sizes.value(0));
1146        assert_eq!(0, blob_sizes.value(1));
1147
1148        let meta_jsons = batch
1149            .column(14)
1150            .as_any()
1151            .downcast_ref::<StringArray>()
1152            .unwrap();
1153        assert_eq!("{\"bloom\":{}}", meta_jsons.value(0));
1154        assert!(meta_jsons.is_null(1));
1155
1156        let node_ids = batch
1157            .column(15)
1158            .as_any()
1159            .downcast_ref::<UInt64Array>()
1160            .unwrap();
1161        assert_eq!(42, node_ids.value(0));
1162        assert!(node_ids.is_null(1));
1163    }
1164
1165    #[test]
1166    fn test_manifest_build_plan() {
1167        let request = ScanRequest {
1168            projection: Some(vec![0, 1]),
1169            filters: vec![col("table_id").gt(lit(0u32))],
1170            limit: Some(5),
1171            ..Default::default()
1172        };
1173
1174        let plan = ManifestSstEntry::build_plan(request).unwrap();
1175
1176        // The hidden table_id predicate must run before projection and limit.
1177        let LogicalPlan::Limit(limit) = &plan else {
1178            panic!("expected limit")
1179        };
1180        let LogicalPlan::Projection(projection) = limit.input.as_ref() else {
1181            panic!("expected projection")
1182        };
1183        let LogicalPlan::Filter(filter) = projection.input.as_ref() else {
1184            panic!("expected filter")
1185        };
1186        let LogicalPlan::TableScan(scan) = filter.input.as_ref() else {
1187            panic!("expected table scan")
1188        };
1189        assert_eq!(
1190            scan.table_name,
1191            TableReference::bare(ManifestSstEntry::reserved_table_name_for_inspection())
1192        );
1193        assert_eq!(scan.projection, None);
1194        assert_eq!(
1195            filter.predicate,
1196            Expr::Column(datafusion_common::Column::new(
1197                Some(scan.table_name.clone()),
1198                "table_id",
1199            ))
1200            .gt(lit(0u32))
1201        );
1202
1203        let fields = plan.schema().fields();
1204        assert_eq!(fields.len(), 2);
1205        assert_eq!(fields[0].name(), "table_dir");
1206        assert_eq!(fields[1].name(), "region_id");
1207    }
1208
1209    #[test]
1210    fn test_storage_build_plan() {
1211        let projection = Some(vec![0, 2]);
1212        let request = ScanRequest {
1213            projection,
1214            filters: vec![binary_expr(col("file_path"), Operator::Eq, lit("/a"))],
1215            limit: Some(1),
1216            ..Default::default()
1217        };
1218
1219        let plan = StorageSstEntry::build_plan(request).unwrap();
1220        let (scan, has_filter, has_limit) = extract_scan(&plan);
1221        assert!(has_filter);
1222        assert!(has_limit);
1223        assert_eq!(
1224            scan.table_name,
1225            TableReference::bare(StorageSstEntry::reserved_table_name_for_inspection())
1226        );
1227        assert_eq!(scan.projection, None);
1228
1229        let fields = plan.schema().fields();
1230        assert_eq!(fields.len(), 2);
1231        assert_eq!(fields[0].name(), "file_path");
1232        assert_eq!(fields[1].name(), "last_modified_ms");
1233    }
1234
1235    // Helper to reach TableScan and detect presence of Filter/Limit in plan
1236    fn extract_scan(plan: &LogicalPlan) -> (&datafusion_expr::logical_plan::TableScan, bool, bool) {
1237        use datafusion_expr::logical_plan::Limit;
1238
1239        match plan {
1240            LogicalPlan::Filter(f) => {
1241                let (scan, _, has_limit) = extract_scan(&f.input);
1242                (scan, true, has_limit)
1243            }
1244            LogicalPlan::Limit(Limit { input, .. }) => {
1245                let (scan, has_filter, _) = extract_scan(input);
1246                (scan, has_filter, true)
1247            }
1248            LogicalPlan::Projection(projection) => extract_scan(&projection.input),
1249            LogicalPlan::TableScan(scan) => (scan, false, false),
1250            other => panic!("unexpected plan: {other:?}"),
1251        }
1252    }
1253}