Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
3821611
add total order enum variants
etseidl Mar 24, 2026
fca8c2e
clean up sort_order
etseidl Mar 24, 2026
032ded6
add nan counts from @Xuanwo
etseidl Mar 25, 2026
00d2786
mod tests for new float ordering
etseidl Mar 26, 2026
410f365
add more tests from @Xuanwo
etseidl Mar 26, 2026
e0f9d07
clippy and formatting
etseidl Mar 26, 2026
08b77cb
add test of mixed all-nan/some-nan/no-nan pages
etseidl Mar 26, 2026
0621be1
fix NaN updates across pages
etseidl Mar 26, 2026
2da596c
fix comment
etseidl Mar 27, 2026
008749d
must always write nan count, even if 0
etseidl Apr 3, 2026
88a8306
add row_group_nan_counts to StatisticsConverter
etseidl Apr 3, 2026
d57bb50
forgot nan counts in convert_stats()
etseidl Apr 3, 2026
e3cddc8
add nan counts for pages
etseidl Apr 3, 2026
87def04
actually write out the nan counts
etseidl Apr 3, 2026
6af5aa0
add interoperability test
etseidl Apr 3, 2026
5ad31fd
add a from scratch round trip test
etseidl Apr 4, 2026
5de1817
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl Apr 6, 2026
e43b0dc
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl May 19, 2026
fa7840e
add nan_count to test
etseidl May 19, 2026
d483171
fix test sizes
etseidl May 19, 2026
844a616
try a few micro-optimizations
etseidl May 21, 2026
d533f73
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl May 21, 2026
e09cce0
finish merge
etseidl May 21, 2026
30f50ca
fix another merge issue
etseidl May 21, 2026
396edf0
rework is_nan to avoid PartialEq::eq calls
etseidl May 21, 2026
13dc03b
rework compare_greater to avoid calls to logical_type_ref in inner loop
etseidl May 21, 2026
091ce4b
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl May 22, 2026
d7030b3
fix some merge leftovers
etseidl May 22, 2026
d0b93d4
compute nan_count with stats, saves a pass through the data
etseidl May 22, 2026
18b0222
rework compare_greater
etseidl May 22, 2026
0aae820
fix cut/paste error
etseidl May 23, 2026
68c52b3
update parquet-testing to get test file
etseidl May 26, 2026
afe7d40
add test for new file
etseidl May 26, 2026
c358922
suggestions from review
etseidl May 27, 2026
7f19b55
rework min/max handling to remove unwrap
etseidl May 27, 2026
f98c8b3
rework nan handling in update_column_offset_index
etseidl May 27, 2026
4ff46cc
fix get_nan_count...it should never return None for floats
etseidl May 27, 2026
d01374f
fix another i64->u64 conversion
etseidl May 28, 2026
570f6de
cut/paste error
etseidl May 28, 2026
2832bd8
clean up some comments
etseidl May 28, 2026
4ee0ad7
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl Jun 1, 2026
134a42b
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl Jun 9, 2026
b422a69
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl Jun 23, 2026
5f03523
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl Jul 13, 2026
e8b49e2
format
etseidl Jul 13, 2026
bf74a28
clippy
etseidl Jul 14, 2026
0d6c063
Merge remote-tracking branch 'origin/main' into total_order_514
etseidl Jul 28, 2026
30b29dc
deprecate sort_order_for_type and refactor a little
etseidl May 28, 2026
766cc97
format
etseidl May 28, 2026
c0eeaf1
update deprecation to 60.0.0
etseidl Jun 9, 2026
422102a
Merge remote-tracking branch 'origin/main' into deprecate_sort_order_…
etseidl Aug 3, 2026
8e4d78c
Update parquet/src/basic.rs
alamb Aug 4, 2026
81945fb
Merge branch 'main' into deprecate_sort_order_for_type
alamb Aug 4, 2026
3af23c2
rough draft
etseidl Jun 9, 2026
d1c2970
match name used in PR
etseidl Jun 9, 2026
c6e5270
add proper references to the format PR
etseidl Jun 9, 2026
4f51c2a
add test
etseidl Jun 10, 2026
6103f4f
update test to use file from parquet-testing
etseidl Jun 23, 2026
509428f
update parquet-testing
etseidl Jul 16, 2026
631526f
add doc link
etseidl Aug 4, 2026
6b5d992
refactor test
etseidl Aug 4, 2026
529b615
Merge remote-tracking branch 'origin/main' into int96_order
etseidl Aug 4, 2026
edc8933
refactor test a bit more
etseidl Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion parquet/src/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,11 @@ pub enum SortOrder {
UNDEFINED,
/// Use IEEE 754 total order.
TOTAL_ORDER,
/// Use INT96 timestamp order (see [parquet-format/#584] and the [Thrift spec]).
///
/// [parquet-format/#584]: https://github.com/apache/parquet-format/pull/584

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 631526f

/// [Thrift spec]: https://github.com/apache/parquet-format/blob/2076361bb64e2de9ca6a8d06eda025a6fa4e9df6/src/main/thrift/parquet.thrift#L1230-L1233
INT96_TIMESTAMP,
}

