Skip to main content

datafusion_cli/
functions.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Functions that are query-able and searchable via the `\h` command
19
20use datafusion_common::instant::Instant;
21use std::fmt;
22use std::fs::File;
23use std::str::FromStr;
24use std::sync::Arc;
25
26use arrow::array::{
27    DurationMillisecondArray, GenericListArray, Int64Array, StringArray, StructArray,
28    TimestampMillisecondArray, UInt64Array,
29};
30use arrow::buffer::{Buffer, OffsetBuffer, ScalarBuffer};
31use arrow::datatypes::{DataType, Field, Fields, Schema, SchemaRef, TimeUnit};
32use arrow::record_batch::RecordBatch;
33use arrow::util::pretty::pretty_format_batches;
34use datafusion::catalog::{Session, TableFunctionArgs, TableFunctionImpl};
35use datafusion::common::{Column, plan_err};
36use datafusion::datasource::TableProvider;
37use datafusion::datasource::memory::MemorySourceConfig;
38use datafusion::error::Result;
39use datafusion::execution::cache::cache_manager::CacheManager;
40use datafusion::logical_expr::Expr;
41use datafusion::physical_plan::ExecutionPlan;
42use datafusion::scalar::ScalarValue;
43
44use async_trait::async_trait;
45use parquet::basic::ConvertedType;
46use parquet::data_type::{ByteArray, FixedLenByteArray};
47use parquet::file::reader::FileReader;
48use parquet::file::serialized_reader::SerializedFileReader;
49use parquet::file::statistics::Statistics;
50
51#[derive(Debug)]
52pub enum Function {
53    Select,
54    Explain,
55    Show,
56    CreateTable,
57    CreateTableAs,
58    Insert,
59    DropTable,
60}
61
62const ALL_FUNCTIONS: [Function; 7] = [
63    Function::CreateTable,
64    Function::CreateTableAs,
65    Function::DropTable,
66    Function::Explain,
67    Function::Insert,
68    Function::Select,
69    Function::Show,
70];
71
72impl Function {
73    pub fn function_details(&self) -> Result<&str> {
74        let details = match self {
75            Function::Select => {
76                r#"
77Command:     SELECT
78Description: retrieve rows from a table or view
79Syntax:
80SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ]
81    [ * | expression [ [ AS ] output_name ] [, ...] ]
82    [ FROM from_item [, ...] ]
83    [ WHERE condition ]
84    [ GROUP BY [ ALL | DISTINCT ] grouping_element [, ...] ]
85    [ HAVING condition ]
86    [ WINDOW window_name AS ( window_definition ) [, ...] ]
87    [ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] select ]
88    [ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ]
89    [ LIMIT { count | ALL } ]
90    [ OFFSET start [ ROW | ROWS ] ]
91
92where from_item can be one of:
93
94    [ ONLY ] table_name [ * ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
95                [ TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ] ]
96    [ LATERAL ] ( select ) [ AS ] alias [ ( column_alias [, ...] ) ]
97    with_query_name [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
98    [ LATERAL ] function_name ( [ argument [, ...] ] )
99                [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
100    [ LATERAL ] function_name ( [ argument [, ...] ] ) [ AS ] alias ( column_definition [, ...] )
101    [ LATERAL ] function_name ( [ argument [, ...] ] ) AS ( column_definition [, ...] )
102    [ LATERAL ] ROWS FROM( function_name ( [ argument [, ...] ] ) [ AS ( column_definition [, ...] ) ] [, ...] )
103                [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
104    from_item [ NATURAL ] join_type from_item [ ON join_condition | USING ( join_column [, ...] ) [ AS join_using_alias ] ]
105
106and grouping_element can be one of:
107
108    ( )
109    expression
110    ( expression [, ...] )
111
112and with_query is:
113
114    with_query_name [ ( column_name [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( select | values | insert | update | delete )
115
116TABLE [ ONLY ] table_name [ * ]"#
117            }
118            Function::Explain => {
119                r#"
120Command:     EXPLAIN
121Description: show the execution plan of a statement
122Syntax:
123EXPLAIN [ ANALYZE ] statement
124"#
125            }
126            Function::Show => {
127                r#"
128Command:     SHOW
129Description: show the value of a run-time parameter
130Syntax:
131SHOW name
132"#
133            }
134            Function::CreateTable => {
135                r#"
136Command:     CREATE TABLE
137Description: define a new table
138Syntax:
139CREATE [ EXTERNAL ]  TABLE table_name ( [
140  { column_name data_type }
141    [, ... ]
142] )
143"#
144            }
145            Function::CreateTableAs => {
146                r#"
147Command:     CREATE TABLE AS
148Description: define a new table from the results of a query
149Syntax:
150CREATE TABLE table_name
151    [ (column_name [, ...] ) ]
152    AS query
153    [ WITH [ NO ] DATA ]
154"#
155            }
156            Function::Insert => {
157                r#"
158Command:     INSERT
159Description: create new rows in a table
160Syntax:
161INSERT INTO table_name [ ( column_name [, ...] ) ]
162    { VALUES ( { expression } [, ...] ) [, ...] }
163"#
164            }
165            Function::DropTable => {
166                r#"
167Command:     DROP TABLE
168Description: remove a table
169Syntax:
170DROP TABLE [ IF EXISTS ] name [, ...]
171"#
172            }
173        };
174        Ok(details)
175    }
176}
177
178impl FromStr for Function {
179    type Err = ();
180
181    fn from_str(s: &str) -> Result<Self, Self::Err> {
182        Ok(match s.trim().to_uppercase().as_str() {
183            "SELECT" => Self::Select,
184            "EXPLAIN" => Self::Explain,
185            "SHOW" => Self::Show,
186            "CREATE TABLE" => Self::CreateTable,
187            "CREATE TABLE AS" => Self::CreateTableAs,
188            "INSERT" => Self::Insert,
189            "DROP TABLE" => Self::DropTable,
190            _ => return Err(()),
191        })
192    }
193}
194
195impl fmt::Display for Function {
196    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197        match *self {
198            Function::Select => write!(f, "SELECT"),
199            Function::Explain => write!(f, "EXPLAIN"),
200            Function::Show => write!(f, "SHOW"),
201            Function::CreateTable => write!(f, "CREATE TABLE"),
202            Function::CreateTableAs => write!(f, "CREATE TABLE AS"),
203            Function::Insert => write!(f, "INSERT"),
204            Function::DropTable => write!(f, "DROP TABLE"),
205        }
206    }
207}
208
209pub fn display_all_functions() -> Result<()> {
210    println!("Available help:");
211    let array = StringArray::from(
212        ALL_FUNCTIONS
213            .iter()
214            .map(|f| format!("{f}"))
215            .collect::<Vec<String>>(),
216    );
217    let schema = Schema::new(vec![Field::new("Function", DataType::Utf8, false)]);
218    let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(array)])?;
219    println!("{}", pretty_format_batches(&[batch]).unwrap());
220    Ok(())
221}
222
223/// PARQUET_META table function
224#[derive(Debug)]
225struct ParquetMetadataTable {
226    schema: SchemaRef,
227    batch: RecordBatch,
228}
229
230#[async_trait]
231impl TableProvider for ParquetMetadataTable {
232    fn schema(&self) -> SchemaRef {
233        self.schema.clone()
234    }
235
236    fn table_type(&self) -> datafusion::logical_expr::TableType {
237        datafusion::logical_expr::TableType::Base
238    }
239
240    async fn scan(
241        &self,
242        _state: &dyn Session,
243        projection: Option<&Vec<usize>>,
244        _filters: &[Expr],
245        _limit: Option<usize>,
246    ) -> Result<Arc<dyn ExecutionPlan>> {
247        Ok(MemorySourceConfig::try_new_exec(
248            &[vec![self.batch.clone()]],
249            TableProvider::schema(self),
250            projection.cloned(),
251        )?)
252    }
253}
254
255fn convert_parquet_statistics(
256    value: &Statistics,
257    converted_type: ConvertedType,
258) -> (Option<String>, Option<String>) {
259    match (value, converted_type) {
260        (Statistics::Boolean(val), _) => (
261            val.min_opt().map(|v| v.to_string()),
262            val.max_opt().map(|v| v.to_string()),
263        ),
264        (Statistics::Int32(val), _) => (
265            val.min_opt().map(|v| v.to_string()),
266            val.max_opt().map(|v| v.to_string()),
267        ),
268        (Statistics::Int64(val), _) => (
269            val.min_opt().map(|v| v.to_string()),
270            val.max_opt().map(|v| v.to_string()),
271        ),
272        (Statistics::Int96(val), _) => (
273            val.min_opt().map(|v| v.to_string()),
274            val.max_opt().map(|v| v.to_string()),
275        ),
276        (Statistics::Float(val), _) => (
277            val.min_opt().map(|v| v.to_string()),
278            val.max_opt().map(|v| v.to_string()),
279        ),
280        (Statistics::Double(val), _) => (
281            val.min_opt().map(|v| v.to_string()),
282            val.max_opt().map(|v| v.to_string()),
283        ),
284        (Statistics::ByteArray(val), ConvertedType::UTF8) => (
285            byte_array_to_string(val.min_opt()),
286            byte_array_to_string(val.max_opt()),
287        ),
288        (Statistics::ByteArray(val), _) => (
289            val.min_opt().map(|v| v.to_string()),
290            val.max_opt().map(|v| v.to_string()),
291        ),
292        (Statistics::FixedLenByteArray(val), ConvertedType::UTF8) => (
293            fixed_len_byte_array_to_string(val.min_opt()),
294            fixed_len_byte_array_to_string(val.max_opt()),
295        ),
296        (Statistics::FixedLenByteArray(val), _) => (
297            val.min_opt().map(|v| v.to_string()),
298            val.max_opt().map(|v| v.to_string()),
299        ),
300    }
301}
302
303/// Convert to a string if it has utf8 encoding, otherwise print bytes directly
304fn byte_array_to_string(val: Option<&ByteArray>) -> Option<String> {
305    val.map(|v| {
306        v.as_utf8()
307            .map(|s| s.to_string())
308            .unwrap_or_else(|_e| v.to_string())
309    })
310}
311
312/// Convert to a string if it has utf8 encoding, otherwise print bytes directly
313fn fixed_len_byte_array_to_string(val: Option<&FixedLenByteArray>) -> Option<String> {
314    val.map(|v| {
315        v.as_utf8()
316            .map(|s| s.to_string())
317            .unwrap_or_else(|_e| v.to_string())
318    })
319}
320
321#[derive(Debug)]
322pub struct ParquetMetadataFunc {}
323
324impl TableFunctionImpl for ParquetMetadataFunc {
325    fn call_with_args(&self, args: TableFunctionArgs) -> Result<Arc<dyn TableProvider>> {
326        let exprs = args.exprs();
327        let filename = match exprs.first() {
328            Some(Expr::Literal(ScalarValue::Utf8(Some(s)), _)) => s, // single quote: parquet_metadata('x.parquet')
329            Some(Expr::Column(Column { name, .. })) => name, // double quote: parquet_metadata("x.parquet")
330            _ => {
331                return plan_err!(
332                    "parquet_metadata requires string argument as its input"
333                );
334            }
335        };
336
337        let file = File::open(filename.clone())?;
338        let reader = SerializedFileReader::new(file)?;
339        let metadata = reader.metadata();
340
341        let schema = Arc::new(Schema::new(vec![
342            Field::new("filename", DataType::Utf8, true),
343            Field::new("row_group_id", DataType::Int64, true),
344            Field::new("row_group_num_rows", DataType::Int64, true),
345            Field::new("row_group_num_columns", DataType::Int64, true),
346            Field::new("row_group_bytes", DataType::Int64, true),
347            Field::new("column_id", DataType::Int64, true),
348            Field::new("file_offset", DataType::Int64, true),
349            Field::new("num_values", DataType::Int64, true),
350            Field::new("path_in_schema", DataType::Utf8, true),
351            Field::new("type", DataType::Utf8, true),
352            Field::new("stats_min", DataType::Utf8, true),
353            Field::new("stats_max", DataType::Utf8, true),
354            Field::new("stats_null_count", DataType::Int64, true),
355            Field::new("stats_distinct_count", DataType::Int64, true),
356            Field::new("stats_min_value", DataType::Utf8, true),
357            Field::new("stats_max_value", DataType::Utf8, true),
358            Field::new("compression", DataType::Utf8, true),
359            Field::new("encodings", DataType::Utf8, true),
360            Field::new("index_page_offset", DataType::Int64, true),
361            Field::new("dictionary_page_offset", DataType::Int64, true),
362            Field::new("data_page_offset", DataType::Int64, true),
363            Field::new("total_compressed_size", DataType::Int64, true),
364            Field::new("total_uncompressed_size", DataType::Int64, true),
365        ]));
366
367        // construct record batch from metadata
368        let mut filename_arr = vec![];
369        let mut row_group_id_arr = vec![];
370        let mut row_group_num_rows_arr = vec![];
371        let mut row_group_num_columns_arr = vec![];
372        let mut row_group_bytes_arr = vec![];
373        let mut column_id_arr = vec![];
374        let mut file_offset_arr = vec![];
375        let mut num_values_arr = vec![];
376        let mut path_in_schema_arr = vec![];
377        let mut type_arr = vec![];
378        let mut stats_min_arr = vec![];
379        let mut stats_max_arr = vec![];
380        let mut stats_null_count_arr = vec![];
381        let mut stats_distinct_count_arr = vec![];
382        let mut stats_min_value_arr = vec![];
383        let mut stats_max_value_arr = vec![];
384        let mut compression_arr = vec![];
385        let mut encodings_arr = vec![];
386        let mut index_page_offset_arr = vec![];
387        let mut dictionary_page_offset_arr = vec![];
388        let mut data_page_offset_arr = vec![];
389        let mut total_compressed_size_arr = vec![];
390        let mut total_uncompressed_size_arr = vec![];
391        for (rg_idx, row_group) in metadata.row_groups().iter().enumerate() {
392            for (col_idx, column) in row_group.columns().iter().enumerate() {
393                filename_arr.push(filename.clone());
394                row_group_id_arr.push(rg_idx as i64);
395                row_group_num_rows_arr.push(row_group.num_rows());
396                row_group_num_columns_arr.push(row_group.num_columns() as i64);
397                row_group_bytes_arr.push(row_group.total_byte_size());
398                column_id_arr.push(col_idx as i64);
399                file_offset_arr.push(column.file_offset());
400                num_values_arr.push(column.num_values());
401                path_in_schema_arr.push(column.column_path().to_string());
402                type_arr.push(column.column_type().to_string());
403                let converted_type = column.column_descr().converted_type();
404
405                if let Some(s) = column.statistics() {
406                    let (min_val, max_val) =
407                        convert_parquet_statistics(s, converted_type);
408                    stats_min_arr.push(min_val.clone());
409                    stats_max_arr.push(max_val.clone());
410                    stats_null_count_arr.push(s.null_count_opt().map(|c| c as i64));
411                    stats_distinct_count_arr
412                        .push(s.distinct_count_opt().map(|c| c as i64));
413                    stats_min_value_arr.push(min_val);
414                    stats_max_value_arr.push(max_val);
415                } else {
416                    stats_min_arr.push(None);
417                    stats_max_arr.push(None);
418                    stats_null_count_arr.push(None);
419                    stats_distinct_count_arr.push(None);
420                    stats_min_value_arr.push(None);
421                    stats_max_value_arr.push(None);
422                };
423                compression_arr.push(format!("{:?}", column.compression()));
424                // need to collect into Vec to format
425                let encodings: Vec<_> = column.encodings().collect();
426                encodings_arr.push(format!("{encodings:?}"));
427                index_page_offset_arr.push(column.index_page_offset());
428                dictionary_page_offset_arr.push(column.dictionary_page_offset());
429                data_page_offset_arr.push(column.data_page_offset());
430                total_compressed_size_arr.push(column.compressed_size());
431                total_uncompressed_size_arr.push(column.uncompressed_size());
432            }
433        }
434
435        let rb = RecordBatch::try_new(
436            schema.clone(),
437            vec![
438                Arc::new(StringArray::from(filename_arr)),
439                Arc::new(Int64Array::from(row_group_id_arr)),
440                Arc::new(Int64Array::from(row_group_num_rows_arr)),
441                Arc::new(Int64Array::from(row_group_num_columns_arr)),
442                Arc::new(Int64Array::from(row_group_bytes_arr)),
443                Arc::new(Int64Array::from(column_id_arr)),
444                Arc::new(Int64Array::from(file_offset_arr)),
445                Arc::new(Int64Array::from(num_values_arr)),
446                Arc::new(StringArray::from(path_in_schema_arr)),
447                Arc::new(StringArray::from(type_arr)),
448                Arc::new(StringArray::from(stats_min_arr)),
449                Arc::new(StringArray::from(stats_max_arr)),
450                Arc::new(Int64Array::from(stats_null_count_arr)),
451                Arc::new(Int64Array::from(stats_distinct_count_arr)),
452                Arc::new(StringArray::from(stats_min_value_arr)),
453                Arc::new(StringArray::from(stats_max_value_arr)),
454                Arc::new(StringArray::from(compression_arr)),
455                Arc::new(StringArray::from(encodings_arr)),
456                Arc::new(Int64Array::from(index_page_offset_arr)),
457                Arc::new(Int64Array::from(dictionary_page_offset_arr)),
458                Arc::new(Int64Array::from(data_page_offset_arr)),
459                Arc::new(Int64Array::from(total_compressed_size_arr)),
460                Arc::new(Int64Array::from(total_uncompressed_size_arr)),
461            ],
462        )?;
463
464        let parquet_metadata = ParquetMetadataTable { schema, batch: rb };
465        Ok(Arc::new(parquet_metadata))
466    }
467}
468
469/// METADATA_CACHE table function
470#[derive(Debug)]
471struct MetadataCacheTable {
472    schema: SchemaRef,
473    batch: RecordBatch,
474}
475
476#[async_trait]
477impl TableProvider for MetadataCacheTable {
478    fn schema(&self) -> SchemaRef {
479        self.schema.clone()
480    }
481
482    fn table_type(&self) -> datafusion::logical_expr::TableType {
483        datafusion::logical_expr::TableType::Base
484    }
485
486    async fn scan(
487        &self,
488        _state: &dyn Session,
489        projection: Option<&Vec<usize>>,
490        _filters: &[Expr],
491        _limit: Option<usize>,
492    ) -> Result<Arc<dyn ExecutionPlan>> {
493        Ok(MemorySourceConfig::try_new_exec(
494            &[vec![self.batch.clone()]],
495            TableProvider::schema(self),
496            projection.cloned(),
497        )?)
498    }
499}
500
501#[derive(Debug)]
502pub struct MetadataCacheFunc {
503    cache_manager: Arc<CacheManager>,
504}
505
506impl MetadataCacheFunc {
507    pub fn new(cache_manager: Arc<CacheManager>) -> Self {
508        Self { cache_manager }
509    }
510}
511
512impl TableFunctionImpl for MetadataCacheFunc {
513    fn call_with_args(&self, args: TableFunctionArgs) -> Result<Arc<dyn TableProvider>> {
514        let exprs = args.exprs();
515        if !exprs.is_empty() {
516            return plan_err!("metadata_cache should have no arguments");
517        }
518
519        let schema = Arc::new(Schema::new(vec![
520            Field::new("path", DataType::Utf8, false),
521            Field::new(
522                "file_modified",
523                DataType::Timestamp(TimeUnit::Millisecond, None),
524                false,
525            ),
526            Field::new("file_size_bytes", DataType::UInt64, false),
527            Field::new("e_tag", DataType::Utf8, true),
528            Field::new("version", DataType::Utf8, true),
529            Field::new("metadata_size_bytes", DataType::UInt64, false),
530            Field::new("hits", DataType::UInt64, false),
531            Field::new("extra", DataType::Utf8, true),
532        ]));
533
534        // construct record batch from metadata
535        let mut path_arr = vec![];
536        let mut file_modified_arr = vec![];
537        let mut file_size_bytes_arr = vec![];
538        let mut e_tag_arr = vec![];
539        let mut version_arr = vec![];
540        let mut metadata_size_bytes = vec![];
541        let mut hits_arr = vec![];
542        let mut extra_arr = vec![];
543
544        let cached_entries = self.cache_manager.get_file_metadata_cache().list_entries();
545
546        for (path, entry) in cached_entries {
547            path_arr.push(path.to_string());
548            file_modified_arr
549                .push(Some(entry.object_meta.last_modified.timestamp_millis()));
550            file_size_bytes_arr.push(entry.object_meta.size);
551            e_tag_arr.push(entry.object_meta.e_tag);
552            version_arr.push(entry.object_meta.version);
553            metadata_size_bytes.push(entry.size_bytes as u64);
554            hits_arr.push(entry.hits as u64);
555
556            let mut extra = entry
557                .extra
558                .iter()
559                .map(|(k, v)| format!("{k}={v}"))
560                .collect::<Vec<_>>();
561            extra.sort();
562            extra_arr.push(extra.join(" "));
563        }
564
565        let batch = RecordBatch::try_new(
566            schema.clone(),
567            vec![
568                Arc::new(StringArray::from(path_arr)),
569                Arc::new(TimestampMillisecondArray::from(file_modified_arr)),
570                Arc::new(UInt64Array::from(file_size_bytes_arr)),
571                Arc::new(StringArray::from(e_tag_arr)),
572                Arc::new(StringArray::from(version_arr)),
573                Arc::new(UInt64Array::from(metadata_size_bytes)),
574                Arc::new(UInt64Array::from(hits_arr)),
575                Arc::new(StringArray::from(extra_arr)),
576            ],
577        )?;
578
579        let metadata_cache = MetadataCacheTable { schema, batch };
580        Ok(Arc::new(metadata_cache))
581    }
582}
583
584/// STATISTICS_CACHE table function
585#[derive(Debug)]
586struct StatisticsCacheTable {
587    schema: SchemaRef,
588    batch: RecordBatch,
589}
590
591#[async_trait]
592impl TableProvider for StatisticsCacheTable {
593    fn schema(&self) -> SchemaRef {
594        self.schema.clone()
595    }
596
597    fn table_type(&self) -> datafusion::logical_expr::TableType {
598        datafusion::logical_expr::TableType::Base
599    }
600
601    async fn scan(
602        &self,
603        _state: &dyn Session,
604        projection: Option<&Vec<usize>>,
605        _filters: &[Expr],
606        _limit: Option<usize>,
607    ) -> Result<Arc<dyn ExecutionPlan>> {
608        Ok(MemorySourceConfig::try_new_exec(
609            &[vec![self.batch.clone()]],
610            TableProvider::schema(self),
611            projection.cloned(),
612        )?)
613    }
614}
615
616#[derive(Debug)]
617pub struct StatisticsCacheFunc {
618    cache_manager: Arc<CacheManager>,
619}
620
621impl StatisticsCacheFunc {
622    pub fn new(cache_manager: Arc<CacheManager>) -> Self {
623        Self { cache_manager }
624    }
625}
626
627impl TableFunctionImpl for StatisticsCacheFunc {
628    fn call_with_args(&self, args: TableFunctionArgs) -> Result<Arc<dyn TableProvider>> {
629        let exprs = args.exprs();
630        if !exprs.is_empty() {
631            return plan_err!("statistics_cache should have no arguments");
632        }
633
634        let schema = Arc::new(Schema::new(vec![
635            Field::new("path", DataType::Utf8, false),
636            Field::new("table", DataType::Utf8, false),
637            Field::new(
638                "file_modified",
639                DataType::Timestamp(TimeUnit::Millisecond, None),
640                false,
641            ),
642            Field::new("file_size_bytes", DataType::UInt64, false),
643            Field::new("e_tag", DataType::Utf8, true),
644            Field::new("version", DataType::Utf8, true),
645            Field::new("num_rows", DataType::Utf8, false),
646            Field::new("num_columns", DataType::UInt64, false),
647            Field::new("table_size_bytes", DataType::Utf8, false),
648            Field::new("statistics_size_bytes", DataType::UInt64, false),
649        ]));
650
651        // construct record batch from metadata
652        let mut path_arr = vec![];
653        let mut table_arr = vec![];
654        let mut file_modified_arr = vec![];
655        let mut file_size_bytes_arr = vec![];
656        let mut e_tag_arr = vec![];
657        let mut version_arr = vec![];
658        let mut num_rows_arr = vec![];
659        let mut num_columns_arr = vec![];
660        let mut table_size_bytes_arr = vec![];
661        let mut statistics_size_bytes_arr = vec![];
662
663        if let Some(file_statistics_cache) = self.cache_manager.get_file_statistic_cache()
664        {
665            for (path, entry) in file_statistics_cache.list_entries() {
666                path_arr.push(path.path.to_string());
667                table_arr
668                    .push(path.table.map_or_else(|| "".to_string(), |t| t.to_string()));
669                file_modified_arr
670                    .push(Some(entry.object_meta.last_modified.timestamp_millis()));
671                file_size_bytes_arr.push(entry.object_meta.size);
672                e_tag_arr.push(entry.object_meta.e_tag);
673                version_arr.push(entry.object_meta.version);
674                num_rows_arr.push(entry.num_rows.to_string());
675                num_columns_arr.push(entry.num_columns as u64);
676                table_size_bytes_arr.push(entry.table_size_bytes.to_string());
677                statistics_size_bytes_arr.push(entry.statistics_size_bytes as u64);
678            }
679        }
680
681        let batch = RecordBatch::try_new(
682            schema.clone(),
683            vec![
684                Arc::new(StringArray::from(path_arr)),
685                Arc::new(StringArray::from(table_arr)),
686                Arc::new(TimestampMillisecondArray::from(file_modified_arr)),
687                Arc::new(UInt64Array::from(file_size_bytes_arr)),
688                Arc::new(StringArray::from(e_tag_arr)),
689                Arc::new(StringArray::from(version_arr)),
690                Arc::new(StringArray::from(num_rows_arr)),
691                Arc::new(UInt64Array::from(num_columns_arr)),
692                Arc::new(StringArray::from(table_size_bytes_arr)),
693                Arc::new(UInt64Array::from(statistics_size_bytes_arr)),
694            ],
695        )?;
696
697        let statistics_cache = StatisticsCacheTable { schema, batch };
698        Ok(Arc::new(statistics_cache))
699    }
700}
701
702/// Implementation of the `list_files_cache` table function in datafusion-cli.
703///
704/// This function returns the cached results of running a LIST command on a
705/// particular object store path for a table. The object metadata is returned as
706/// a List of Structs, with one Struct for each object. DataFusion uses these
707/// cached results to plan queries against external tables.
708///
709/// # Schema
710/// ```sql
711/// > describe select * from list_files_cache();
712/// +---------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+
713/// | column_name         | data_type                                                                                                                                                                | is_nullable |
714/// +---------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+
715/// | table               | Utf8                                                                                                                                                                     | NO          |
716/// | path                | Utf8                                                                                                                                                                     | NO          |
717/// | metadata_size_bytes | UInt64                                                                                                                                                                   | NO          |
718/// | expires_in          | Duration(ms)                                                                                                                                                             | YES         |
719/// | metadata_list       | List(Struct("file_path": non-null Utf8, "file_modified": non-null Timestamp(ms), "file_size_bytes": non-null UInt64, "e_tag": Utf8, "version": Utf8), field: 'metadata') | YES         |
720/// +---------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+
721/// ```
722#[derive(Debug)]
723struct ListFilesCacheTable {
724    schema: SchemaRef,
725    batch: RecordBatch,
726}
727
728#[async_trait]
729impl TableProvider for ListFilesCacheTable {
730    fn schema(&self) -> SchemaRef {
731        self.schema.clone()
732    }
733
734    fn table_type(&self) -> datafusion::logical_expr::TableType {
735        datafusion::logical_expr::TableType::Base
736    }
737
738    async fn scan(
739        &self,
740        _state: &dyn Session,
741        projection: Option<&Vec<usize>>,
742        _filters: &[Expr],
743        _limit: Option<usize>,
744    ) -> Result<Arc<dyn ExecutionPlan>> {
745        Ok(MemorySourceConfig::try_new_exec(
746            &[vec![self.batch.clone()]],
747            TableProvider::schema(self),
748            projection.cloned(),
749        )?)
750    }
751}
752
753#[derive(Debug)]
754pub struct ListFilesCacheFunc {
755    cache_manager: Arc<CacheManager>,
756}
757
758impl ListFilesCacheFunc {
759    pub fn new(cache_manager: Arc<CacheManager>) -> Self {
760        Self { cache_manager }
761    }
762}
763
764impl TableFunctionImpl for ListFilesCacheFunc {
765    fn call_with_args(&self, args: TableFunctionArgs) -> Result<Arc<dyn TableProvider>> {
766        let exprs = args.exprs();
767        if !exprs.is_empty() {
768            return plan_err!("list_files_cache should have no arguments");
769        }
770
771        let nested_fields = Fields::from(vec![
772            Field::new("file_path", DataType::Utf8, false),
773            Field::new(
774                "file_modified",
775                DataType::Timestamp(TimeUnit::Millisecond, None),
776                false,
777            ),
778            Field::new("file_size_bytes", DataType::UInt64, false),
779            Field::new("e_tag", DataType::Utf8, true),
780            Field::new("version", DataType::Utf8, true),
781        ]);
782
783        let metadata_field =
784            Field::new("metadata", DataType::Struct(nested_fields.clone()), true);
785
786        let schema = Arc::new(Schema::new(vec![
787            Field::new("table", DataType::Utf8, true),
788            Field::new("path", DataType::Utf8, false),
789            Field::new("metadata_size_bytes", DataType::UInt64, false),
790            // expires field in ListFilesEntry has type Instant when set, from which we cannot get "the number of seconds", hence using Duration instead of Timestamp as data type.
791            Field::new(
792                "expires_in",
793                DataType::Duration(TimeUnit::Millisecond),
794                true,
795            ),
796            Field::new(
797                "metadata_list",
798                DataType::List(Arc::new(metadata_field.clone())),
799                true,
800            ),
801        ]));
802
803        let mut table_arr = vec![];
804        let mut path_arr = vec![];
805        let mut metadata_size_bytes_arr = vec![];
806        let mut expires_arr = vec![];
807
808        let mut file_path_arr = vec![];
809        let mut file_modified_arr = vec![];
810        let mut file_size_bytes_arr = vec![];
811        let mut etag_arr = vec![];
812        let mut version_arr = vec![];
813        let mut offsets: Vec<i32> = vec![0];
814
815        if let Some(list_files_cache) = self.cache_manager.get_list_files_cache() {
816            let now = Instant::now();
817            let mut current_offset: i32 = 0;
818
819            for (path, entry) in list_files_cache.list_entries() {
820                table_arr.push(path.table.map(|t| t.to_string()));
821                path_arr.push(path.path.to_string());
822                metadata_size_bytes_arr.push(entry.size_bytes as u64);
823                // calculates time left before entry expires
824                expires_arr.push(
825                    entry
826                        .expires
827                        .map(|t| t.duration_since(now).as_millis() as i64),
828                );
829
830                for meta in entry.metas.files.iter() {
831                    file_path_arr.push(meta.location.to_string());
832                    file_modified_arr.push(meta.last_modified.timestamp_millis());
833                    file_size_bytes_arr.push(meta.size);
834                    etag_arr.push(meta.e_tag.clone());
835                    version_arr.push(meta.version.clone());
836                }
837                current_offset += entry.metas.files.len() as i32;
838                offsets.push(current_offset);
839            }
840        }
841
842        let struct_arr = StructArray::new(
843            nested_fields,
844            vec![
845                Arc::new(StringArray::from(file_path_arr)),
846                Arc::new(TimestampMillisecondArray::from(file_modified_arr)),
847                Arc::new(UInt64Array::from(file_size_bytes_arr)),
848                Arc::new(StringArray::from(etag_arr)),
849                Arc::new(StringArray::from(version_arr)),
850            ],
851            None,
852        );
853
854        let offsets_buffer: OffsetBuffer<i32> =
855            OffsetBuffer::new(ScalarBuffer::from(Buffer::from_vec(offsets)));
856
857        let batch = RecordBatch::try_new(
858            schema.clone(),
859            vec![
860                Arc::new(StringArray::from(table_arr)),
861                Arc::new(StringArray::from(path_arr)),
862                Arc::new(UInt64Array::from(metadata_size_bytes_arr)),
863                Arc::new(DurationMillisecondArray::from(expires_arr)),
864                Arc::new(GenericListArray::new(
865                    Arc::new(metadata_field),
866                    offsets_buffer,
867                    Arc::new(struct_arr),
868                    None,
869                )),
870            ],
871        )?;
872
873        let list_files_cache = ListFilesCacheTable { schema, batch };
874        Ok(Arc::new(list_files_cache))
875    }
876}