1use std::borrow::Cow;
3use std::ops::Deref;
4use std::path::PathBuf;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use delta_kernel_derive::internal_api;
8use url::Url;
9
10use crate::{DeltaResult, Error};
11
12macro_rules! require {
14 ( $cond:expr, $err:expr ) => {
15 if !($cond) {
16 return Err($err);
17 }
18 };
19}
20
21pub(crate) use require;
22
23pub trait CollectInto<T>: IntoIterator + Sized {
47 fn collect_into(self) -> T;
49}
50
51impl<I: IntoIterator, T: FromIterator<I::Item>> CollectInto<T> for I {
53 fn collect_into(self) -> T {
54 T::from_iter(self)
55 }
56}
57
58#[allow(unused)]
61#[internal_api]
62pub(crate) fn try_parse_uri(uri: impl AsRef<str>) -> DeltaResult<Url> {
63 let uri = uri.as_ref();
64 let uri_type = resolve_uri_type(uri)?;
65 let url = match uri_type {
66 UriType::LocalPath(path) => {
67 if !path.exists() {
68 return Err(Error::InvalidTableLocation(format!(
70 "Path does not exist: {path:?}"
71 )));
72 }
73 if !path.is_dir() {
74 return Err(Error::InvalidTableLocation(format!(
75 "{path:?} is not a directory"
76 )));
77 }
78 let path = std::fs::canonicalize(path).map_err(|err| {
79 let msg = format!("Invalid table location: {uri} Error: {err:?}");
80 Error::InvalidTableLocation(msg)
81 })?;
82 Url::from_directory_path(path.clone()).map_err(|_| {
83 let msg = format!(
84 "Could not construct a URL from canonicalized path: {path:?}.\n\
85 Something must be very wrong with the table path."
86 );
87 Error::InvalidTableLocation(msg)
88 })?
89 }
90 UriType::Url(url) => url,
91 };
92 Ok(url)
93}
94
95#[allow(unused)]
96#[derive(Debug)]
97enum UriType {
98 LocalPath(PathBuf),
99 Url(Url),
100}
101
102#[allow(unused)]
107fn resolve_uri_type(table_uri: impl AsRef<str>) -> DeltaResult<UriType> {
108 let table_uri = table_uri.as_ref();
109 let table_uri = if table_uri.ends_with('/') {
110 Cow::Borrowed(table_uri)
111 } else {
112 Cow::Owned(format!("{table_uri}/"))
113 };
114 if let Ok(url) = Url::parse(&table_uri) {
115 let scheme = url.scheme().to_string();
116 if url.scheme() == "file" {
117 Ok(UriType::LocalPath(
118 url.to_file_path()
119 .map_err(|_| Error::invalid_table_location(table_uri))?,
120 ))
121 } else if scheme.len() == 1 {
122 Ok(UriType::LocalPath(PathBuf::from(table_uri.as_ref())))
125 } else {
126 Ok(UriType::Url(url))
127 }
128 } else {
129 Ok(UriType::LocalPath(table_uri.deref().into()))
130 }
131}
132
133pub(crate) fn current_time_duration() -> DeltaResult<Duration> {
135 SystemTime::now()
136 .duration_since(UNIX_EPOCH)
137 .map_err(|e| Error::generic(format!("System time before Unix epoch: {e}")))
138}
139
140pub(crate) fn current_time_ms() -> DeltaResult<i64> {
142 let duration = current_time_duration()?;
143 i64::try_from(duration.as_millis())
144 .map_err(|_| Error::generic("Current timestamp exceeds i64 millisecond range"))
145}
146
147#[internal_api]
149pub(crate) trait FoldWithOption: Sized {
150 fn fold_with<U>(self, opt: Option<U>, f: impl FnOnce(Self, U) -> Self) -> Self;
156}
157
158impl<T: Sized> FoldWithOption for T {
159 fn fold_with<U>(self, opt: Option<U>, f: impl FnOnce(Self, U) -> Self) -> Self {
160 match opt {
161 Some(value) => f(self, value),
162 None => self,
163 }
164 }
165}
166
167pub(crate) trait IteratorExt: Iterator + Sized {
169 fn on_complete<F: FnOnce()>(self, f: F) -> OnComplete<Self, F> {
174 OnComplete {
175 inner: self,
176 on_complete: Some(f),
177 }
178 }
179}
180
181impl<I: Iterator> IteratorExt for I {}
182
183pub(crate) struct OnComplete<I, F: FnOnce()> {
185 inner: I,
186 on_complete: Option<F>,
187}
188
189impl<I, F: FnOnce()> Drop for OnComplete<I, F> {
190 fn drop(&mut self) {
191 if self.on_complete.is_some() {
192 tracing::debug!(
193 "OnComplete iterator dropped before exhaustion; completion callback not called"
194 );
195 }
196 }
197}
198
199impl<I, F> Iterator for OnComplete<I, F>
200where
201 I: Iterator,
202 F: FnOnce(),
203{
204 type Item = I::Item;
205
206 fn size_hint(&self) -> (usize, Option<usize>) {
207 self.inner.size_hint()
208 }
209
210 fn next(&mut self) -> Option<Self::Item> {
211 match self.inner.next() {
212 Some(item) => Some(item),
213 None => {
214 if let Some(f) = self.on_complete.take() {
215 f();
216 }
217 None
218 }
219 }
220 }
221}
222
223#[cfg(test)]
224pub(crate) mod test_utils {
225 use std::path::{Path, PathBuf};
226 use std::sync::{Arc, Mutex};
227
228 use itertools::Itertools;
229 use serde::Serialize;
230 use tempfile::TempDir;
231 use test_utils::{delta_path_for_version, load_test_data};
232 use tracing::subscriber::DefaultGuard;
233 use tracing_subscriber::util::SubscriberInitExt as _;
234 use url::Url;
235
236 use crate::actions::{
237 get_all_actions_schema, Add, Cdc, CommitInfo, Metadata, Protocol, Remove,
238 };
239 use crate::arrow::array::{RecordBatch, StringArray, StructArray};
240 use crate::arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
241 use crate::committer::FileSystemCommitter;
242 use crate::engine::arrow_conversion::{parquet_field_id_metadata, TryIntoArrow as _};
243 use crate::engine::arrow_data::ArrowEngineData;
244 use crate::engine::sync::SyncEngine;
245 use crate::metrics::{MetricEvent, MetricsReporter, WithMetricsReporterLayer as _};
246 use crate::object_store::local::LocalFileSystem;
247 use crate::object_store::memory::InMemory;
248 use crate::object_store::ObjectStoreExt as _;
249 use crate::parquet::arrow::PARQUET_FIELD_ID_META_KEY;
250 use crate::path::ParsedLogPath;
251 use crate::table_features::ColumnMappingMode;
252 use crate::transaction::create_table::create_table;
253 use crate::transaction::{CreateTable, Transaction};
254 use crate::{DeltaResult, Engine, EngineData, Error, FileMeta, Snapshot, SnapshotRef};
255
256 pub(crate) fn create_log_path(path: &str) -> ParsedLogPath<FileMeta> {
259 create_log_path_with_size(path, 0)
260 }
261
262 pub(crate) fn create_log_path_with_size(path: &str, size: u64) -> ParsedLogPath<FileMeta> {
264 ParsedLogPath::try_from(FileMeta {
265 location: Url::parse(path).expect("Invalid file URL"),
266 last_modified: 0,
267 size,
268 })
269 .unwrap()
270 .unwrap()
271 }
272
273 #[derive(Debug, Default)]
275 pub(crate) struct CapturingReporter {
276 events: Mutex<Vec<MetricEvent>>,
277 }
278
279 impl MetricsReporter for CapturingReporter {
280 fn report(&self, event: MetricEvent) {
281 self.events.lock().unwrap().push(event);
282 }
283 }
284
285 impl CapturingReporter {
286 pub(crate) fn events(&self) -> Vec<MetricEvent> {
288 self.events.lock().unwrap().clone()
289 }
290 }
291
292 pub(crate) fn install_thread_local_metrics_reporter(
300 reporter: Arc<dyn MetricsReporter>,
301 ) -> DefaultGuard {
302 test_utils::ensure_metrics_compatible_global_subscriber();
303 tracing_subscriber::registry()
304 .with_metrics_reporter_layer(reporter)
305 .set_default()
306 }
307
308 #[derive(Serialize)]
309 pub(crate) enum Action {
310 #[serde(rename = "add")]
311 Add(Add),
312 #[serde(rename = "remove")]
313 Remove(Remove),
314 #[serde(rename = "cdc")]
315 Cdc(Cdc),
316 #[serde(rename = "metaData")]
317 Metadata(Metadata),
318 #[serde(rename = "protocol")]
319 Protocol(Protocol),
320 #[allow(unused)]
321 #[serde(rename = "commitInfo")]
322 CommitInfo(CommitInfo),
323 }
324
325 use crate::schema::{
326 schema, schema_ref, ArrayType, ColumnMetadataKey, DataType as KernelDataType, MapType,
327 MetadataValue, SchemaRef, StructField, StructType,
328 };
329
330 pub(crate) struct LocalMockTable {
333 commit_num: u64,
334 store: Arc<LocalFileSystem>,
335 dir: TempDir,
336 }
337
338 impl LocalMockTable {
339 pub(crate) fn new() -> Self {
340 let dir = tempfile::tempdir().unwrap();
341 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path()).unwrap());
342 Self {
343 commit_num: 0,
344 store,
345 dir,
346 }
347 }
348 pub(crate) async fn commit(&mut self, actions: impl IntoIterator<Item = Action>) {
350 let data = actions
351 .into_iter()
352 .map(|action| serde_json::to_string(&action).unwrap())
353 .join("\n");
354
355 let path = delta_path_for_version(self.commit_num, "json");
356 self.commit_num += 1;
357
358 self.store
359 .put(&path, data.into())
360 .await
361 .expect("put log file in store");
362 }
363
364 pub(crate) fn table_root(&self) -> &Path {
366 self.dir.path()
367 }
368 }
369
370 fn into_record_batch(engine_data: Box<dyn EngineData>) -> RecordBatch {
373 ArrowEngineData::try_from_engine_data(engine_data)
374 .unwrap()
375 .into()
376 }
377
378 pub(crate) fn assert_batch_matches(actual: Box<dyn EngineData>, expected: Box<dyn EngineData>) {
381 assert_eq!(into_record_batch(actual), into_record_batch(expected));
382 }
383
384 pub(crate) fn string_array_to_engine_data(string_array: StringArray) -> Box<dyn EngineData> {
385 let string_field = Arc::new(Field::new("a", DataType::Utf8, true));
386 let schema = Arc::new(ArrowSchema::new(vec![string_field]));
387 let batch = RecordBatch::try_new(schema, vec![Arc::new(string_array)])
388 .expect("Can't convert to record batch");
389 Box::new(ArrowEngineData::new(batch))
390 }
391
392 pub(crate) fn parse_json_batch(json_strings: StringArray) -> Box<dyn EngineData> {
393 let engine = SyncEngine::new();
394 let json_handler = engine.json_handler();
395 let output_schema = get_all_actions_schema().clone();
396 json_handler
397 .parse_json(string_array_to_engine_data(json_strings), output_schema)
398 .unwrap()
399 }
400
401 pub(crate) fn action_batch() -> Box<dyn EngineData> {
402 let json_strings: StringArray = vec![
403 r#"{"add":{"path":"part-00000-fae5310a-a37d-4e51-827b-c3d5516560ca-c000.snappy.parquet","partitionValues":{},"size":635,"modificationTime":1677811178336,"dataChange":true,"stats":"{\"numRecords\":10,\"minValues\":{\"value\":0},\"maxValues\":{\"value\":9},\"nullCount\":{\"value\":0},\"tightBounds\":true}","tags":{"INSERTION_TIME":"1677811178336000","MIN_INSERTION_TIME":"1677811178336000","MAX_INSERTION_TIME":"1677811178336000","OPTIMIZE_TARGET_SIZE":"268435456"}}}"#,
404 r#"{"remove":{"path":"part-00003-f525f459-34f9-46f5-82d6-d42121d883fd.c000.snappy.parquet","deletionTimestamp":1670892998135,"dataChange":true,"partitionValues":{"c1":"4","c2":"c"},"size":452}}"#,
405 r#"{"commitInfo":{"timestamp":1677811178585,"operation":"WRITE","operationParameters":{"mode":"ErrorIfExists","partitionBy":"[]"},"isolationLevel":"WriteSerializable","isBlindAppend":true,"operationMetrics":{"numFiles":"1","numOutputRows":"10","numOutputBytes":"635"},"engineInfo":"Databricks-Runtime/<unknown>","txnId":"a6a94671-55ef-450e-9546-b8465b9147de"}}"#,
406 r#"{"protocol":{"minReaderVersion":3,"minWriterVersion":7,"readerFeatures":["deletionVectors"],"writerFeatures":["deletionVectors"]}}"#,
407 r#"{"metaData":{"id":"testId","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"value\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{"delta.enableDeletionVectors":"true","delta.columnMapping.mode":"none", "delta.enableChangeDataFeed":"true"},"createdTime":1677811175819}}"#,
408 r#"{"cdc":{"path":"_change_data/age=21/cdc-00000-93f7fceb-281a-446a-b221-07b88132d203.c000.snappy.parquet","partitionValues":{"age":"21"},"size":1033,"dataChange":false}}"#,
409 r#"{"sidecar":{"path":"016ae953-37a9-438e-8683-9a9a4a79a395.parquet","sizeInBytes":9268,"modificationTime":1714496113961,"tags":{"tag_foo":"tag_bar"}}}"#,
410 r#"{"txn":{"appId":"myApp","version": 3}}"#,
411 r#"{"checkpointMetadata":{"version":2, "tags":{"tag_foo":"tag_bar"}}}"#,
412 ]
413 .into();
414 parse_json_batch(json_strings)
415 }
416
417 #[track_caller]
419 pub(crate) fn assert_result_error_with_message<T, E: ToString>(
420 res: Result<T, E>,
421 message: &str,
422 ) {
423 match res {
424 Ok(_) => panic!("Expected error with message {message}, but got Ok result"),
425 Err(error) => {
426 let error_str = error.to_string();
427 assert!(
428 error_str.contains(message),
429 "Error message does not contain the expected message.\nExpected message:\t{message}\nActual message:\t\t{error_str}"
430 );
431 }
432 }
433 }
434
435 #[track_caller]
444 pub(crate) fn assert_schema_feature_validation(
445 schema_with: &StructType,
446 schema_without: &StructType,
447 protocol_with: &Protocol,
448 protocol_without: &Protocol,
449 extra_err_schemas: &[&StructType],
450 err_msg: &str,
451 ) {
452 make_test_tc(schema_with.clone(), protocol_with.clone(), [])
453 .expect("feature present + supported");
454 make_test_tc(schema_without.clone(), protocol_without.clone(), [])
455 .expect("feature absent + unsupported");
456 make_test_tc(schema_without.clone(), protocol_with.clone(), [])
457 .expect("feature absent + supported");
458 assert_result_error_with_message(
459 make_test_tc(schema_with.clone(), protocol_without.clone(), []),
460 err_msg,
461 );
462 for schema in extra_err_schemas {
463 assert_result_error_with_message(
464 make_test_tc((*schema).clone(), protocol_without.clone(), []),
465 err_msg,
466 );
467 }
468 }
469
470 pub(crate) fn make_test_tc(
473 schema: StructType,
474 protocol: Protocol,
475 props: impl IntoIterator<Item = (String, String)>,
476 ) -> crate::DeltaResult<crate::table_configuration::TableConfiguration> {
477 let schema = std::sync::Arc::new(schema);
478 let metadata =
479 Metadata::try_new(None, None, schema, vec![], 0, props.into_iter().collect()).unwrap();
480 let table_root = Url::try_from("file:///").unwrap();
481 crate::table_configuration::TableConfiguration::try_new(metadata, protocol, table_root, 0)
482 }
483
484 fn cm_field(
491 name: &str,
492 id: i64,
493 physical_name: &str,
494 ty: impl Into<KernelDataType>,
495 ) -> StructField {
496 StructField::nullable(name, ty).with_metadata([
497 (
498 ColumnMetadataKey::ColumnMappingId.as_ref(),
499 MetadataValue::Number(id),
500 ),
501 (
502 ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(),
503 MetadataValue::String(physical_name.into()),
504 ),
505 ])
506 }
507
508 pub(crate) struct NestedFieldIdFixture {
510 pub(crate) kernel_schema: StructType,
511 pub(crate) input_arrow_data: StructArray,
512 pub(crate) expected_arrow_schema: ArrowSchema,
513 }
514
515 pub(crate) fn collect_arrow_field_metadata(
518 schema: &ArrowSchema,
519 metadata_key: &str,
520 ) -> Vec<(String, String)> {
521 fn collect_from_fields(
522 fields: &[Arc<Field>],
523 metadata_key: &str,
524 out: &mut Vec<(String, String)>,
525 ) {
526 for field in fields {
527 collect_from_field(field, metadata_key, out);
528 }
529 }
530
531 fn collect_from_field(field: &Field, metadata_key: &str, out: &mut Vec<(String, String)>) {
532 if let Some(value) = field.metadata().get(metadata_key) {
533 out.push((field.name().clone(), value.clone()));
534 }
535
536 match field.data_type() {
537 DataType::Struct(fields) => collect_from_fields(fields, metadata_key, out),
538 DataType::List(entry) | DataType::Map(entry, _) => {
539 collect_from_field(entry, metadata_key, out)
540 }
541 _ => {}
542 }
543 }
544
545 let mut out = Vec::new();
546 collect_from_fields(schema.fields(), metadata_key, &mut out);
547 out
548 }
549
550 pub(crate) fn array_in_map_kernel_schema(
553 metadata: impl IntoIterator<Item = (String, MetadataValue)>,
554 ) -> StructType {
555 let array_in_map = StructField::nullable(
556 "array_in_map",
557 MapType::new(
558 KernelDataType::INTEGER,
559 ArrayType::new(KernelDataType::INTEGER, true),
560 true,
561 ),
562 )
563 .with_metadata(metadata);
564 StructType::try_new(vec![array_in_map]).unwrap()
565 }
566
567 pub(crate) fn array_in_map_with_field_ids(nested_ids_meta_key: &str) -> StructType {
570 let nested_ids = MetadataValue::Other(test_utils::nested_ids_json(&[
571 ("array_in_map.key", 100),
572 ("array_in_map.value", 101),
573 ("array_in_map.value.element", 102),
574 ]));
575 array_in_map_kernel_schema([
576 (
577 ColumnMetadataKey::ParquetFieldId.as_ref().to_string(),
578 MetadataValue::from(1i64),
579 ),
580 (nested_ids_meta_key.to_string(), nested_ids),
581 ])
582 }
583
584 pub(crate) fn array_in_map_arrow_data_without_field_ids() -> StructArray {
587 let kernel_schema =
588 array_in_map_kernel_schema(std::iter::empty::<(String, MetadataValue)>());
589 let arrow_schema: ArrowSchema = (&kernel_schema).try_into_arrow().unwrap();
590 let batch = RecordBatch::new_empty(Arc::new(arrow_schema));
591 StructArray::try_new(
592 batch.schema().fields.clone(),
593 batch.columns().to_vec(),
594 None,
595 )
596 .unwrap()
597 }
598
599 pub(crate) fn complex_nested_with_field_ids(nested_ids_meta_key: &str) -> NestedFieldIdFixture {
693 NestedFieldIdFixture {
694 kernel_schema: build_complex_nested_kernel_schema(nested_ids_meta_key),
695 input_arrow_data: build_arrow_input_with_stale_element_id(),
696 expected_arrow_schema: expected_complex_nested_arrow_schema(),
697 }
698 }
699
700 fn build_arrow_input_with_stale_element_id() -> StructArray {
704 let plain_inner = StructField::nullable("inner", complex_nested_inner_map_type());
706 let plain_top = StructField::nullable(
707 "top",
708 complex_nested_outer_map_type(StructType::try_new(vec![plain_inner]).unwrap()),
709 );
710 let plain_kernel_schema = StructType::try_new(vec![plain_top]).unwrap();
711 let plain_arrow_schema: ArrowSchema = (&plain_kernel_schema).try_into_arrow().unwrap();
712
713 let top = plain_arrow_schema.field(0);
715 let DataType::Map(entries, sorted) = top.data_type() else {
716 unreachable!("top is a Map by construction");
717 };
718 let DataType::Struct(entries_fields) = entries.data_type() else {
719 unreachable!("map entries is a Struct by construction");
720 };
721 let outer_key = &entries_fields[0];
722 let DataType::List(outer_element) = outer_key.data_type() else {
723 unreachable!("outer map key is a List by construction");
724 };
725 let stale_element = outer_element
726 .as_ref()
727 .clone()
728 .with_metadata([(PARQUET_FIELD_ID_META_KEY.to_string(), "999".to_string())].into());
729 let new_outer_key = Field::new(
730 outer_key.name(),
731 DataType::List(Arc::new(stale_element)),
732 outer_key.is_nullable(),
733 );
734 let new_entries = Field::new(
735 entries.name(),
736 DataType::Struct(vec![new_outer_key, entries_fields[1].as_ref().clone()].into()),
737 entries.is_nullable(),
738 );
739 let new_top = Field::new(
740 top.name(),
741 DataType::Map(Arc::new(new_entries), *sorted),
742 top.is_nullable(),
743 );
744 let arrow_input_schema = ArrowSchema::new(vec![new_top]);
745 let batch = RecordBatch::new_empty(Arc::new(arrow_input_schema));
746 StructArray::try_new(
747 batch.schema().fields.clone(),
748 batch.columns().to_vec(),
749 None,
750 )
751 .unwrap()
752 }
753
754 fn complex_nested_inner_map_type() -> KernelDataType {
755 KernelDataType::from(MapType::new(
756 KernelDataType::INTEGER,
757 ArrayType::new(KernelDataType::INTEGER, true),
758 true,
759 ))
760 }
761
762 fn complex_nested_outer_map_type(struct_value: StructType) -> KernelDataType {
763 KernelDataType::from(MapType::new(
764 ArrayType::new(KernelDataType::INTEGER, true),
765 struct_value,
766 true,
767 ))
768 }
769
770 pub(crate) fn build_complex_nested_kernel_schema(nested_ids_meta_key: &str) -> StructType {
772 let top_nested_ids = test_utils::nested_ids_json(&[
773 ("top.key", 100),
774 ("top.key.element", 101),
775 ("top.value", 102),
776 ]);
777 let inner_nested_ids = test_utils::nested_ids_json(&[
778 ("inner.key", 200),
779 ("inner.value", 201),
780 ("inner.value.element", 202),
781 ]);
782 let inner_field = StructField::nullable("inner", complex_nested_inner_map_type())
784 .with_metadata([
785 (
786 ColumnMetadataKey::ParquetFieldId.as_ref().to_string(),
787 MetadataValue::from(2i64),
788 ),
789 (
790 nested_ids_meta_key.to_string(),
791 MetadataValue::Other(inner_nested_ids),
792 ),
793 ]);
794 let top_field = StructField::nullable(
795 "top",
796 complex_nested_outer_map_type(StructType::try_new(vec![inner_field]).unwrap()),
797 )
798 .with_metadata([
799 (
800 ColumnMetadataKey::ParquetFieldId.as_ref().to_string(),
801 MetadataValue::from(1i64),
802 ),
803 (
804 nested_ids_meta_key.to_string(),
805 MetadataValue::Other(top_nested_ids),
806 ),
807 ]);
808 StructType::try_new(vec![top_field]).unwrap()
809 }
810
811 fn expected_complex_nested_arrow_schema() -> ArrowSchema {
813 let inner_list_element = Field::new("element", DataType::Int32, true)
815 .with_metadata(parquet_field_id_metadata(Some(202)));
816 let inner_value = Field::new("value", DataType::List(Arc::new(inner_list_element)), true)
818 .with_metadata(parquet_field_id_metadata(Some(201)));
819 let inner_key = Field::new("key", DataType::Int32, false)
821 .with_metadata(parquet_field_id_metadata(Some(200)));
822 let inner_entries = Field::new(
824 "key_value",
825 DataType::Struct(vec![inner_key, inner_value].into()),
826 false,
827 );
828 let inner_field = Field::new("inner", DataType::Map(Arc::new(inner_entries), false), true)
830 .with_metadata(parquet_field_id_metadata(Some(2)));
831 let struct_value_field =
833 Field::new("value", DataType::Struct(vec![inner_field].into()), true)
834 .with_metadata(parquet_field_id_metadata(Some(102)));
835 let outer_key_element = Field::new("element", DataType::Int32, true)
837 .with_metadata(parquet_field_id_metadata(Some(101)));
838 let outer_key = Field::new("key", DataType::List(Arc::new(outer_key_element)), false)
840 .with_metadata(parquet_field_id_metadata(Some(100)));
841 let outer_entries = Field::new(
843 "key_value",
844 DataType::Struct(vec![outer_key, struct_value_field].into()),
845 false,
846 );
847 let top_field = Field::new("top", DataType::Map(Arc::new(outer_entries), false), true)
849 .with_metadata(parquet_field_id_metadata(Some(1)));
850 ArrowSchema::new(vec![top_field])
851 }
852
853 pub(crate) fn test_schema_flat() -> SchemaRef {
855 schema_ref! {
856 nullable "id": LONG,
857 nullable "name": STRING,
858 }
859 }
860
861 pub(crate) fn test_schema_flat_with_column_mapping() -> SchemaRef {
863 schema_ref! {
864 (cm_field("id", 1, "phys_id", KernelDataType::LONG)),
865 (cm_field("name", 2, "phys_name", KernelDataType::STRING)),
866 }
867 }
868
869 pub(crate) fn test_schema_nested() -> SchemaRef {
871 schema_ref! {
872 nullable "id": LONG,
873 nullable "info": {
874 nullable "name": STRING,
875 nullable "age": INTEGER,
876 nullable "tags": { STRING => nullable STRING },
877 nullable "scores": [ nullable INTEGER ],
878 },
879 }
880 }
881
882 pub(crate) fn test_schema_nested_with_column_mapping() -> SchemaRef {
884 schema_ref! {
885 (cm_field("id", 1, "phys_id", KernelDataType::LONG)),
886 (cm_field("info", 2, "phys_info", schema! {
887 (cm_field("name", 3, "phys_name", KernelDataType::STRING)),
888 (cm_field("age", 4, "phys_age", KernelDataType::INTEGER)),
889 (cm_field("tags", 5, "phys_tags",
890 MapType::new(KernelDataType::STRING, KernelDataType::STRING, true))),
891 (cm_field("scores", 6, "phys_scores",
892 ArrayType::new(KernelDataType::INTEGER, true))),
893 })),
894 }
895 }
896
897 pub(crate) fn test_schema_with_map() -> SchemaRef {
899 schema_ref! {
900 nullable "id": LONG,
901 nullable "entries": { STRING => nullable {
902 nullable "key": STRING,
903 nullable "value": INTEGER,
904 } },
905 nullable "name": STRING,
906 }
907 }
908
909 pub(crate) fn test_schema_with_map_and_column_mapping() -> SchemaRef {
911 let value_struct = schema! {
912 (cm_field("key", 4, "phys_key", KernelDataType::STRING)),
913 (cm_field("value", 5, "phys_value", KernelDataType::INTEGER)),
914 };
915 schema_ref! {
916 (cm_field("id", 1, "phys_id", KernelDataType::LONG)),
917 (cm_field("entries", 2, "phys_entries",
918 MapType::new(KernelDataType::STRING, value_struct, true))),
919 (cm_field("name", 3, "phys_name", KernelDataType::STRING)),
920 }
921 }
922
923 pub(crate) fn test_schema_with_array() -> SchemaRef {
925 schema_ref! {
926 nullable "id": LONG,
927 nullable "items": [ nullable {
928 nullable "label": STRING,
929 nullable "count": INTEGER,
930 } ],
931 nullable "name": STRING,
932 }
933 }
934
935 pub(crate) fn test_schema_with_array_and_column_mapping() -> SchemaRef {
937 let item_struct = schema! {
938 (cm_field("label", 4, "phys_label", KernelDataType::STRING)),
939 (cm_field("count", 5, "phys_count", KernelDataType::INTEGER)),
940 };
941 schema_ref! {
942 (cm_field("id", 1, "phys_id", KernelDataType::LONG)),
943 (cm_field("items", 2, "phys_items", ArrayType::new(item_struct, true))),
944 (cm_field("name", 3, "phys_name", KernelDataType::STRING)),
945 }
946 }
947
948 pub(crate) fn test_deep_nested_schema_missing_leaf_cm() -> StructType {
953 let map_type = MapType::new(
954 KernelDataType::STRING,
955 schema! { not_null "leaf": INTEGER },
956 true,
957 );
958 let array_type = ArrayType::new(
959 schema! { (cm_field("mid_field", 2, "phys_mid_field", map_type)) },
960 true,
961 );
962 schema! {
963 (cm_field("top", 1, "phys_top", array_type)),
964 }
965 }
966
967 pub(crate) fn setup_column_mapping_txn(
970 schema: SchemaRef,
971 mode: ColumnMappingMode,
972 ) -> DeltaResult<(Arc<dyn Engine>, Transaction<CreateTable>)> {
973 let mode_str = match mode {
974 ColumnMappingMode::Name => "name",
975 ColumnMappingMode::Id => "id",
976 ColumnMappingMode::None => "none",
977 };
978 let store = Arc::new(InMemory::new());
979 let engine: Arc<dyn Engine> = Arc::new(SyncEngine::new_with_store(store));
980
981 let txn = create_table("memory:///test_table", schema, "DefaultEngine")
982 .with_table_properties([("delta.columnMapping.mode", mode_str)])
983 .build(engine.as_ref(), Box::new(FileSystemCommitter::new()))?;
984 Ok((engine, txn))
985 }
986
987 pub(crate) fn validate_physical_schema_column_mapping(
991 logical_schema: &StructType,
992 physical_schema: &StructType,
993 mode: ColumnMappingMode,
994 ) {
995 assert_eq!(
996 physical_schema.fields().count(),
997 logical_schema.fields().count()
998 );
999
1000 let expected: Vec<_> = logical_schema
1002 .fields()
1003 .map(|f| {
1004 let physical_name =
1005 match f.get_config_value(&ColumnMetadataKey::ColumnMappingPhysicalName) {
1006 Some(MetadataValue::String(name)) => name.clone(),
1007 _ if mode == ColumnMappingMode::None => f.name().to_string(),
1008 _ => panic!("Logical field '{}' missing physicalName metadata", f.name()),
1009 };
1010 let field_id = match f.get_config_value(&ColumnMetadataKey::ColumnMappingId) {
1011 Some(MetadataValue::Number(id)) => *id,
1012 _ if mode == ColumnMappingMode::None => -1,
1013 _ => panic!(
1014 "Logical field '{}' missing columnMapping.id metadata",
1015 f.name()
1016 ),
1017 };
1018 (physical_name, field_id)
1019 })
1020 .collect();
1021
1022 for (physical_field, (expected_name, expected_id)) in
1024 physical_schema.fields().zip(expected.iter())
1025 {
1026 assert_eq!(
1027 physical_field.name(),
1028 expected_name,
1029 "Physical field name mismatch"
1030 );
1031
1032 if mode == ColumnMappingMode::None {
1033 continue;
1034 }
1035
1036 assert_eq!(
1037 physical_field.get_config_value(&ColumnMetadataKey::ColumnMappingPhysicalName),
1038 Some(&MetadataValue::String(expected_name.clone())),
1039 "columnMapping.physicalName mismatch for '{}'",
1040 physical_field.name()
1041 );
1042
1043 assert_eq!(
1044 physical_field.get_config_value(&ColumnMetadataKey::ColumnMappingId),
1045 Some(&MetadataValue::Number(*expected_id)),
1046 "columnMapping.id mismatch for '{}'",
1047 physical_field.name()
1048 );
1049
1050 assert_eq!(
1051 physical_field.get_config_value(&ColumnMetadataKey::ParquetFieldId),
1052 Some(&MetadataValue::Number(*expected_id)),
1053 "parquet.field.id mismatch for '{}'",
1054 physical_field.name()
1055 );
1056 }
1057 }
1058
1059 pub(crate) fn load_test_table(
1064 table_name: &str,
1065 ) -> DeltaResult<(Arc<dyn Engine>, SnapshotRef, Option<TempDir>)> {
1066 let (path, tempdir) = match load_test_data("tests/data", table_name) {
1068 Ok(test_dir) => {
1069 let test_path = test_dir.path().join(table_name);
1070 (test_path, Some(test_dir))
1071 }
1072 Err(_) => {
1073 let manifest_dir = env!("CARGO_MANIFEST_DIR");
1075 let mut path = PathBuf::from(manifest_dir);
1076 path.push("tests/data");
1077 path.push(table_name);
1078 let path = std::fs::canonicalize(path)
1079 .map_err(|e| Error::Generic(format!("Failed to canonicalize path: {e}")))?;
1080 (path, None)
1081 }
1082 };
1083
1084 let url = Url::from_directory_path(&path)
1086 .map_err(|_| Error::Generic("Failed to create URL from path".to_string()))?;
1087
1088 let engine = Arc::new(SyncEngine::new());
1089 let snapshot = Snapshot::builder_for(url).build(engine.as_ref())?;
1090 Ok((engine, snapshot, tempdir))
1091 }
1092
1093 pub(crate) mod column_mapping_physical_name_dedup_fixtures {
1094 use crate::schema::{
1095 schema, ArrayType, ColumnMetadataKey, DataType, MapType, MetadataValue, StructField,
1096 StructType,
1097 };
1098
1099 pub(crate) fn same_phy_name_different_paths() -> StructType {
1101 let nested = StructType::new_unchecked([cm_field("id", 3, "id", DataType::INTEGER)]);
1102 StructType::new_unchecked([
1103 cm_field("id", 1, "id", DataType::INTEGER),
1104 cm_field("nested", 2, "nested", nested),
1105 ])
1106 }
1107
1108 pub(crate) fn deeply_nested_repeat_physical_paths() -> StructType {
1110 let inner = StructType::new_unchecked([
1111 cm_field("a", 2, "x", DataType::INTEGER),
1112 cm_field("b", 3, "x", DataType::INTEGER),
1113 ]);
1114 let arr_of_struct = ArrayType::new(inner, true);
1115 let map_to_arr = MapType::new(DataType::STRING, arr_of_struct, true);
1116 StructType::new_unchecked([cm_field("outer", 1, "outer", map_to_arr)])
1117 }
1118
1119 pub(crate) fn deeply_nested_collider_paths() -> (&'static str, &'static str) {
1122 (
1123 "outer.`<map value>`.`<array element>`.a",
1124 "outer.`<map value>`.`<array element>`.b",
1125 )
1126 }
1127
1128 pub(crate) fn multiple_physical_name_collisions() -> StructType {
1136 schema! {
1137 (cm_field("a", 1, "p", schema! { (cm_field("aa", 6, "aa", DataType::INTEGER)) })),
1138 (cm_field("b", 2, "p", schema! { (cm_field("bb", 7, "bb", DataType::INTEGER)) })),
1139 (cm_field("nested", 3, "nested", schema! {
1140 (cm_field("x", 4, "q", DataType::INTEGER)),
1141 (cm_field("y", 5, "q", DataType::INTEGER)),
1142 })),
1143 }
1144 }
1145
1146 fn cm_field(name: &str, id: i64, phys: &str, ty: impl Into<DataType>) -> StructField {
1147 StructField::new(name, ty, true).with_metadata([
1148 (
1149 ColumnMetadataKey::ColumnMappingId.as_ref(),
1150 MetadataValue::Number(id),
1151 ),
1152 (
1153 ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(),
1154 MetadataValue::String(phys.to_string()),
1155 ),
1156 ])
1157 }
1158 }
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163 use super::*;
1164
1165 #[test]
1166 fn test_path_parsing() {
1167 for x in [
1168 #[cfg(not(windows))]
1170 "file:///foo/bar",
1171 #[cfg(not(windows))]
1172 "file:///foo/bar/",
1173 "/foo/bar",
1174 "/foo/bar/",
1175 "../foo/bar",
1176 "../foo/bar/",
1177 "c:/foo/bar",
1178 "c:/",
1179 "file:///C:/",
1180 ] {
1181 match resolve_uri_type(x) {
1182 Ok(UriType::LocalPath(_)) => {}
1183 x => panic!("Should have parsed as a local path {x:?}"),
1184 }
1185 }
1186
1187 for x in [
1188 "s3://foo/bar",
1189 "s3a://foo/bar",
1190 "memory://foo/bar",
1191 "gs://foo/bar",
1192 "https://foo/bar/",
1193 "unknown://foo/bar",
1194 "s2://foo/bar",
1195 ] {
1196 match resolve_uri_type(x) {
1197 Ok(UriType::Url(_)) => {}
1198 x => panic!("Should have parsed as a url {x:?}"),
1199 }
1200 }
1201
1202 #[cfg(not(windows))]
1203 resolve_uri_type("file://foo/bar").expect_err("file://foo/bar should not have parsed");
1204 }
1205
1206 #[test]
1207 fn try_from_uri_without_trailing_slash() {
1208 let location = "s3://foo/__unitystorage/catalogs/cid/tables/tid";
1209 let url = try_parse_uri(location).unwrap();
1210
1211 assert_eq!(
1212 url.to_string(),
1213 "s3://foo/__unitystorage/catalogs/cid/tables/tid/"
1214 );
1215 }
1216
1217 mod on_complete_tests {
1218 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
1219 use std::sync::Arc;
1220
1221 use super::*;
1222
1223 #[test]
1224 fn test_calls_on_exhaustion() {
1225 let called = Arc::new(AtomicBool::new(false));
1226 let called_clone = called.clone();
1227 let mut iter = vec![1, 2].into_iter().on_complete(move || {
1228 called_clone.store(true, Ordering::SeqCst);
1229 });
1230 assert_eq!(iter.next(), Some(1));
1231 assert!(!called.load(Ordering::SeqCst));
1232 assert_eq!(iter.next(), Some(2));
1233 assert_eq!(iter.next(), None);
1234 assert!(called.load(Ordering::SeqCst));
1235 }
1236
1237 #[test]
1238 fn test_does_not_call_on_early_drop() {
1239 let called = Arc::new(AtomicBool::new(false));
1240 let called_clone = called.clone();
1241 {
1242 let mut iter = vec![1, 2].into_iter().on_complete(move || {
1243 called_clone.store(true, Ordering::SeqCst);
1244 });
1245 assert_eq!(iter.next(), Some(1));
1246 }
1248 assert!(!called.load(Ordering::SeqCst));
1249 }
1250
1251 #[test]
1252 fn test_calls_only_once() {
1253 let count = Arc::new(AtomicU32::new(0));
1254 let count_clone = count.clone();
1255 {
1256 let mut iter = vec![1].into_iter().on_complete(move || {
1257 count_clone.fetch_add(1, Ordering::SeqCst);
1258 });
1259 assert_eq!(iter.next(), Some(1));
1260 assert_eq!(iter.next(), None); assert_eq!(iter.next(), None); } assert_eq!(count.load(Ordering::SeqCst), 1);
1264 }
1265 }
1266}