Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pola-rs
GitHub Repository: pola-rs/polars
Path: blob/main/crates/polars-parquet/src/arrow/read/statistics.rs
8492 views
1
//! APIs exposing `crate::parquet`'s statistics as arrow's statistics.
2
use arrow::array::{
3
Array, BinaryViewArray, BooleanArray, FixedSizeBinaryArray, MutableBinaryViewArray,
4
MutableBooleanArray, MutableFixedSizeBinaryArray, MutablePrimitiveArray, NullArray,
5
PrimitiveArray, Utf8ViewArray,
6
};
7
use arrow::datatypes::{ArrowDataType, Field, IntegerType, IntervalUnit, TimeUnit};
8
use arrow::types::{days_ms, i256};
9
use ethnum::I256;
10
use num_traits::{AsPrimitive, FromBytes};
11
use polars_utils::IdxSize;
12
use polars_utils::float16::pf16;
13
use polars_utils::pl_str::PlSmallStr;
14
15
use super::{ParquetTimeUnit, RowGroupMetadata};
16
use crate::parquet::error::{ParquetError, ParquetResult};
17
use crate::parquet::schema::types::PhysicalType as ParquetPhysicalType;
18
use crate::parquet::statistics::Statistics as ParquetStatistics;
19
use crate::read::{
20
ColumnChunkMetadata, PrimitiveLogicalType, convert_days_ms, convert_i128, convert_i256,
21
convert_year_month, int96_to_i64_ns,
22
};
23
24
/// Parquet statistics for a nesting level
25
#[derive(Debug, PartialEq)]
26
pub enum Statistics {
27
Column(Box<ColumnStatistics>),
28
29
List(Option<Box<Statistics>>),
30
FixedSizeList(Option<Box<Statistics>>, usize),
31
32
Struct(Box<[Option<Statistics>]>),
33
Dictionary(IntegerType, Option<Box<Statistics>>, bool),
34
}
35
36
/// Arrow-deserialized parquet statistics of a leaf-column
37
#[derive(Debug, PartialEq)]
38
pub struct ColumnStatistics {
39
field: Field,
40
41
logical_type: Option<PrimitiveLogicalType>,
42
physical_type: ParquetPhysicalType,
43
44
/// Statistics of the leaf array of the column
45
statistics: ParquetStatistics,
46
}
47
48
#[derive(Debug, PartialEq)]
49
pub enum ColumnPathSegment {
50
List { is_large: bool },
51
FixedSizeList { width: usize },
52
Dictionary { key: IntegerType, is_sorted: bool },
53
Struct { column_idx: usize },
54
}
55
56
/// Arrow-deserialized parquet statistics of a leaf-column
57
#[derive(Debug, PartialEq)]
58
pub struct ArrowColumnStatistics {
59
pub null_count: Option<u64>,
60
pub distinct_count: Option<u64>,
61
62
// While these two are Box<dyn Array>, they will only ever contain one valid value. This might
63
// seems dumb, and don't get me wrong it is, but arrow::Scalar is basically useless.
64
pub min_value: Option<Box<dyn Array>>,
65
pub max_value: Option<Box<dyn Array>>,
66
}
67
68
/// Arrow-deserialized parquet statistics of a leaf-column
69
pub struct ArrowColumnStatisticsArrays {
70
pub null_count: PrimitiveArray<IdxSize>,
71
pub distinct_count: PrimitiveArray<IdxSize>,
72
pub min_value: Box<dyn Array>,
73
pub max_value: Box<dyn Array>,
74
}
75
76
fn timestamp(logical_type: Option<&PrimitiveLogicalType>, time_unit: TimeUnit, x: i64) -> i64 {
77
let unit = if let Some(PrimitiveLogicalType::Timestamp { unit, .. }) = logical_type {
78
unit
79
} else {
80
return x;
81
};
82
83
match (unit, time_unit) {
84
(ParquetTimeUnit::Milliseconds, TimeUnit::Second) => x / 1_000,
85
(ParquetTimeUnit::Microseconds, TimeUnit::Second) => x / 1_000_000,
86
(ParquetTimeUnit::Nanoseconds, TimeUnit::Second) => x * 1_000_000_000,
87
88
(ParquetTimeUnit::Milliseconds, TimeUnit::Millisecond) => x,
89
(ParquetTimeUnit::Microseconds, TimeUnit::Millisecond) => x / 1_000,
90
(ParquetTimeUnit::Nanoseconds, TimeUnit::Millisecond) => x / 1_000_000,
91
92
(ParquetTimeUnit::Milliseconds, TimeUnit::Microsecond) => x * 1_000,
93
(ParquetTimeUnit::Microseconds, TimeUnit::Microsecond) => x,
94
(ParquetTimeUnit::Nanoseconds, TimeUnit::Microsecond) => x / 1_000,
95
96
(ParquetTimeUnit::Milliseconds, TimeUnit::Nanosecond) => x * 1_000_000,
97
(ParquetTimeUnit::Microseconds, TimeUnit::Nanosecond) => x * 1_000,
98
(ParquetTimeUnit::Nanoseconds, TimeUnit::Nanosecond) => x,
99
}
100
}
101
102
impl ColumnStatistics {
103
pub fn into_arrow(self) -> ParquetResult<ArrowColumnStatistics> {
104
use ParquetStatistics as S;
105
let (null_count, distinct_count) = match &self.statistics {
106
S::Binary(s) => (s.null_count, s.distinct_count),
107
S::Boolean(s) => (s.null_count, s.distinct_count),
108
S::FixedLen(s) => (s.null_count, s.distinct_count),
109
S::Int32(s) => (s.null_count, s.distinct_count),
110
S::Int64(s) => (s.null_count, s.distinct_count),
111
S::Int96(s) => (s.null_count, s.distinct_count),
112
S::Float(s) => (s.null_count, s.distinct_count),
113
S::Double(s) => (s.null_count, s.distinct_count),
114
};
115
116
let null_count = null_count.map(|v| v as u64);
117
let distinct_count = distinct_count.map(|v| v as u64);
118
119
macro_rules! rmap {
120
($expect:ident, $map:expr) => {{
121
let s = self.statistics.$expect();
122
123
let min = s.min_value;
124
let max = s.max_value;
125
126
let min = ($map)(min)?.map(|x| Box::new(x) as Box<dyn Array>);
127
let max = ($map)(max)?.map(|x| Box::new(x) as Box<dyn Array>);
128
129
(min, max)
130
}};
131
($expect:ident, @prim $from:ty $(as $to:ty)? $(, $map:expr)?) => {{
132
rmap!(
133
$expect,
134
|x: Option<$from>| {
135
$(
136
let x = x.map(|x| AsPrimitive::<$to>::as_(x));
137
)?
138
$(
139
let x = x.map($map);
140
)?
141
ParquetResult::Ok(x.map(|x| PrimitiveArray::$(<$to>::)?new(
142
self.field.dtype().clone(),
143
vec![x].into(),
144
None,
145
)))
146
}
147
)
148
}};
149
(@binary $(, $map:expr)?) => {{
150
rmap!(
151
expect_binary,
152
|x: Option<Vec<u8>>| {
153
$(
154
let x = x.map($map);
155
)?
156
ParquetResult::Ok(x.map(|x| BinaryViewArray::from_slice([Some(x)])))
157
}
158
)
159
}};
160
(@string) => {{
161
rmap!(
162
expect_binary,
163
|x: Option<Vec<u8>>| {
164
let x = x.map(String::from_utf8).transpose().map_err(|_| {
165
ParquetError::oos("Invalid UTF8 in Statistics")
166
})?;
167
ParquetResult::Ok(x.map(|x| Utf8ViewArray::from_slice([Some(x)])))
168
}
169
)
170
}};
171
}
172
173
use {ArrowDataType as D, ParquetPhysicalType as PPT};
174
let (min_value, max_value) = match (self.field.dtype(), &self.physical_type) {
175
(D::Null, _) => (None, None),
176
177
(D::Boolean, _) => rmap!(expect_boolean, |x: Option<bool>| ParquetResult::Ok(
178
x.map(|x| BooleanArray::new(ArrowDataType::Boolean, vec![x].into(), None,))
179
)),
180
181
(D::Int8, _) => rmap!(expect_int32, @prim i32 as i8),
182
(D::Int16, _) => rmap!(expect_int32, @prim i32 as i16),
183
(D::Int32 | D::Date32 | D::Time32(_), _) => rmap!(expect_int32, @prim i32 as i32),
184
185
// some implementations of parquet write arrow's date64 into i32.
186
(D::Date64, PPT::Int32) => rmap!(expect_int32, @prim i32 as i64, |x| x * 86400000),
187
188
(D::Int64 | D::Time64(_) | D::Duration(_), _) | (D::Date64, PPT::Int64) => {
189
rmap!(expect_int64, @prim i64 as i64)
190
},
191
192
(D::Interval(IntervalUnit::YearMonth), _) => rmap!(
193
expect_binary,
194
@prim Vec<u8>,
195
|x| convert_year_month(&x)
196
),
197
(D::Interval(IntervalUnit::DayTime), _) => rmap!(
198
expect_binary,
199
@prim Vec<u8>,
200
|x| convert_days_ms(&x)
201
),
202
203
(D::UInt8, _) => rmap!(expect_int32, @prim i32 as u8),
204
(D::UInt16, _) => rmap!(expect_int32, @prim i32 as u16),
205
(D::UInt32, PPT::Int32) => rmap!(expect_int32, @prim i32 as u32),
206
207
// some implementations of parquet write arrow's u32 into i64.
208
(D::UInt32, PPT::Int64) => rmap!(expect_int64, @prim i64 as u32),
209
(D::UInt64, _) => rmap!(expect_int64, @prim i64 as u64),
210
211
(D::Timestamp(time_unit, _), PPT::Int96) => {
212
rmap!(expect_int96, @prim [u32; 3], |x| {
213
timestamp(self.logical_type.as_ref(), *time_unit, int96_to_i64_ns(x))
214
})
215
},
216
(D::Timestamp(time_unit, _), PPT::Int64) => {
217
rmap!(expect_int64, @prim i64, |x| {
218
timestamp(self.logical_type.as_ref(), *time_unit, x)
219
})
220
},
221
222
(D::Float16, PPT::FixedLenByteArray(2))
223
if matches!(
224
self.logical_type.as_ref(),
225
Some(PrimitiveLogicalType::Float16)
226
) =>
227
{
228
rmap!(expect_fixedlen, @prim Vec<u8>, |v| pf16::from_le_bytes(&[v[0], v[1]]))
229
},
230
(D::Float32, _) => rmap!(expect_float, @prim f32),
231
(D::Float64, _) => rmap!(expect_double, @prim f64),
232
233
(D::Decimal(_, _), PPT::Int32) => rmap!(expect_int32, @prim i32 as i128),
234
(D::Decimal(_, _), PPT::Int64) => rmap!(expect_int64, @prim i64 as i128),
235
(D::Decimal(_, _), PPT::FixedLenByteArray(n)) if *n > 16 => {
236
return Err(ParquetError::not_supported(format!(
237
"Can't decode Decimal128 type from Fixed Size Byte Array of len {n:?}",
238
)));
239
},
240
(D::Decimal(_, _), PPT::FixedLenByteArray(n)) => rmap!(
241
expect_fixedlen,
242
@prim Vec<u8>,
243
|x| convert_i128(&x, *n)
244
),
245
(D::Decimal256(_, _), PPT::Int32) => {
246
rmap!(expect_int32, @prim i32, |x: i32| i256(I256::new(x.into())))
247
},
248
(D::Decimal256(_, _), PPT::Int64) => {
249
rmap!(expect_int64, @prim i64, |x: i64| i256(I256::new(x.into())))
250
},
251
(D::Decimal256(_, _), PPT::FixedLenByteArray(n)) if *n > 16 => {
252
return Err(ParquetError::not_supported(format!(
253
"Can't decode Decimal256 type from Fixed Size Byte Array of len {n:?}",
254
)));
255
},
256
(D::Decimal256(_, _), PPT::FixedLenByteArray(_)) => rmap!(
257
expect_fixedlen,
258
@prim Vec<u8>,
259
|x| convert_i256(&x)
260
),
261
(D::Binary, _) => rmap!(@binary),
262
(D::LargeBinary, _) => rmap!(@binary),
263
(D::Utf8, _) => rmap!(@string),
264
(D::LargeUtf8, _) => rmap!(@string),
265
266
(D::BinaryView, _) => rmap!(@binary),
267
(D::Utf8View, _) => rmap!(@string),
268
269
(D::FixedSizeBinary(_), _) => {
270
rmap!(expect_fixedlen, |x: Option<Vec<u8>>| ParquetResult::Ok(
271
x.map(|x| FixedSizeBinaryArray::new(
272
self.field.dtype().clone(),
273
x.into(),
274
None
275
))
276
))
277
},
278
279
other => todo!("{:?}", other),
280
};
281
282
Ok(ArrowColumnStatistics {
283
null_count,
284
distinct_count,
285
286
min_value,
287
max_value,
288
})
289
}
290
}
291
292
/// Deserializes the statistics in the column chunks from a single `row_group`
293
/// into [`Statistics`] associated from `field`'s name.
294
///
295
/// # Errors
296
/// This function errors if the deserialization of the statistics fails (e.g. invalid utf8)
297
pub fn deserialize_all(
298
field: &Field,
299
row_groups: &[RowGroupMetadata],
300
field_idx: usize,
301
) -> ParquetResult<Option<ArrowColumnStatisticsArrays>> {
302
assert!(!row_groups.is_empty());
303
use ArrowDataType as D;
304
match field.dtype() {
305
// @TODO: These are all a bit more complex, skip for now.
306
D::List(..) | D::LargeList(..) => Ok(None),
307
D::Dictionary(..) => Ok(None),
308
D::FixedSizeList(..) => Ok(None),
309
D::Struct(..) => Ok(None),
310
311
_ => {
312
let mut null_count = MutablePrimitiveArray::<IdxSize>::with_capacity(row_groups.len());
313
let mut distinct_count =
314
MutablePrimitiveArray::<IdxSize>::with_capacity(row_groups.len());
315
316
let primitive_type = &row_groups[0].parquet_columns()[field_idx]
317
.descriptor()
318
.descriptor
319
.primitive_type;
320
321
let logical_type = &primitive_type.logical_type;
322
let physical_type = &primitive_type.physical_type;
323
324
macro_rules! rmap {
325
($expect:ident, $map:expr, $arr:ty$(, $arg:expr)?) => {{
326
let mut min_arr = <$arr>::with_capacity(row_groups.len()$(, $arg)?);
327
let mut max_arr = <$arr>::with_capacity(row_groups.len()$(, $arg)?);
328
329
for rg in row_groups {
330
let column = &rg.parquet_columns()[field_idx];
331
let s = column.statistics().transpose()?;
332
333
let (v_min, v_max, v_null_count, v_distinct_count) = match s {
334
None => (None, None, None, None),
335
Some(s) => {
336
let s = s.$expect();
337
338
let min = s.min_value;
339
let max = s.max_value;
340
341
let min = ($map)(min)?;
342
let max = ($map)(max)?;
343
344
(
345
min,
346
max,
347
s.null_count.map(|v| v as IdxSize),
348
s.distinct_count.map(|v| v as IdxSize),
349
)
350
}
351
};
352
353
min_arr.push(v_min);
354
max_arr.push(v_max);
355
null_count.push(v_null_count);
356
distinct_count.push(v_distinct_count);
357
}
358
359
(min_arr.freeze().to_boxed(), max_arr.freeze().to_boxed())
360
}};
361
($expect:ident, $arr:ty, @prim $from:ty $(as $to:ty)? $(, $map:expr)?) => {{
362
rmap!(
363
$expect,
364
|x: Option<$from>| {
365
$(
366
let x = x.map(|x| AsPrimitive::<$to>::as_(x));
367
)?
368
$(
369
let x = x.map($map);
370
)?
371
ParquetResult::Ok(x)
372
},
373
$arr
374
)
375
}};
376
(@binary $(, $map:expr)?) => {{
377
rmap!(
378
expect_binary,
379
|x: Option<Vec<u8>>| {
380
$(
381
let x = x.map($map);
382
)?
383
ParquetResult::Ok(x)
384
},
385
MutableBinaryViewArray<[u8]>
386
)
387
}};
388
(@string) => {{
389
rmap!(
390
expect_binary,
391
|x: Option<Vec<u8>>| {
392
let x = x.map(String::from_utf8).transpose().map_err(|_| {
393
ParquetError::oos("Invalid UTF8 in Statistics")
394
})?;
395
ParquetResult::Ok(x)
396
},
397
MutableBinaryViewArray<str>
398
)
399
}};
400
}
401
402
use {ArrowDataType as D, ParquetPhysicalType as PPT};
403
let (min_value, max_value) = match (field.dtype(), physical_type) {
404
(D::Null, _) => (
405
NullArray::new(ArrowDataType::Null, row_groups.len()).to_boxed(),
406
NullArray::new(ArrowDataType::Null, row_groups.len()).to_boxed(),
407
),
408
409
(D::Boolean, _) => rmap!(
410
expect_boolean,
411
|x: Option<bool>| ParquetResult::Ok(x),
412
MutableBooleanArray
413
),
414
415
(D::Int8, _) => rmap!(expect_int32, MutablePrimitiveArray::<i8>, @prim i32 as i8),
416
(D::Int16, _) => {
417
rmap!(expect_int32, MutablePrimitiveArray::<i16>, @prim i32 as i16)
418
},
419
(D::Int32 | D::Date32 | D::Time32(_), _) => {
420
rmap!(expect_int32, MutablePrimitiveArray::<i32>, @prim i32 as i32)
421
},
422
423
// some implementations of parquet write arrow's date64 into i32.
424
(D::Date64, PPT::Int32) => {
425
rmap!(expect_int32, MutablePrimitiveArray::<i64>, @prim i32 as i64, |x| x * 86400000)
426
},
427
428
(D::Int64 | D::Time64(_) | D::Duration(_), _) | (D::Date64, PPT::Int64) => {
429
rmap!(expect_int64, MutablePrimitiveArray::<i64>, @prim i64 as i64)
430
},
431
432
(D::Interval(IntervalUnit::YearMonth), _) => rmap!(
433
expect_binary,
434
MutablePrimitiveArray::<i32>,
435
@prim Vec<u8>,
436
|x| convert_year_month(&x)
437
),
438
(D::Interval(IntervalUnit::DayTime), _) => rmap!(
439
expect_binary,
440
MutablePrimitiveArray::<days_ms>,
441
@prim Vec<u8>,
442
|x| convert_days_ms(&x)
443
),
444
445
(D::UInt8, _) => rmap!(expect_int32, MutablePrimitiveArray::<u8>, @prim i32 as u8),
446
(D::UInt16, _) => {
447
rmap!(expect_int32, MutablePrimitiveArray::<u16>, @prim i32 as u16)
448
},
449
(D::UInt32, PPT::Int32) => {
450
rmap!(expect_int32, MutablePrimitiveArray::<u32>, @prim i32 as u32)
451
},
452
453
// some implementations of parquet write arrow's u32 into i64.
454
(D::UInt32, PPT::Int64) => {
455
rmap!(expect_int64, MutablePrimitiveArray::<u32>, @prim i64 as u32)
456
},
457
(D::UInt64, _) => {
458
rmap!(expect_int64, MutablePrimitiveArray::<u64>, @prim i64 as u64)
459
},
460
461
(D::Timestamp(time_unit, _), PPT::Int96) => {
462
rmap!(expect_int96, MutablePrimitiveArray::<i64>, @prim [u32; 3], |x| {
463
timestamp(logical_type.as_ref(), *time_unit, int96_to_i64_ns(x))
464
})
465
},
466
(D::Timestamp(time_unit, _), PPT::Int64) => {
467
rmap!(expect_int64, MutablePrimitiveArray::<i64>, @prim i64, |x| {
468
timestamp(logical_type.as_ref(), *time_unit, x)
469
})
470
},
471
472
(D::Float16, _) => {
473
rmap!(expect_fixedlen, MutablePrimitiveArray::<pf16>, @prim Vec<u8>, |v| {
474
let le_bytes: [u8; 2] = [v[0], v[1]];
475
pf16::from_le_bytes(&le_bytes)
476
})
477
},
478
(D::Float32, _) => rmap!(expect_float, MutablePrimitiveArray::<f32>, @prim f32),
479
(D::Float64, _) => rmap!(expect_double, MutablePrimitiveArray::<f64>, @prim f64),
480
481
(D::Decimal(_, _), PPT::Int32) => {
482
rmap!(expect_int32, MutablePrimitiveArray::<i128>, @prim i32 as i128)
483
},
484
(D::Decimal(_, _), PPT::Int64) => {
485
rmap!(expect_int64, MutablePrimitiveArray::<i128>, @prim i64 as i128)
486
},
487
(D::Decimal(_, _), PPT::FixedLenByteArray(n)) if *n > 16 => {
488
return Err(ParquetError::not_supported(format!(
489
"Can't decode Decimal128 type from Fixed Size Byte Array of len {n:?}",
490
)));
491
},
492
(D::Decimal(_, _), PPT::FixedLenByteArray(n)) => rmap!(
493
expect_fixedlen,
494
MutablePrimitiveArray::<i128>,
495
@prim Vec<u8>,
496
|x| convert_i128(&x, *n)
497
),
498
(D::Decimal256(_, _), PPT::Int32) => {
499
rmap!(expect_int32, MutablePrimitiveArray::<i256>, @prim i32, |x: i32| i256(I256::new(x.into())))
500
},
501
(D::Decimal256(_, _), PPT::Int64) => {
502
rmap!(expect_int64, MutablePrimitiveArray::<i256>, @prim i64, |x: i64| i256(I256::new(x.into())))
503
},
504
(D::Decimal256(_, _), PPT::FixedLenByteArray(n)) if *n > 16 => {
505
return Err(ParquetError::not_supported(format!(
506
"Can't decode Decimal256 type from Fixed Size Byte Array of len {n:?}",
507
)));
508
},
509
(D::Decimal256(_, _), PPT::FixedLenByteArray(_)) => rmap!(
510
expect_fixedlen,
511
MutablePrimitiveArray::<i256>,
512
@prim Vec<u8>,
513
|x| convert_i256(&x)
514
),
515
(D::Binary, _) => rmap!(@binary),
516
(D::LargeBinary, _) => rmap!(@binary),
517
(D::Utf8, _) => rmap!(@string),
518
(D::LargeUtf8, _) => rmap!(@string),
519
520
(D::BinaryView, _) => rmap!(@binary),
521
(D::Utf8View, _) => rmap!(@string),
522
523
(D::FixedSizeBinary(width), _) => {
524
rmap!(
525
expect_fixedlen,
526
|x: Option<Vec<u8>>| ParquetResult::Ok(x),
527
MutableFixedSizeBinaryArray,
528
*width
529
)
530
},
531
532
other => todo!("{:?}", other),
533
};
534
535
Ok(Some(ArrowColumnStatisticsArrays {
536
null_count: null_count.freeze(),
537
distinct_count: distinct_count.freeze(),
538
min_value,
539
max_value,
540
}))
541
},
542
}
543
}
544
545
/// Deserializes the statistics in the column chunks from a single `row_group`
546
/// into [`Statistics`] associated from `field`'s name.
547
///
548
/// # Errors
549
/// This function errors if the deserialization of the statistics fails (e.g. invalid utf8)
550
pub fn deserialize<'a>(
551
field: &Field,
552
columns: &mut impl ExactSizeIterator<Item = &'a ColumnChunkMetadata>,
553
) -> ParquetResult<Option<Statistics>> {
554
use ArrowDataType as D;
555
match field.dtype() {
556
D::List(field) | D::LargeList(field) => Ok(Some(Statistics::List(
557
deserialize(field.as_ref(), columns)?.map(Box::new),
558
))),
559
D::Dictionary(key, dtype, is_sorted) => Ok(Some(Statistics::Dictionary(
560
*key,
561
deserialize(
562
&Field::new(PlSmallStr::EMPTY, dtype.as_ref().clone(), true),
563
columns,
564
)?
565
.map(Box::new),
566
*is_sorted,
567
))),
568
D::FixedSizeList(field, width) => Ok(Some(Statistics::FixedSizeList(
569
deserialize(field.as_ref(), columns)?.map(Box::new),
570
*width,
571
))),
572
D::Struct(fields) => {
573
let field_columns = fields
574
.iter()
575
.map(|f| deserialize(f, columns))
576
.collect::<ParquetResult<_>>()?;
577
Ok(Some(Statistics::Struct(field_columns)))
578
},
579
_ => {
580
let column = columns.next().unwrap();
581
582
Ok(column.statistics().transpose()?.map(|statistics| {
583
let primitive_type = &column.descriptor().descriptor.primitive_type;
584
585
Statistics::Column(Box::new(ColumnStatistics {
586
field: field.clone(),
587
588
logical_type: primitive_type.logical_type,
589
physical_type: primitive_type.physical_type,
590
591
statistics,
592
}))
593
}))
594
},
595
}
596
}
597
598