Skip to content

Commit e6599f1

Browse files
committed
Maintain schema metadata on export
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent 2af32c0 commit e6599f1

2 files changed

Lines changed: 100 additions & 8 deletions

File tree

vortex-datafusion/src/convert/schema.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ pub fn calculate_physical_schema(
5656
})
5757
.collect::<DFResult<Vec<_>>>()?;
5858

59-
Ok(Schema::new(fields))
59+
Ok(Schema::new_with_metadata(
60+
fields,
61+
reference_logical_schema.metadata().clone(),
62+
))
6063
}
6164

6265
/// Calculate the physical Arrow type for a field, preferring the logical type when the
@@ -246,6 +249,32 @@ mod tests {
246249
);
247250
}
248251

252+
#[test]
253+
fn test_schema_metadata_preserved() -> DFResult<()> {
254+
let logical_schema = Schema::new_with_metadata(
255+
vec![Field::new("col", DataType::Int32, false)],
256+
[("table".to_string(), "metadata".to_string())]
257+
.into_iter()
258+
.collect(),
259+
);
260+
let dtype = DType::Struct(
261+
StructFields::from_iter([(
262+
"col",
263+
DType::Primitive(PType::I32, Nullability::NonNullable),
264+
)]),
265+
Nullability::NonNullable,
266+
);
267+
268+
let physical_schema =
269+
calculate_physical_schema(&dtype, &logical_schema, &ArrowSession::default())?;
270+
271+
assert_eq!(
272+
physical_schema.metadata().get("table"),
273+
Some(&"metadata".to_string())
274+
);
275+
Ok(())
276+
}
277+
249278
#[test]
250279
fn test_utf8_variants_preserved() {
251280
// Non-view string types become view types after roundtrip through DType,

vortex-datafusion/src/persistent/opener.rs

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ pub(crate) struct VortexOpener {
111111

112112
impl FileOpener for VortexOpener {
113113
fn open(&self, file: PartitionedFile) -> DFResult<FileOpenFuture> {
114+
// Calculate the output schema before replacing partition columns with literals so it
115+
// retains the table and partition-field metadata declared by the plan.
116+
let output_schema = Arc::new(
117+
self.projection
118+
.project_schema(self.table_schema.table_schema())?,
119+
);
114120
let session = self.session.clone();
115121
let metrics_registry = Arc::clone(&self.metrics_registry);
116122
let labels = vec![
@@ -259,8 +265,6 @@ impl FileOpener for VortexOpener {
259265
&session.arrow(),
260266
)?);
261267

262-
let projected_physical_schema = projection.project_schema(&unified_file_schema)?;
263-
264268
let expr_adapter = expr_adapter_factory.create(
265269
Arc::clone(&unified_file_schema),
266270
Arc::clone(&this_file_schema),
@@ -287,7 +291,7 @@ impl FileOpener for VortexOpener {
287291
expr_convertor.split_projection(
288292
projection.clone(),
289293
&this_file_schema,
290-
&projected_physical_schema,
294+
output_schema.as_ref(),
291295
)?
292296
} else {
293297
// When projection pushdown is disabled, read only the required columns
@@ -304,15 +308,15 @@ impl FileOpener for VortexOpener {
304308
// When projection pushdown is enabled, the scan outputs the projected columns.
305309
// When disabled, the scan outputs raw columns and the projection is applied after.
306310
let scan_reference_schema = if projection_pushdown {
307-
projected_physical_schema
311+
(*output_schema).clone()
308312
} else {
309313
// Build schema from the raw columns being read
310314
let column_indices = projection.column_indices();
311315
let fields: Vec<_> = column_indices
312316
.into_iter()
313317
.map(|idx| this_file_schema.field(idx).clone())
314318
.collect();
315-
Schema::new(fields)
319+
Schema::new_with_metadata(fields, this_file_schema.metadata().clone())
316320
};
317321
let stream_schema =
318322
calculate_physical_schema(&scan_dtype, &scan_reference_schema, &session.arrow())?;
@@ -450,11 +454,15 @@ impl FileOpener for VortexOpener {
450454
))))
451455
})
452456
.map(move |batch| {
453-
if projector.projection().as_ref().is_empty() {
457+
let batch = if projector.projection().as_ref().is_empty() {
454458
batch
455459
} else {
456460
batch.and_then(|b| projector.project_batch(&b))
457-
}
461+
}?;
462+
463+
batch
464+
.with_schema(Arc::clone(&output_schema))
465+
.map_err(Into::into)
458466
})
459467
.boxed();
460468

@@ -814,6 +822,61 @@ mod tests {
814822
Ok(())
815823
}
816824

825+
#[tokio::test]
826+
async fn test_open_preserves_declared_schema_metadata() -> anyhow::Result<()> {
827+
let object_store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
828+
let file_path = "part=1/file.vortex";
829+
let batch = record_batch!(("a", Int32, vec![Some(1), Some(2), Some(3)]))?;
830+
let data_size =
831+
write_arrow_to_vortex(Arc::clone(&object_store), file_path, batch.clone()).await?;
832+
833+
let file_schema = Arc::new(
834+
batch.schema().as_ref().clone().with_metadata(
835+
[("table".to_string(), "metadata".to_string())]
836+
.into_iter()
837+
.collect(),
838+
),
839+
);
840+
let table_schema = TableSchema::new(
841+
file_schema,
842+
vec![Arc::new(
843+
Field::new("part", DataType::Int32, false).with_metadata(
844+
[("partition".to_string(), "metadata".to_string())]
845+
.into_iter()
846+
.collect(),
847+
),
848+
)],
849+
);
850+
let projection = ProjectionExprs::from_indices(&[0, 1], table_schema.table_schema());
851+
let expected_schema = Arc::new(projection.project_schema(table_schema.table_schema())?);
852+
853+
assert_eq!(
854+
expected_schema.metadata().get("table"),
855+
Some(&"metadata".to_string())
856+
);
857+
assert_eq!(
858+
expected_schema.field(1).metadata().get("partition"),
859+
Some(&"metadata".to_string())
860+
);
861+
862+
for projection_pushdown in [false, true] {
863+
let mut opener = make_opener(Arc::clone(&object_store), table_schema.clone(), None);
864+
opener.projection = projection.clone();
865+
opener.projection_pushdown = projection_pushdown;
866+
867+
let mut file = PartitionedFile::new(file_path.to_string(), data_size);
868+
file.partition_values = vec![ScalarValue::Int32(Some(1))];
869+
let batches = opener.open(file)?.await?.try_collect::<Vec<_>>().await?;
870+
871+
assert!(!batches.is_empty());
872+
for batch in batches {
873+
assert_eq!(batch.schema().as_ref(), expected_schema.as_ref());
874+
}
875+
}
876+
877+
Ok(())
878+
}
879+
817880
#[tokio::test]
818881
async fn test_file_pruning_replaces_partition_columns_without_file_statistics()
819882
-> anyhow::Result<()> {

0 commit comments

Comments
 (0)