datafusion_datasource_parquet/schema_coercion.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//! Arrow-schema coercion utilities used by the Parquet reader to make a
19//! file schema match the table schema (binary→string, regular→view,
20//! INT96→Timestamp).
21//!
22//! These helpers are independent of the [`ParquetFormat`](crate::file_format::ParquetFormat)
23//! type and several have been re-exported at the crate root for use by
24//! callers outside the format implementation.
25
26use std::cell::RefCell;
27use std::collections::{HashMap, HashSet};
28use std::rc::Rc;
29use std::sync::Arc;
30
31use arrow::datatypes::{DataType, Field, FieldRef, Schema, TimeUnit};
32use parquet::basic::Type;
33use parquet::schema::types::SchemaDescriptor;
34
35/// Apply necessary schema type coercions to make file schema match table schema.
36///
37/// This function performs two main types of transformations in a single pass:
38/// 1. Binary types to string types conversion - Converts binary data types to their
39/// corresponding string types when the table schema expects string data
40/// 2. Regular to view types conversion - Converts standard string/binary types to
41/// view types when the table schema uses view types
42///
43/// # Arguments
44/// * `table_schema` - The table schema containing the desired types
45/// * `file_schema` - The file schema to be transformed
46///
47/// # Returns
48/// * `Some(Schema)` - If any transformations were applied, returns the transformed schema
49/// * `None` - If no transformations were needed
50pub fn apply_file_schema_type_coercions(
51 table_schema: &Schema,
52 file_schema: &Schema,
53) -> Option<Schema> {
54 let mut needs_view_transform = false;
55 let mut needs_string_transform = false;
56
57 // Create a mapping of table field names to their data types for fast lookup
58 // and simultaneously check if we need any transformations
59 let table_fields: HashMap<_, _> = table_schema
60 .fields()
61 .iter()
62 .map(|f| {
63 let dt = f.data_type();
64 // Check if we need view type transformation
65 if matches!(dt, &DataType::Utf8View | &DataType::BinaryView) {
66 needs_view_transform = true;
67 }
68 // Check if we need string type transformation
69 if matches!(
70 dt,
71 &DataType::Utf8 | &DataType::LargeUtf8 | &DataType::Utf8View
72 ) {
73 needs_string_transform = true;
74 }
75
76 (f.name(), dt)
77 })
78 .collect();
79
80 // Early return if no transformation needed
81 if !needs_view_transform && !needs_string_transform {
82 return None;
83 }
84
85 let transformed_fields: Vec<Arc<Field>> = file_schema
86 .fields()
87 .iter()
88 .map(|field| {
89 let field_name = field.name();
90 let field_type = field.data_type();
91
92 // Look up the corresponding field type in the table schema
93 if let Some(table_type) = table_fields.get(field_name) {
94 match (table_type, field_type) {
95 // table schema uses string type, coerce the file schema to use string type
96 (
97 &DataType::Utf8,
98 DataType::Binary | DataType::LargeBinary | DataType::BinaryView,
99 ) => {
100 return field_with_new_type(field, DataType::Utf8);
101 }
102 // table schema uses large string type, coerce the file schema to use large string type
103 (
104 &DataType::LargeUtf8,
105 DataType::Binary | DataType::LargeBinary | DataType::BinaryView,
106 ) => {
107 return field_with_new_type(field, DataType::LargeUtf8);
108 }
109 // table schema uses string view type, coerce the file schema to use view type
110 (
111 &DataType::Utf8View,
112 DataType::Binary | DataType::LargeBinary | DataType::BinaryView,
113 ) => {
114 return field_with_new_type(field, DataType::Utf8View);
115 }
116 // Handle view type conversions
117 (&DataType::Utf8View, DataType::Utf8 | DataType::LargeUtf8) => {
118 return field_with_new_type(field, DataType::Utf8View);
119 }
120 (&DataType::BinaryView, DataType::Binary | DataType::LargeBinary) => {
121 return field_with_new_type(field, DataType::BinaryView);
122 }
123 _ => {}
124 }
125 }
126
127 // If no transformation is needed, keep the original field
128 Arc::clone(field)
129 })
130 .collect();
131
132 Some(Schema::new_with_metadata(
133 transformed_fields,
134 file_schema.metadata.clone(),
135 ))
136}
137
138/// Coerces the file schema's Timestamps to the provided TimeUnit if the
139/// Parquet schema contains INT96.
140///
141/// Deprecated wrapper around [`Int96Coercer`]; use the builder directly
142/// instead — it also supports attaching a timezone via
143/// [`Int96Coercer::with_timezone`].
144#[deprecated(since = "53.2.0", note = "use `Int96Coercer` instead")]
145pub fn coerce_int96_to_resolution(
146 parquet_schema: &SchemaDescriptor,
147 file_schema: &Schema,
148 time_unit: &TimeUnit,
149) -> Option<Schema> {
150 Int96Coercer::new(parquet_schema, file_schema, time_unit).coerce()
151}
152
153/// Builder for coercing INT96-originated Timestamp columns in `file_schema`
154/// to a specific [`TimeUnit`], optionally attaching a timezone.
155///
156/// INT96 is the legacy Parquet representation that systems like Spark use for
157/// timestamps. Arrow surfaces it as `Timestamp(Nanosecond, None)`, but the
158/// underlying values are written as UTC-adjusted instants. Use this builder
159/// to:
160///
161/// - Coerce INT96-derived columns to a smaller [`TimeUnit`] (e.g. microseconds)
162/// to extend the representable date range.
163/// - Optionally attach a timezone so the resulting Arrow type carries the
164/// timezone-aware semantic (`Timestamp(unit, Some(tz))`). Without a
165/// timezone, INT96-derived columns become `Timestamp(unit, None)` — the
166/// historical default.
167///
168/// Returns `None` if `file_schema` contains no INT96-derived columns.
169///
170/// # Example
171///
172/// ```ignore
173/// use std::sync::Arc;
174/// use arrow::datatypes::TimeUnit;
175/// use datafusion_datasource_parquet::Int96Coercer;
176///
177/// let coerced = Int96Coercer::new(parquet_schema, file_schema, &TimeUnit::Microsecond)
178/// .with_timezone(Some(Arc::from("UTC")))
179/// .coerce();
180/// ```
181pub struct Int96Coercer<'a> {
182 parquet_schema: &'a SchemaDescriptor,
183 file_schema: &'a Schema,
184 time_unit: &'a TimeUnit,
185 timezone: Option<Arc<str>>,
186}
187
188impl<'a> Int96Coercer<'a> {
189 /// Create a new builder. INT96-derived columns will coerce to
190 /// `Timestamp(time_unit, None)` unless [`Self::with_timezone`] is set.
191 pub fn new(
192 parquet_schema: &'a SchemaDescriptor,
193 file_schema: &'a Schema,
194 time_unit: &'a TimeUnit,
195 ) -> Self {
196 Self {
197 parquet_schema,
198 file_schema,
199 time_unit,
200 timezone: None,
201 }
202 }
203
204 /// Attach a timezone to INT96-derived columns. When `Some`, INT96-derived
205 /// columns coerce to `Timestamp(time_unit, Some(timezone))` instead of
206 /// the default `Timestamp(time_unit, None)`. Spark and other systems
207 /// write INT96 as UTC-adjusted instants, so callers that need the
208 /// resulting Arrow type to be timezone-aware should pass
209 /// `Some(Arc::from("UTC"))`.
210 pub fn with_timezone(mut self, timezone: Option<Arc<str>>) -> Self {
211 self.timezone = timezone;
212 self
213 }
214
215 /// Run the coercion, returning the rewritten schema or `None` if
216 /// `file_schema` contains no INT96-derived columns.
217 pub fn coerce(self) -> Option<Schema> {
218 let Self {
219 parquet_schema,
220 file_schema,
221 time_unit,
222 timezone,
223 } = self;
224 coerce_int96_to_resolution_impl(
225 parquet_schema,
226 file_schema,
227 time_unit,
228 timezone.as_ref(),
229 )
230 }
231}
232
233fn coerce_int96_to_resolution_impl(
234 parquet_schema: &SchemaDescriptor,
235 file_schema: &Schema,
236 time_unit: &TimeUnit,
237 timezone: Option<&Arc<str>>,
238) -> Option<Schema> {
239 // Traverse the parquet_schema columns looking for int96 physical types. If encountered, insert
240 // the field's full path into a set.
241 let int96_fields: HashSet<_> = parquet_schema
242 .columns()
243 .iter()
244 .filter(|f| f.physical_type() == Type::INT96)
245 .map(|f| f.path().string())
246 .collect();
247
248 if int96_fields.is_empty() {
249 // The schema doesn't contain any int96 fields, so skip the remaining logic.
250 return None;
251 }
252
253 // Do a DFS into the schema using a stack, looking for timestamp(nanos) fields that originated
254 // as int96 to coerce to the provided time_unit.
255
256 type NestedFields = Rc<RefCell<Vec<FieldRef>>>;
257 type StackContext<'a> = (
258 Vec<&'a str>, // The Parquet column path (e.g., "c0.list.element.c1") for the current field.
259 &'a FieldRef, // The current field to be processed.
260 NestedFields, // The parent's fields that this field will be (possibly) type-coerced and
261 // inserted into. All fields have a parent, so this is not an Option type.
262 Option<NestedFields>, // Nested types need to create their own vector of fields for their
263 // children. For primitive types this will remain None. For nested
264 // types it is None the first time they are processed. Then, we
265 // instantiate a vector for its children, push the field back onto the
266 // stack to be processed again, and DFS into its children. The next
267 // time we process the field, we know we have DFS'd into the children
268 // because this field is Some.
269 );
270
271 // This is our top-level fields from which we will construct our schema. We pass this into our
272 // initial stack context as the parent fields, and the DFS populates it.
273 let fields = Rc::new(RefCell::new(Vec::with_capacity(file_schema.fields.len())));
274
275 // TODO: It might be possible to only DFS into nested fields that we know contain an int96 if we
276 // use some sort of LPM data structure to check if we're currently DFS'ing nested types that are
277 // in a column path that contains an int96. That can be a future optimization for large schemas.
278 let transformed_schema = {
279 // Populate the stack with our top-level fields.
280 let mut stack: Vec<StackContext> = file_schema
281 .fields()
282 .iter()
283 .rev()
284 .map(|f| (vec![f.name().as_str()], f, Rc::clone(&fields), None))
285 .collect();
286
287 // Pop fields to DFS into until we have exhausted the stack.
288 while let Some((parquet_path, current_field, parent_fields, child_fields)) =
289 stack.pop()
290 {
291 match (current_field.data_type(), child_fields) {
292 (DataType::Struct(unprocessed_children), None) => {
293 // This is the first time popping off this struct. We don't yet know the
294 // correct types of its children (i.e., if they need coercing) so we create
295 // a vector for child_fields, push the struct node back onto the stack to be
296 // processed again (see below) after processing all its children.
297 let child_fields = Rc::new(RefCell::new(Vec::with_capacity(
298 unprocessed_children.len(),
299 )));
300 // Note that here we push the struct back onto the stack with its
301 // parent_fields in the same position, now with Some(child_fields).
302 stack.push((
303 parquet_path.clone(),
304 current_field,
305 parent_fields,
306 Some(Rc::clone(&child_fields)),
307 ));
308 // Push all the children in reverse to maintain original schema order due to
309 // stack processing.
310 for child in unprocessed_children.into_iter().rev() {
311 let mut child_path = parquet_path.clone();
312 // Build up a normalized path that we'll use as a key into the original
313 // int96_fields set above to test if this originated as int96.
314 child_path.push(".");
315 child_path.push(child.name());
316 // Note that here we push the field onto the stack using the struct's
317 // new child_fields vector as the field's parent_fields.
318 stack.push((child_path, child, Rc::clone(&child_fields), None));
319 }
320 }
321 (DataType::Struct(unprocessed_children), Some(processed_children)) => {
322 // This is the second time popping off this struct. The child_fields vector
323 // now contains each field that has been DFS'd into, and we can construct
324 // the resulting struct with correct child types.
325 let processed_children = processed_children.borrow();
326 assert_eq!(processed_children.len(), unprocessed_children.len());
327 let processed_struct = Field::new_struct(
328 current_field.name(),
329 processed_children.as_slice(),
330 current_field.is_nullable(),
331 );
332 parent_fields.borrow_mut().push(Arc::new(processed_struct));
333 }
334 (DataType::List(unprocessed_child), None) => {
335 // This is the first time popping off this list. See struct docs above.
336 let child_fields = Rc::new(RefCell::new(Vec::with_capacity(1)));
337 stack.push((
338 parquet_path.clone(),
339 current_field,
340 parent_fields,
341 Some(Rc::clone(&child_fields)),
342 ));
343 let mut child_path = parquet_path.clone();
344 // Spark uses a definition for arrays/lists that results in a group
345 // named "list" that is not maintained when parsing to Arrow. We just push
346 // this name into the path.
347 child_path.push(".list.");
348 child_path.push(unprocessed_child.name());
349 stack.push((
350 child_path.clone(),
351 unprocessed_child,
352 Rc::clone(&child_fields),
353 None,
354 ));
355 }
356 (DataType::List(_), Some(processed_children)) => {
357 // This is the second time popping off this list. See struct docs above.
358 let processed_children = processed_children.borrow();
359 assert_eq!(processed_children.len(), 1);
360 let processed_list = Field::new_list(
361 current_field.name(),
362 Arc::clone(&processed_children[0]),
363 current_field.is_nullable(),
364 );
365 parent_fields.borrow_mut().push(Arc::new(processed_list));
366 }
367 (DataType::Map(unprocessed_child, _), None) => {
368 // This is the first time popping off this map. See struct docs above.
369 let child_fields = Rc::new(RefCell::new(Vec::with_capacity(1)));
370 stack.push((
371 parquet_path.clone(),
372 current_field,
373 parent_fields,
374 Some(Rc::clone(&child_fields)),
375 ));
376 let mut child_path = parquet_path.clone();
377 child_path.push(".");
378 child_path.push(unprocessed_child.name());
379 stack.push((
380 child_path.clone(),
381 unprocessed_child,
382 Rc::clone(&child_fields),
383 None,
384 ));
385 }
386 (DataType::Map(_, sorted), Some(processed_children)) => {
387 // This is the second time popping off this map. See struct docs above.
388 let processed_children = processed_children.borrow();
389 assert_eq!(processed_children.len(), 1);
390 let processed_map = Field::new(
391 current_field.name(),
392 DataType::Map(Arc::clone(&processed_children[0]), *sorted),
393 current_field.is_nullable(),
394 );
395 parent_fields.borrow_mut().push(Arc::new(processed_map));
396 }
397 (DataType::Timestamp(TimeUnit::Nanosecond, None), None)
398 if int96_fields.contains(parquet_path.concat().as_str()) =>
399 // We found a timestamp(nanos) and it originated as int96. Coerce it to the correct
400 // time_unit, optionally attaching the requested timezone.
401 {
402 parent_fields.borrow_mut().push(field_with_new_type(
403 current_field,
404 DataType::Timestamp(*time_unit, timezone.cloned()),
405 ));
406 }
407 // Other types can be cloned as they are.
408 _ => parent_fields.borrow_mut().push(Arc::clone(current_field)),
409 }
410 }
411 assert_eq!(fields.borrow().len(), file_schema.fields.len());
412 Schema::new_with_metadata(
413 fields.borrow_mut().clone(),
414 file_schema.metadata.clone(),
415 )
416 };
417
418 Some(transformed_schema)
419}
420
421/// Create a new field with the specified data type, copying the other
422/// properties from the input field
423fn field_with_new_type(field: &FieldRef, new_type: DataType) -> FieldRef {
424 Arc::new(field.as_ref().clone().with_data_type(new_type))
425}
426
427/// Transform a schema to use view types for Utf8 and Binary
428///
429/// See [`ParquetFormat::force_view_types`](crate::file_format::ParquetFormat::force_view_types) for details
430pub fn transform_schema_to_view(schema: &Schema) -> Schema {
431 let transformed_fields: Vec<Arc<Field>> = schema
432 .fields
433 .iter()
434 .map(|field| match field.data_type() {
435 DataType::Utf8 | DataType::LargeUtf8 => {
436 field_with_new_type(field, DataType::Utf8View)
437 }
438 DataType::Binary | DataType::LargeBinary => {
439 field_with_new_type(field, DataType::BinaryView)
440 }
441 _ => Arc::clone(field),
442 })
443 .collect();
444 Schema::new_with_metadata(transformed_fields, schema.metadata.clone())
445}
446
447/// Transform a schema so that any binary types are strings
448pub fn transform_binary_to_string(schema: &Schema) -> Schema {
449 let transformed_fields: Vec<Arc<Field>> = schema
450 .fields
451 .iter()
452 .map(|field| match field.data_type() {
453 DataType::Binary => field_with_new_type(field, DataType::Utf8),
454 DataType::LargeBinary => field_with_new_type(field, DataType::LargeUtf8),
455 DataType::BinaryView => field_with_new_type(field, DataType::Utf8View),
456 _ => Arc::clone(field),
457 })
458 .collect();
459 Schema::new_with_metadata(transformed_fields, schema.metadata.clone())
460}
461#[cfg(test)]
462mod tests {
463 use parquet::arrow::parquet_to_arrow_schema;
464
465 use super::*;
466
467 use parquet::schema::parser::parse_message_type;
468
469 #[test]
470 fn coerce_int96_to_resolution_with_mixed_timestamps() {
471 // Unclear if Spark (or other writer) could generate a file with mixed timestamps like this,
472 // but we want to test the scenario just in case since it's at least a valid schema as far
473 // as the Parquet spec is concerned.
474 let spark_schema = "
475 message spark_schema {
476 optional int96 c0;
477 optional int64 c1 (TIMESTAMP(NANOS,true));
478 optional int64 c2 (TIMESTAMP(NANOS,false));
479 optional int64 c3 (TIMESTAMP(MILLIS,true));
480 optional int64 c4 (TIMESTAMP(MILLIS,false));
481 optional int64 c5 (TIMESTAMP(MICROS,true));
482 optional int64 c6 (TIMESTAMP(MICROS,false));
483 }
484 ";
485
486 let schema = parse_message_type(spark_schema).expect("should parse schema");
487 let descr = SchemaDescriptor::new(Arc::new(schema));
488
489 let arrow_schema = parquet_to_arrow_schema(&descr, None).unwrap();
490
491 let result = Int96Coercer::new(&descr, &arrow_schema, &TimeUnit::Microsecond)
492 .coerce()
493 .unwrap();
494
495 // Only the first field (c0) should be converted to a microsecond timestamp because it's the
496 // only timestamp that originated from an INT96.
497 let expected_schema = Schema::new(vec![
498 Field::new("c0", DataType::Timestamp(TimeUnit::Microsecond, None), true),
499 Field::new(
500 "c1",
501 DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())),
502 true,
503 ),
504 Field::new("c2", DataType::Timestamp(TimeUnit::Nanosecond, None), true),
505 Field::new(
506 "c3",
507 DataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
508 true,
509 ),
510 Field::new("c4", DataType::Timestamp(TimeUnit::Millisecond, None), true),
511 Field::new(
512 "c5",
513 DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
514 true,
515 ),
516 Field::new("c6", DataType::Timestamp(TimeUnit::Microsecond, None), true),
517 ]);
518
519 assert_eq!(result, expected_schema);
520 }
521
522 #[test]
523 fn coerce_int96_to_resolution_with_tz_applies_timezone() {
524 // Same input schema as `coerce_int96_to_resolution_with_mixed_timestamps`, but with a
525 // non-empty `timezone` argument. Only c0 (the INT96 column) should pick up the timezone;
526 // the other timestamp columns must keep whatever timezone they were declared with.
527 let spark_schema = "
528 message spark_schema {
529 optional int96 c0;
530 optional int64 c1 (TIMESTAMP(NANOS,true));
531 optional int64 c2 (TIMESTAMP(NANOS,false));
532 optional int64 c3 (TIMESTAMP(MILLIS,true));
533 optional int64 c4 (TIMESTAMP(MILLIS,false));
534 optional int64 c5 (TIMESTAMP(MICROS,true));
535 optional int64 c6 (TIMESTAMP(MICROS,false));
536 }
537 ";
538
539 let schema = parse_message_type(spark_schema).expect("should parse schema");
540 let descr = SchemaDescriptor::new(Arc::new(schema));
541
542 let arrow_schema = parquet_to_arrow_schema(&descr, None).unwrap();
543
544 let result = Int96Coercer::new(&descr, &arrow_schema, &TimeUnit::Microsecond)
545 .with_timezone(Some(Arc::from("UTC")))
546 .coerce()
547 .unwrap();
548
549 let expected_schema = Schema::new(vec![
550 Field::new(
551 "c0",
552 DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
553 true,
554 ),
555 Field::new(
556 "c1",
557 DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())),
558 true,
559 ),
560 Field::new("c2", DataType::Timestamp(TimeUnit::Nanosecond, None), true),
561 Field::new(
562 "c3",
563 DataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
564 true,
565 ),
566 Field::new("c4", DataType::Timestamp(TimeUnit::Millisecond, None), true),
567 Field::new(
568 "c5",
569 DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
570 true,
571 ),
572 Field::new("c6", DataType::Timestamp(TimeUnit::Microsecond, None), true),
573 ]);
574
575 assert_eq!(result, expected_schema);
576 }
577
578 #[test]
579 fn coerce_int96_to_resolution_with_nested_types() {
580 // This schema is derived from Comet's CometFuzzTestSuite ParquetGenerator only using int96
581 // primitive types with generateStruct, generateArray, and generateMap set to true, with one
582 // additional field added to c4's struct to make sure all fields in a struct get modified.
583 // https://github.com/apache/datafusion-comet/blob/main/spark/src/main/scala/org/apache/comet/testing/ParquetGenerator.scala
584 let spark_schema = "
585 message spark_schema {
586 optional int96 c0;
587 optional group c1 {
588 optional int96 c0;
589 }
590 optional group c2 {
591 optional group c0 (LIST) {
592 repeated group list {
593 optional int96 element;
594 }
595 }
596 }
597 optional group c3 (LIST) {
598 repeated group list {
599 optional int96 element;
600 }
601 }
602 optional group c4 (LIST) {
603 repeated group list {
604 optional group element {
605 optional int96 c0;
606 optional int96 c1;
607 }
608 }
609 }
610 optional group c5 (MAP) {
611 repeated group key_value {
612 required int96 key;
613 optional int96 value;
614 }
615 }
616 optional group c6 (LIST) {
617 repeated group list {
618 optional group element (MAP) {
619 repeated group key_value {
620 required int96 key;
621 optional int96 value;
622 }
623 }
624 }
625 }
626 }
627 ";
628
629 let schema = parse_message_type(spark_schema).expect("should parse schema");
630 let descr = SchemaDescriptor::new(Arc::new(schema));
631
632 let arrow_schema = parquet_to_arrow_schema(&descr, None).unwrap();
633
634 let result = Int96Coercer::new(&descr, &arrow_schema, &TimeUnit::Microsecond)
635 .coerce()
636 .unwrap();
637
638 let expected_schema = Schema::new(vec![
639 Field::new("c0", DataType::Timestamp(TimeUnit::Microsecond, None), true),
640 Field::new_struct(
641 "c1",
642 vec![Field::new(
643 "c0",
644 DataType::Timestamp(TimeUnit::Microsecond, None),
645 true,
646 )],
647 true,
648 ),
649 Field::new_struct(
650 "c2",
651 vec![Field::new_list(
652 "c0",
653 Field::new(
654 "element",
655 DataType::Timestamp(TimeUnit::Microsecond, None),
656 true,
657 ),
658 true,
659 )],
660 true,
661 ),
662 Field::new_list(
663 "c3",
664 Field::new(
665 "element",
666 DataType::Timestamp(TimeUnit::Microsecond, None),
667 true,
668 ),
669 true,
670 ),
671 Field::new_list(
672 "c4",
673 Field::new_struct(
674 "element",
675 vec![
676 Field::new(
677 "c0",
678 DataType::Timestamp(TimeUnit::Microsecond, None),
679 true,
680 ),
681 Field::new(
682 "c1",
683 DataType::Timestamp(TimeUnit::Microsecond, None),
684 true,
685 ),
686 ],
687 true,
688 ),
689 true,
690 ),
691 Field::new_map(
692 "c5",
693 "key_value",
694 Field::new(
695 "key",
696 DataType::Timestamp(TimeUnit::Microsecond, None),
697 false,
698 ),
699 Field::new(
700 "value",
701 DataType::Timestamp(TimeUnit::Microsecond, None),
702 true,
703 ),
704 false,
705 true,
706 ),
707 Field::new_list(
708 "c6",
709 Field::new_map(
710 "element",
711 "key_value",
712 Field::new(
713 "key",
714 DataType::Timestamp(TimeUnit::Microsecond, None),
715 false,
716 ),
717 Field::new(
718 "value",
719 DataType::Timestamp(TimeUnit::Microsecond, None),
720 true,
721 ),
722 false,
723 true,
724 ),
725 true,
726 ),
727 ]);
728
729 assert_eq!(result, expected_schema);
730 }
731}