use arrow::datatypes::{FieldRef, Fields, SchemaBuilder, SchemaRef};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct TableSchema {
file_schema: SchemaRef,
table_partition_cols: Fields,
virtual_columns: Fields,
table_schema: SchemaRef,
schema_without_virtual_columns: SchemaRef,
}
impl TableSchema {
pub fn builder(file_schema: SchemaRef) -> TableSchemaBuilder {
TableSchemaBuilder::new(file_schema)
}
#[deprecated(
since = "55.0.0",
note = "use TableSchema::builder(file_schema).with_table_partition_cols(cols).build() (or TableSchema::from(file_schema) for no partition columns)"
)]
pub fn new(file_schema: SchemaRef, table_partition_cols: Vec<FieldRef>) -> Self {
TableSchemaBuilder::new(file_schema)
.with_table_partition_cols(table_partition_cols)
.build()
}
#[deprecated(
since = "55.0.0",
note = "use TableSchema::from(file_schema) / file_schema.into()"
)]
pub fn from_file_schema(file_schema: SchemaRef) -> Self {
TableSchemaBuilder::new(file_schema).build()
}
#[deprecated(
since = "55.0.0",
note = "use TableSchema::builder(file_schema).with_table_partition_cols(cols).build()"
)]
pub fn with_table_partition_cols(self, partition_cols: Vec<FieldRef>) -> Self {
TableSchemaBuilder::new(self.file_schema)
.with_table_partition_cols(partition_cols)
.with_virtual_columns(self.virtual_columns)
.build()
}
pub fn file_schema(&self) -> &SchemaRef {
&self.file_schema
}
pub fn table_partition_cols(&self) -> &Fields {
&self.table_partition_cols
}
pub fn virtual_columns(&self) -> &Fields {
&self.virtual_columns
}
pub fn table_schema(&self) -> &SchemaRef {
&self.table_schema
}
pub fn schema_without_virtual_columns(&self) -> &SchemaRef {
&self.schema_without_virtual_columns
}
}
impl From<SchemaRef> for TableSchema {
fn from(schema: SchemaRef) -> Self {
TableSchemaBuilder::new(schema).build()
}
}
impl From<&SchemaRef> for TableSchema {
fn from(schema: &SchemaRef) -> Self {
TableSchemaBuilder::new(Arc::clone(schema)).build()
}
}
#[derive(Debug, Clone)]
pub struct TableSchemaBuilder {
file_schema: SchemaRef,
table_partition_cols: Fields,
virtual_columns: Fields,
}
impl TableSchemaBuilder {
pub fn new(file_schema: SchemaRef) -> Self {
Self {
file_schema,
table_partition_cols: Fields::empty(),
virtual_columns: Fields::empty(),
}
}
pub fn with_table_partition_cols(
mut self,
table_partition_cols: impl Into<Fields>,
) -> Self {
self.table_partition_cols = table_partition_cols.into();
self
}
pub fn with_virtual_columns(mut self, virtual_columns: impl Into<Fields>) -> Self {
self.virtual_columns = virtual_columns.into();
self
}
pub fn build(self) -> TableSchema {
debug_assert!(
self.virtual_columns.iter().enumerate().all(|(i, v)| {
let name = v.name();
!self.file_schema.fields().iter().any(|f| f.name() == name)
&& !self.table_partition_cols.iter().any(|p| p.name() == name)
&& !self.virtual_columns[..i].iter().any(|w| w.name() == name)
}),
"virtual column name collides with an existing file, partition, or virtual column"
);
let mut builder = SchemaBuilder::from(self.file_schema.as_ref());
builder.extend(self.table_partition_cols.iter().cloned());
let (table_schema, schema_without_virtual_columns) =
if self.virtual_columns.is_empty() {
let schema = Arc::new(builder.finish());
(Arc::clone(&schema), schema)
} else {
let without_virtual = Arc::new(builder.finish());
let mut builder = SchemaBuilder::from(without_virtual.as_ref());
builder.extend(self.virtual_columns.iter().cloned());
(Arc::new(builder.finish()), without_virtual)
};
TableSchema {
file_schema: self.file_schema,
table_partition_cols: self.table_partition_cols,
virtual_columns: self.virtual_columns,
table_schema,
schema_without_virtual_columns,
}
}
}
impl From<SchemaRef> for TableSchemaBuilder {
fn from(schema: SchemaRef) -> Self {
TableSchemaBuilder::new(schema)
}
}
impl From<&SchemaRef> for TableSchemaBuilder {
fn from(schema: &SchemaRef) -> Self {
TableSchemaBuilder::new(Arc::clone(schema))
}
}
#[cfg(test)]
mod tests {
use super::{TableSchema, TableSchemaBuilder};
use arrow::datatypes::{DataType, Field, Schema};
use std::sync::Arc;
#[test]
fn test_table_schema_creation() {
let file_schema = Arc::new(Schema::new(vec![
Field::new("user_id", DataType::Int64, false),
Field::new("amount", DataType::Float64, false),
]));
let partition_cols = vec![
Arc::new(Field::new("date", DataType::Utf8, false)),
Arc::new(Field::new("region", DataType::Utf8, false)),
];
let table_schema = TableSchema::builder(file_schema.clone())
.with_table_partition_cols(partition_cols.clone())
.build();
assert_eq!(table_schema.file_schema().as_ref(), file_schema.as_ref());
assert_eq!(table_schema.table_partition_cols().len(), 2);
assert_eq!(table_schema.table_partition_cols()[0], partition_cols[0]);
assert_eq!(table_schema.table_partition_cols()[1], partition_cols[1]);
let expected_fields = vec![
Field::new("user_id", DataType::Int64, false),
Field::new("amount", DataType::Float64, false),
Field::new("date", DataType::Utf8, false),
Field::new("region", DataType::Utf8, false),
];
let expected_schema = Schema::new(expected_fields);
assert_eq!(table_schema.table_schema().as_ref(), &expected_schema);
}
#[test]
fn test_builder_with_partition_cols() {
let file_schema =
Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]));
let table_schema = TableSchemaBuilder::new(Arc::clone(&file_schema))
.with_table_partition_cols(vec![
Arc::new(Field::new("country", DataType::Utf8, false)),
Arc::new(Field::new("year", DataType::Int32, false)),
])
.build();
assert_eq!(table_schema.file_schema().as_ref(), file_schema.as_ref());
assert_eq!(table_schema.table_partition_cols().len(), 2);
assert_eq!(table_schema.table_partition_cols()[0].name(), "country");
assert_eq!(table_schema.table_partition_cols()[1].name(), "year");
let expected_schema = Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("country", DataType::Utf8, false),
Field::new("year", DataType::Int32, false),
]);
assert_eq!(table_schema.table_schema().as_ref(), &expected_schema);
}
#[test]
fn test_builder_with_table_partition_cols_replaces() {
let file_schema =
Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]));
let table_schema = TableSchemaBuilder::new(file_schema)
.with_table_partition_cols(vec![Arc::new(Field::new(
"country",
DataType::Utf8,
false,
))])
.with_table_partition_cols(vec![Arc::new(Field::new(
"city",
DataType::Utf8,
false,
))])
.build();
assert_eq!(table_schema.table_partition_cols().len(), 1);
assert_eq!(table_schema.table_partition_cols()[0].name(), "city");
}
#[test]
fn test_builder_accepts_fields_zero_copy() {
let file_schema =
Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]));
let partition_schema =
Schema::new(vec![Field::new("date", DataType::Utf8, false)]);
let table_schema = TableSchemaBuilder::new(file_schema)
.with_table_partition_cols(partition_schema.fields().clone())
.build();
assert_eq!(table_schema.table_partition_cols().len(), 1);
assert_eq!(table_schema.table_partition_cols()[0].name(), "date");
}
#[test]
#[expect(deprecated)]
fn test_deprecated_with_table_partition_cols_replaces() {
let file_schema =
Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]));
let original = TableSchema::builder(file_schema)
.with_table_partition_cols(vec![Arc::new(Field::new(
"country",
DataType::Utf8,
false,
))])
.build();
let replaced =
original
.clone()
.with_table_partition_cols(vec![Arc::new(Field::new(
"city",
DataType::Utf8,
false,
))]);
assert_eq!(replaced.table_partition_cols().len(), 1);
assert_eq!(replaced.table_partition_cols()[0].name(), "city");
assert_eq!(original.table_partition_cols().len(), 1);
assert_eq!(original.table_partition_cols()[0].name(), "country");
}
#[test]
fn test_builder_with_virtual_columns_layout() {
let file_schema = Arc::new(Schema::new(vec![
Field::new("user_id", DataType::Int64, false),
Field::new("amount", DataType::Float64, false),
]));
let virtual_cols =
vec![Arc::new(Field::new("row_number", DataType::Int64, true))];
let partition_cols = vec![Arc::new(Field::new("date", DataType::Utf8, false))];
let built_virtual_first = TableSchemaBuilder::new(Arc::clone(&file_schema))
.with_virtual_columns(virtual_cols.clone())
.with_table_partition_cols(partition_cols.clone())
.build();
let built_partition_first = TableSchemaBuilder::new(Arc::clone(&file_schema))
.with_table_partition_cols(partition_cols.clone())
.with_virtual_columns(virtual_cols.clone())
.build();
let expected = Schema::new(vec![
Field::new("user_id", DataType::Int64, false),
Field::new("amount", DataType::Float64, false),
Field::new("date", DataType::Utf8, false),
Field::new("row_number", DataType::Int64, true),
]);
for ts in [built_virtual_first, built_partition_first] {
assert_eq!(ts.table_schema().as_ref(), &expected);
assert_eq!(ts.virtual_columns().len(), 1);
assert_eq!(ts.virtual_columns()[0].name(), "row_number");
assert_eq!(ts.table_partition_cols().len(), 1);
assert_eq!(ts.file_schema().fields().len(), 2);
}
}
#[test]
#[should_panic(expected = "virtual column name collides")]
#[cfg(debug_assertions)]
fn test_virtual_column_collides_with_file_schema_panics_in_debug() {
let file_schema = Arc::new(Schema::new(vec![Field::new(
"row_number",
DataType::Int64,
false,
)]));
let _ = TableSchemaBuilder::new(file_schema)
.with_virtual_columns(vec![Arc::new(Field::new(
"row_number",
DataType::Int64,
true,
))])
.build();
}
#[test]
#[should_panic(expected = "virtual column name collides")]
#[cfg(debug_assertions)]
fn test_virtual_column_collides_with_partition_panics_in_debug() {
let file_schema = Arc::new(Schema::new(vec![Field::new(
"user_id",
DataType::Int64,
false,
)]));
let partition_cols =
vec![Arc::new(Field::new("row_number", DataType::Utf8, false))];
let _ = TableSchemaBuilder::new(file_schema)
.with_table_partition_cols(partition_cols)
.with_virtual_columns(vec![Arc::new(Field::new(
"row_number",
DataType::Int64,
true,
))])
.build();
}
#[test]
#[should_panic(expected = "virtual column name collides")]
#[cfg(debug_assertions)]
fn test_duplicate_virtual_columns_panic_in_debug() {
let file_schema = Arc::new(Schema::new(vec![Field::new(
"user_id",
DataType::Int64,
false,
)]));
let _ = TableSchemaBuilder::new(file_schema)
.with_virtual_columns(vec![
Arc::new(Field::new("vc", DataType::Int64, true)),
Arc::new(Field::new("vc", DataType::Int64, true)),
])
.build();
}
#[test]
#[should_panic(expected = "virtual column name collides")]
#[cfg(debug_assertions)]
fn test_partition_column_added_after_colliding_virtual_panics_in_debug() {
let file_schema = Arc::new(Schema::new(vec![Field::new(
"user_id",
DataType::Int64,
false,
)]));
let _ = TableSchemaBuilder::new(file_schema)
.with_virtual_columns(vec![Arc::new(Field::new(
"row_number",
DataType::Int64,
true,
))])
.with_table_partition_cols(vec![Arc::new(Field::new(
"row_number",
DataType::Utf8,
false,
))])
.build();
}
}