Skip to content

Commit d9597f7

Browse files
authored
feat(datafusion): Add $table_indexes system table (#327)
1 parent df6074a commit d9597f7

3 files changed

Lines changed: 491 additions & 2 deletions

File tree

crates/integrations/datafusion/src/system_tables/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mod referenced_files_size;
3838
mod row_string_cast;
3939
mod schemas;
4040
mod snapshots;
41+
mod table_indexes;
4142
mod tags;
4243

4344
type Builder = fn(Table) -> DFResult<Arc<dyn TableProvider>>;
@@ -53,6 +54,7 @@ const TABLES: &[(&str, Builder)] = &[
5354
("referenced_files_size", referenced_files_size::build),
5455
("schemas", schemas::build),
5556
("snapshots", snapshots::build),
57+
("table_indexes", table_indexes::build),
5658
("tags", tags::build),
5759
];
5860

@@ -65,6 +67,7 @@ const SYSTEM_TABLE_NAMES: &[&str] = &[
6567
"referenced_files_size",
6668
"schemas",
6769
"snapshots",
70+
"table_indexes",
6871
"tags",
6972
];
7073

@@ -190,6 +193,9 @@ mod tests {
190193
assert!(is_registered("manifests"));
191194
assert!(is_registered("Manifests"));
192195
assert!(is_registered("MANIFESTS"));
196+
assert!(is_registered("table_indexes"));
197+
assert!(is_registered("Table_Indexes"));
198+
assert!(is_registered("TABLE_INDEXES"));
193199
assert!(is_registered("partitions"));
194200
assert!(is_registered("Partitions"));
195201
assert!(is_registered("PARTITIONS"));
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
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+
//! Mirrors Java [TableIndexesTable](https://github.com/apache/paimon/blob/master/paimon-core/src/main/java/org/apache/paimon/table/system/TableIndexesTable.java).
19+
20+
use std::any::Any;
21+
use std::sync::{Arc, OnceLock};
22+
23+
use async_trait::async_trait;
24+
use datafusion::arrow::array::builder::{
25+
ArrayBuilder, Int32Builder, Int64Builder, ListBuilder, StringBuilder, StructBuilder,
26+
};
27+
use datafusion::arrow::array::{ArrayRef, Int32Array, Int64Array, RecordBatch, StringArray};
28+
use datafusion::arrow::datatypes::{DataType, Field, Fields, Schema, SchemaRef};
29+
use datafusion::catalog::Session;
30+
use datafusion::datasource::memory::MemorySourceConfig;
31+
use datafusion::datasource::{TableProvider, TableType};
32+
use datafusion::error::Result as DFResult;
33+
use datafusion::logical_expr::Expr;
34+
use datafusion::physical_plan::ExecutionPlan;
35+
use paimon::spec::{
36+
BinaryRow, DataField, DeletionVectorMeta, FileKind, IndexManifest, IndexManifestEntry,
37+
};
38+
use paimon::table::{SnapshotManager, Table};
39+
40+
use super::row_string_cast::format_row_as_java_cast_string;
41+
use crate::error::to_datafusion_error;
42+
43+
pub(super) fn build(table: Table) -> DFResult<Arc<dyn TableProvider>> {
44+
Ok(Arc::new(TableIndexesTable { table }))
45+
}
46+
47+
fn table_indexes_schema() -> SchemaRef {
48+
static SCHEMA: OnceLock<SchemaRef> = OnceLock::new();
49+
SCHEMA
50+
.get_or_init(|| {
51+
Arc::new(Schema::new(vec![
52+
Field::new("partition", DataType::Utf8, true),
53+
Field::new("bucket", DataType::Int32, false),
54+
Field::new("index_type", DataType::Utf8, false),
55+
Field::new("file_name", DataType::Utf8, false),
56+
Field::new("file_size", DataType::Int64, false),
57+
Field::new("row_count", DataType::Int64, false),
58+
Field::new("dv_ranges", dv_ranges_data_type(), true),
59+
Field::new("row_range_start", DataType::Int64, true),
60+
Field::new("row_range_end", DataType::Int64, true),
61+
Field::new("index_field_id", DataType::Int32, true),
62+
Field::new("index_field_name", DataType::Utf8, true),
63+
]))
64+
})
65+
.clone()
66+
}
67+
68+
fn dv_ranges_data_type() -> DataType {
69+
DataType::List(Arc::new(Field::new(
70+
"item",
71+
DataType::Struct(dv_meta_fields()),
72+
true,
73+
)))
74+
}
75+
76+
fn dv_meta_fields() -> Fields {
77+
vec![
78+
Arc::new(Field::new("f0", DataType::Utf8, false)),
79+
Arc::new(Field::new("f1", DataType::Int32, false)),
80+
Arc::new(Field::new("f2", DataType::Int32, false)),
81+
Arc::new(Field::new("_CARDINALITY", DataType::Int64, true)),
82+
]
83+
.into()
84+
}
85+
86+
#[derive(Debug)]
87+
struct TableIndexesTable {
88+
table: Table,
89+
}
90+
91+
#[async_trait]
92+
impl TableProvider for TableIndexesTable {
93+
fn as_any(&self) -> &dyn Any {
94+
self
95+
}
96+
97+
fn schema(&self) -> SchemaRef {
98+
table_indexes_schema()
99+
}
100+
101+
fn table_type(&self) -> TableType {
102+
TableType::View
103+
}
104+
105+
async fn scan(
106+
&self,
107+
_state: &dyn Session,
108+
projection: Option<&Vec<usize>>,
109+
_filters: &[Expr],
110+
_limit: Option<usize>,
111+
) -> DFResult<Arc<dyn ExecutionPlan>> {
112+
let table = self.table.clone();
113+
let entries =
114+
crate::runtime::await_with_runtime(async move { collect_index_entries(&table).await })
115+
.await
116+
.map_err(to_datafusion_error)?;
117+
118+
let partition_fields = self.table.schema().partition_fields();
119+
let fields = self.table.schema().fields();
120+
let n = entries.len();
121+
let mut partitions: Vec<Option<String>> = Vec::with_capacity(n);
122+
let mut buckets = Vec::with_capacity(n);
123+
let mut index_types = Vec::with_capacity(n);
124+
let mut file_names = Vec::with_capacity(n);
125+
let mut file_sizes = Vec::with_capacity(n);
126+
let mut row_counts = Vec::with_capacity(n);
127+
let mut dv_ranges = dv_ranges_builder();
128+
let mut row_range_starts: Vec<Option<i64>> = Vec::with_capacity(n);
129+
let mut row_range_ends: Vec<Option<i64>> = Vec::with_capacity(n);
130+
let mut index_field_ids: Vec<Option<i32>> = Vec::with_capacity(n);
131+
let mut index_field_names: Vec<Option<String>> = Vec::with_capacity(n);
132+
133+
for entry in &entries {
134+
let index_file = &entry.index_file;
135+
partitions.push(Some(format_partition(&entry.partition, &partition_fields)?));
136+
buckets.push(entry.bucket);
137+
index_types.push(index_file.index_type.as_str());
138+
file_names.push(index_file.file_name.as_str());
139+
file_sizes.push(i64::from(index_file.file_size));
140+
row_counts.push(i64::from(index_file.row_count));
141+
append_dv_ranges(
142+
&mut dv_ranges,
143+
index_file
144+
.deletion_vectors_ranges
145+
.as_ref()
146+
.map(|ranges| ranges.iter()),
147+
);
148+
149+
if let Some(global_meta) = &index_file.global_index_meta {
150+
row_range_starts.push(Some(global_meta.row_range_start));
151+
row_range_ends.push(Some(global_meta.row_range_end));
152+
index_field_ids.push(Some(global_meta.index_field_id));
153+
index_field_names.push(
154+
fields
155+
.iter()
156+
.find(|field| field.id() == global_meta.index_field_id)
157+
.map(|field| field.name().to_string()),
158+
);
159+
} else {
160+
row_range_starts.push(None);
161+
row_range_ends.push(None);
162+
index_field_ids.push(None);
163+
index_field_names.push(None);
164+
}
165+
}
166+
167+
let schema = table_indexes_schema();
168+
let batch = RecordBatch::try_new(
169+
schema.clone(),
170+
vec![
171+
Arc::new(StringArray::from(partitions)),
172+
Arc::new(Int32Array::from(buckets)),
173+
Arc::new(StringArray::from(index_types)),
174+
Arc::new(StringArray::from(file_names)),
175+
Arc::new(Int64Array::from(file_sizes)),
176+
Arc::new(Int64Array::from(row_counts)),
177+
Arc::new(dv_ranges.finish()) as ArrayRef,
178+
Arc::new(Int64Array::from(row_range_starts)),
179+
Arc::new(Int64Array::from(row_range_ends)),
180+
Arc::new(Int32Array::from(index_field_ids)),
181+
Arc::new(StringArray::from(index_field_names)),
182+
],
183+
)?;
184+
185+
Ok(MemorySourceConfig::try_new_exec(
186+
&[vec![batch]],
187+
schema,
188+
projection.cloned(),
189+
)?)
190+
}
191+
}
192+
193+
async fn collect_index_entries(table: &Table) -> paimon::Result<Vec<IndexManifestEntry>> {
194+
let file_io = table.file_io();
195+
let sm = SnapshotManager::new(file_io.clone(), table.location().to_string());
196+
let snapshot = match sm.get_latest_snapshot().await? {
197+
Some(s) => s,
198+
None => return Ok(Vec::new()),
199+
};
200+
let Some(index_manifest_name) = snapshot.index_manifest() else {
201+
return Ok(Vec::new());
202+
};
203+
204+
let path = sm.manifest_path(index_manifest_name);
205+
if !file_io.exists(&path).await? {
206+
return Ok(Vec::new());
207+
}
208+
209+
let entries = IndexManifest::read(file_io, &path).await?;
210+
Ok(visible_index_entries(entries))
211+
}
212+
213+
fn visible_index_entries(entries: Vec<IndexManifestEntry>) -> Vec<IndexManifestEntry> {
214+
entries
215+
.into_iter()
216+
.filter(|entry| entry.kind == FileKind::Add)
217+
.collect()
218+
}
219+
220+
fn format_partition(partition: &[u8], partition_fields: &[DataField]) -> DFResult<String> {
221+
let row = BinaryRow::from_serialized_bytes(partition).map_err(to_datafusion_error)?;
222+
format_row_as_java_cast_string(&row, partition_fields).map_err(to_datafusion_error)
223+
}
224+
225+
fn dv_ranges_builder() -> ListBuilder<StructBuilder> {
226+
let fields = dv_meta_fields();
227+
let element_field = Arc::new(Field::new("item", DataType::Struct(fields.clone()), true));
228+
let struct_builder = StructBuilder::new(
229+
fields,
230+
vec![
231+
Box::new(StringBuilder::new()) as Box<dyn ArrayBuilder>,
232+
Box::new(Int32Builder::new()) as Box<dyn ArrayBuilder>,
233+
Box::new(Int32Builder::new()) as Box<dyn ArrayBuilder>,
234+
Box::new(Int64Builder::new()) as Box<dyn ArrayBuilder>,
235+
],
236+
);
237+
ListBuilder::new(struct_builder).with_field(element_field)
238+
}
239+
240+
fn append_dv_ranges<'a, I>(builder: &mut ListBuilder<StructBuilder>, ranges: Option<I>)
241+
where
242+
I: IntoIterator<Item = (&'a String, &'a DeletionVectorMeta)>,
243+
{
244+
let Some(ranges) = ranges else {
245+
builder.append(false);
246+
return;
247+
};
248+
249+
for (data_file_name, meta) in ranges {
250+
let values = builder.values();
251+
values
252+
.field_builder::<StringBuilder>(0)
253+
.expect("dv f0 builder")
254+
.append_value(data_file_name);
255+
values
256+
.field_builder::<Int32Builder>(1)
257+
.expect("dv f1 builder")
258+
.append_value(meta.offset);
259+
values
260+
.field_builder::<Int32Builder>(2)
261+
.expect("dv f2 builder")
262+
.append_value(meta.length);
263+
let cardinality_builder = values
264+
.field_builder::<Int64Builder>(3)
265+
.expect("dv _CARDINALITY builder");
266+
if let Some(cardinality) = meta.cardinality {
267+
cardinality_builder.append_value(cardinality);
268+
} else {
269+
cardinality_builder.append_null();
270+
}
271+
values.append(true);
272+
}
273+
builder.append(true);
274+
}
275+
276+
#[cfg(test)]
277+
mod tests {
278+
use super::*;
279+
use paimon::spec::IndexFileMeta;
280+
281+
fn index_entry(kind: FileKind, file_name: &str) -> IndexManifestEntry {
282+
IndexManifestEntry {
283+
kind,
284+
partition: vec![],
285+
bucket: 0,
286+
index_file: IndexFileMeta {
287+
index_type: "DELETION_VECTORS".to_string(),
288+
file_name: file_name.to_string(),
289+
file_size: 1,
290+
row_count: 1,
291+
deletion_vectors_ranges: None,
292+
global_index_meta: None,
293+
},
294+
version: 1,
295+
}
296+
}
297+
298+
#[test]
299+
fn visible_index_entries_skips_delete_entries() {
300+
let entries = vec![
301+
index_entry(FileKind::Add, "live.idx"),
302+
index_entry(FileKind::Delete, "deleted.idx"),
303+
];
304+
305+
let visible = visible_index_entries(entries);
306+
307+
assert_eq!(visible.len(), 1);
308+
assert_eq!(visible[0].kind, FileKind::Add);
309+
assert_eq!(visible[0].index_file.file_name, "live.idx");
310+
}
311+
}

0 commit comments

Comments
 (0)