impl SortOrder {
Expand All @@ -1009,6 +1014,8 @@ pub enum ColumnOrder {
TYPE_DEFINED_ORDER(SortOrder),
/// Column ordering to use for floating point types.
IEEE_754_TOTAL_ORDER,
/// Column ordering to use for INT96 types.
INT96_TIMESTAMP_ORDER,
// The following are not defined in the Parquet spec and should always be last.
/// Undefined column order, means legacy behaviour before parquet-format 2.4.0.
/// Sort order is always SIGNED.
Expand Down Expand Up @@ -1043,6 +1050,8 @@ impl ColumnOrder {
|| matches!(physical_type, Type::FLOAT | Type::DOUBLE)
{
ColumnOrder::IEEE_754_TOTAL_ORDER
} else if matches!(physical_type, Type::INT96) {
ColumnOrder::INT96_TIMESTAMP_ORDER
} else {
let sort_order =
Self::get_sort_order_for_type(logical_type, converted_type, physical_type, true);
Expand Down Expand Up @@ -1161,7 +1170,13 @@ impl ColumnOrder {
// Order: false, true
Type::BOOLEAN => SortOrder::UNSIGNED,
Type::INT32 | Type::INT64 => SortOrder::SIGNED,
Type::INT96 => SortOrder::UNDEFINED,
Type::INT96 => {
if is_type_defined {
SortOrder::UNDEFINED
} else {
SortOrder::INT96_TIMESTAMP
}
}
// Notes to remember when comparing float/double values:
// If legacy TYPE_DEFINED_ORDER is specified:
// If the min is a NaN, it should be ignored.
Expand Down Expand Up @@ -1190,6 +1205,7 @@ impl ColumnOrder {
match *self {
ColumnOrder::TYPE_DEFINED_ORDER(order) => order,
ColumnOrder::IEEE_754_TOTAL_ORDER => SortOrder::TOTAL_ORDER,
ColumnOrder::INT96_TIMESTAMP_ORDER => SortOrder::INT96_TIMESTAMP,
ColumnOrder::UNDEFINED => SortOrder::SIGNED,
ColumnOrder::UNKNOWN => SortOrder::UNDEFINED,
}
Expand All @@ -1212,6 +1228,10 @@ impl<'a, R: ThriftCompactInputProtocol<'a>> ReadThrift<'a, R> for ColumnOrder {
prot.skip_empty_struct()?;
Self::IEEE_754_TOTAL_ORDER
}
3 => {
prot.skip_empty_struct()?;
Self::INT96_TIMESTAMP_ORDER
}
_ => {
prot.skip(field_ident.field_type)?;
Self::UNKNOWN
Expand Down Expand Up @@ -1240,6 +1260,10 @@ impl WriteThrift for ColumnOrder {
writer.write_field_begin(FieldType::Struct, 2, 0)?;
writer.write_struct_end()?;
}
Self::INT96_TIMESTAMP_ORDER => {
writer.write_field_begin(FieldType::Struct, 3, 0)?;
writer.write_struct_end()?;
}
_ => return Err(general_err!("Attempt to write undefined ColumnOrder")),
}
// write end of struct for this union
Expand Down Expand Up @@ -2040,6 +2064,7 @@ mod tests {
assert_eq!(SortOrder::UNSIGNED.to_string(), "UNSIGNED");
assert_eq!(SortOrder::UNDEFINED.to_string(), "UNDEFINED");
assert_eq!(SortOrder::TOTAL_ORDER.to_string(), "TOTAL_ORDER");
assert_eq!(SortOrder::INT96_TIMESTAMP.to_string(), "INT96_TIMESTAMP");
}

#[test]
Expand All @@ -2060,6 +2085,10 @@ mod tests {
ColumnOrder::IEEE_754_TOTAL_ORDER.to_string(),
"IEEE_754_TOTAL_ORDER"
);
assert_eq!(
ColumnOrder::INT96_TIMESTAMP_ORDER.to_string(),
"INT96_TIMESTAMP_ORDER"
);
assert_eq!(ColumnOrder::UNDEFINED.to_string(), "UNDEFINED");
}

Expand Down Expand Up @@ -2206,6 +2235,10 @@ mod tests {
ColumnOrder::get_default_sort_order(Type::INT96, true),
SortOrder::UNDEFINED
);
assert_eq!(
ColumnOrder::get_default_sort_order(Type::INT96, false),
SortOrder::INT96_TIMESTAMP
);
assert_eq!(
ColumnOrder::get_default_sort_order(Type::FLOAT, false),
SortOrder::TOTAL_ORDER
Expand Down Expand Up @@ -2250,6 +2283,10 @@ mod tests {
ColumnOrder::IEEE_754_TOTAL_ORDER.sort_order(),
SortOrder::TOTAL_ORDER
);
assert_eq!(
ColumnOrder::INT96_TIMESTAMP_ORDER.sort_order(),
SortOrder::INT96_TIMESTAMP
);
assert_eq!(ColumnOrder::UNDEFINED.sort_order(), SortOrder::SIGNED);
}

Expand Down
10 changes: 5 additions & 5 deletions parquet/src/data_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ impl PartialOrd for Int96 {
impl Ord for Int96 {
/// Order `Int96` correctly for (deprecated) timestamp types.
///
/// Note: this is done even though the Int96 type is deprecated and the
/// [spec does not define the sort order]
/// because some engines, notably Spark and Databricks Photon still write
/// Int96 timestamps and rely on their order for optimization.
/// Note: this is done even though the Int96 type is deprecated.
/// Because some engines, notably Spark and Databricks Photon, still write
/// Int96 timestamps, a new `ColumnOrder` variant has been added to
/// the Parquet specification. See [parquet-format/#584].
///
/// [spec does not define the sort order]: https://github.com/apache/parquet-format/blob/cf943c197f4fad826b14ba0c40eb0ffdab585285/src/main/thrift/parquet.thrift#L1079
/// [parquet-format/#584]: https://github.com/apache/parquet-format/pull/584
fn cmp(&self, other: &Self) -> Ordering {
match self.get_days().cmp(&other.get_days()) {
Ordering::Equal => self.get_nanos().cmp(&other.get_nanos()),
Expand Down
76 changes: 75 additions & 1 deletion parquet/src/file/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1160,7 +1160,7 @@ mod tests {
use crate::column::page::{Page, PageReader};
use crate::column::reader::get_typed_column_reader;
use crate::compression::{Codec, CodecOptionsBuilder, create_codec};
use crate::data_type::{BoolType, ByteArrayType, Int32Type};
use crate::data_type::{BoolType, ByteArrayType, Int32Type, Int96, Int96Type};
use crate::file::page_index::column_index::ColumnIndexMetaData;
use crate::file::properties::EnabledStatistics;
use crate::file::serialized_reader::ReadOptionsBuilder;
Expand Down Expand Up @@ -2674,4 +2674,78 @@ mod tests {
}
writer.close().unwrap();
}

#[test]
fn test_int96_interop() {
// this file has an INT96 column. rewrite it with min/max statistics sorted per
// recent changes to the spec. (see https://github.com/apache/parquet-format/pull/584)
let file = get_test_file("int96_timestamp_order.parquet");
let read_opts = ReadOptionsBuilder::new().with_page_index().build();
let reader = SerializedFileReader::new_with_options(file, read_opts).unwrap();
let file_metadata = reader.metadata().file_metadata();
let schema = file_metadata.schema_descr().root_schema_ptr();

// helper function to extract Int96 min/max from column metadata and the column index
fn retrieve_stats(metadata: &ParquetMetaData) -> (&[u8], &[u8], &Int96, &Int96) {
// sanity check that the proper column order is specified
let column_orders = metadata
.file_metadata()
.column_orders()
.expect("column_orders is missing");
assert_eq!(column_orders[0], ColumnOrder::INT96_TIMESTAMP_ORDER);

let stats = metadata
.row_group(0)
.column(0)
.statistics()
.expect("statistics missing");
let min = stats.min_bytes_opt().expect("min stats missing");
let max = stats.max_bytes_opt().expect("max stats missing");

let col_idx = metadata.column_index().expect("column index not present");
let col0 = match &col_idx[0][0] {
ColumnIndexMetaData::INT96(index) => index,
_ => panic!("expected INT96 stats"),
};
let col_min = col0.min_value(0).expect("ColumnIndex min not present");
let col_max = col0.max_value(0).expect("ColumnIndex max not present");

(min, max, col_min, col_max)
}

// save read stats for later
let (exp_min, exp_max, exp_col_min, exp_col_max) = retrieve_stats(reader.metadata());

// write file back out again
let props = Arc::new(WriterProperties::builder().build());
let output = Vec::<u8>::new();
let mut writer = SerializedFileWriter::new(output, schema, props).unwrap();

let mut rg_out = writer.next_row_group().unwrap();
let rg_in = reader.get_row_group(0).unwrap();

// int96 is column 0
let col_in = rg_in.get_column_reader(0).unwrap();
let mut typed_in = get_typed_column_reader::<Int96Type>(col_in);

let mut values = Vec::new();
typed_in.read_records(4, None, None, &mut values).unwrap();

let mut col_out = rg_out.next_column().unwrap().unwrap();
col_out
.typed::<Int96Type>()
.write_batch(&values, None, None)
.unwrap();
col_out.close().unwrap();
rg_out.close().unwrap();

let new_metadata = writer.close().unwrap();

// check that new stats match the original stats
let (new_min, new_max, new_col_min, new_col_max) = retrieve_stats(&new_metadata);
assert_eq!(new_min, exp_min);
assert_eq!(new_max, exp_max);
assert_eq!(new_col_min, exp_col_min);
assert_eq!(new_col_max, exp_col_max);
}
}
Loading