Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
100 changes: 99 additions & 1 deletion cpp/include/cudf/io/orc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#pragma once

#include <cudf/io/detail/orc.hpp>
#include <cudf/io/orc_types.hpp>
#include <cudf/io/types.hpp>
#include <cudf/table/table_view.hpp>
#include <cudf/types.hpp>
Expand Down Expand Up @@ -82,7 +83,8 @@ class orc_reader_options {
bool _use_index = true;

// Whether to use numpy-compatible dtypes
bool _use_np_dtypes = true;
bool _use_np_dtypes = true;
bool _convert_dates_to_proleptic = false;
// Cast timestamp columns to a specific type
data_type _timestamp_type{type_id::EMPTY};

Expand Down Expand Up @@ -167,6 +169,15 @@ class orc_reader_options {
*/
[[nodiscard]] bool is_enabled_use_np_dtypes() const { return _use_np_dtypes; }

/**
* @brief Whether explicitly marked hybrid DATE values are converted to proleptic Gregorian.
* @return Whether DATE conversion is enabled
*/
[[nodiscard]] bool is_enabled_convert_dates_to_proleptic() const
{
return _convert_dates_to_proleptic;
}

/**
* @brief Returns timestamp type to which timestamp column will be cast.
*
Expand Down Expand Up @@ -274,6 +285,17 @@ class orc_reader_options {
*/
void enable_use_np_dtypes(bool use) { _use_np_dtypes = use; }

/**
* @brief Convert DATE values from explicitly marked JULIAN_GREGORIAN files to proleptic
* Gregorian.
*
* Disabled by default. Only DATE columns, including nested ones, are converted. Dates with no
* proleptic Gregorian equivalent cause a read error. Conversion precedes numpy dtype promotion.
*
* @param convert Whether to convert explicitly marked hybrid DATE values
*/
void enable_convert_dates_to_proleptic(bool convert) { _convert_dates_to_proleptic = convert; }

/**
* @brief Sets timestamp type to which timestamp column will be cast.
*
Expand Down Expand Up @@ -400,6 +422,16 @@ class orc_reader_options_builder {
return *this;
}

/**
* @copydoc orc_reader_options::enable_convert_dates_to_proleptic
* @return this for chaining
*/
orc_reader_options_builder& convert_dates_to_proleptic(bool convert)
{
options.enable_convert_dates_to_proleptic(convert);
return *this;
}

/**
* @brief Sets timestamp type to which timestamp column will be cast.
*
Expand Down Expand Up @@ -664,6 +696,8 @@ class orc_writer_options {
bool _enable_dictionary_sort = true;
// Timezone that the written timestamps are relative to, recorded in the stripe footers
std::string _writer_timezone = "UTC";
// Calendar used to encode DATE values and recorded in the file footer
orc::CalendarKind _calendar_kind = orc::CalendarKind::PROLEPTIC_GREGORIAN;

friend orc_writer_options_builder;

Expand Down Expand Up @@ -800,6 +834,12 @@ class orc_writer_options {
*/
[[nodiscard]] std::string const& get_writer_timezone() const { return _writer_timezone; }

/**
* @brief Returns the calendar recorded in the file footer.
* @return Calendar kind
*/
[[nodiscard]] orc::CalendarKind get_calendar_kind() const { return _calendar_kind; }

// Setters

/**
Expand Down Expand Up @@ -933,6 +973,26 @@ class orc_writer_options {
* @param timezone Timezone name, for example "America/Los_Angeles"
*/
void set_writer_timezone(std::string timezone) { _writer_timezone = std::move(timezone); }

/**
* @brief Sets the calendar recorded in the ORC file footer.
*
* `PROLEPTIC_GREGORIAN` is the default and retains libcudf's DATE and TIMESTAMP values.
* Legacy mode rebases DATE values, including nested values, before encoding and computing
* statistics. TIMESTAMP columns, dates in 1582-10-05 through 1582-10-14, and dates outside
* the int32 hybrid domain are rejected. Use convert_dates_to_proleptic when reading legacy
* output back into libcudf.
*
* @param calendar_kind Calendar kind
* @throw cudf::logic_error if the calendar kind is unknown or invalid
*/
void set_calendar_kind(orc::CalendarKind calendar_kind)
{
CUDF_EXPECTS(calendar_kind == orc::CalendarKind::PROLEPTIC_GREGORIAN or
calendar_kind == orc::CalendarKind::JULIAN_GREGORIAN,
"Unsupported ORC calendar kind");
_calendar_kind = calendar_kind;
}
};

/**
Expand Down Expand Up @@ -1095,6 +1155,16 @@ class orc_writer_options_builder {
return *this;
}

/**
* @copydoc orc_writer_options::set_calendar_kind
* @return this for chaining
*/
orc_writer_options_builder& calendar_kind(orc::CalendarKind calendar_kind)
{
options.set_calendar_kind(calendar_kind);
return *this;
}

/**
* @brief move orc_writer_options member once it's built.
*/
Expand Down Expand Up @@ -1164,6 +1234,8 @@ class chunked_orc_writer_options {
bool _enable_dictionary_sort = true;
// Timezone that the written timestamps are relative to, recorded in the stripe footers
std::string _writer_timezone = "UTC";
// Calendar recorded in the file footer
orc::CalendarKind _calendar_kind = orc::CalendarKind::PROLEPTIC_GREGORIAN;

friend chunked_orc_writer_options_builder;

Expand Down Expand Up @@ -1278,6 +1350,11 @@ class chunked_orc_writer_options {
*/
[[nodiscard]] std::string const& get_writer_timezone() const { return _writer_timezone; }

/**
* @copydoc orc_writer_options::get_calendar_kind
*/
[[nodiscard]] orc::CalendarKind get_calendar_kind() const { return _calendar_kind; }

// Setters

/**
Expand Down Expand Up @@ -1385,6 +1462,17 @@ class chunked_orc_writer_options {
* @copydoc orc_writer_options::set_writer_timezone
*/
void set_writer_timezone(std::string timezone) { _writer_timezone = std::move(timezone); }

/**
* @copydoc orc_writer_options::set_calendar_kind
*/
void set_calendar_kind(orc::CalendarKind calendar_kind)
{
CUDF_EXPECTS(calendar_kind == orc::CalendarKind::PROLEPTIC_GREGORIAN or
calendar_kind == orc::CalendarKind::JULIAN_GREGORIAN,
"Unsupported ORC calendar kind");
_calendar_kind = calendar_kind;
}
};

/**
Expand Down Expand Up @@ -1533,6 +1621,16 @@ class chunked_orc_writer_options_builder {
return *this;
}

/**
* @copydoc chunked_orc_writer_options::set_calendar_kind
* @return this for chaining
*/
chunked_orc_writer_options_builder& calendar_kind(orc::CalendarKind calendar_kind)
{
options.set_calendar_kind(calendar_kind);
return *this;
}

/**
* @brief move chunked_orc_writer_options member once it's built.
*/
Expand Down
23 changes: 21 additions & 2 deletions cpp/include/cudf/io/orc_metadata.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,16 @@ class orc_metadata {
* @param schema ORC schema
* @param num_rows number of rows
* @param num_stripes number of stripes
* @param calendar_kind calendar recorded in the footer, or `std::nullopt` if absent
*/
orc_metadata(orc_schema schema, uint64_t num_rows, size_type num_stripes)
: _schema{std::move(schema)}, _num_rows{num_rows}, _num_stripes{num_stripes}
orc_metadata(orc_schema schema,
uint64_t num_rows,
size_type num_stripes,
std::optional<orc::CalendarKind> calendar_kind = std::nullopt)
: _schema{std::move(schema)},
_num_rows{num_rows},
_num_stripes{num_stripes},
_calendar_kind{calendar_kind}
{
}

Expand Down Expand Up @@ -356,10 +363,22 @@ class orc_metadata {
*/
[[nodiscard]] auto num_stripes() const { return _num_stripes; }

/**
* @brief Returns the calendar recorded in the ORC footer.
*
* A missing value means the writer did not specify a calendar. This accessor does not
* convert values. See `orc_reader_options::enable_convert_dates_to_proleptic` for DATE
* conversion.
*
* @return Calendar kind, or `std::nullopt` if the footer has no calendar field
*/
[[nodiscard]] auto calendar_kind() const { return _calendar_kind; }

private:
orc_schema _schema;
uint64_t _num_rows;
size_type _num_stripes;
std::optional<orc::CalendarKind> _calendar_kind;
};

/**
Expand Down
9 changes: 9 additions & 0 deletions cpp/include/cudf/io/orc_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ enum CompressionKind : uint8_t {
ZSTD = 5,
};

/**
* @brief Identifies the calendar used to encode dates and timestamps in an ORC file.
*/
enum class CalendarKind : uint32_t {
UNKNOWN_CALENDAR = 0, ///< Calendar is unspecified
JULIAN_GREGORIAN = 1, ///< Hybrid calendar with the October 1582 cutover
PROLEPTIC_GREGORIAN = 2, ///< Gregorian calendar extended to all dates
};

/**
* @brief Identifies a data type in an orc file
*/
Expand Down
3 changes: 2 additions & 1 deletion cpp/src/io/functions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,8 @@ orc_metadata read_orc_metadata(source_info const& src_info, cuda::stream_ref str

return {{make_orc_column_schema(footer.types, 0, "")},
footer.numberOfRows,
static_cast<size_type>(footer.stripes.size())};
static_cast<size_type>(footer.stripes.size()),
footer.calendar};
}

/**
Expand Down
73 changes: 73 additions & 0 deletions cpp/src/io/orc/calendar.cuh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include <cudf/types.hpp>

#include <cstdint>

namespace cudf::io::orc::detail {

// Epoch days of 1582-10-15. Earlier hybrid dates use the Julian calendar.
constexpr int32_t calendar_cutover = -141'427;

CUDF_HOST_DEVICE constexpr int64_t floor_div(int64_t value, int64_t divisor)
{
return value / divisor - (value % divisor < 0);
}

/**
* @brief Convert epoch days while preserving the civil year, month, and day.
*
* March-based 400-year Gregorian and 4-year Julian cycles keep intermediate values positive
* within each cycle, including for astronomical years zero and below. All arithmetic is 64 bit
* so that the complete int32 DATE domain can be checked before narrowing the result.
*/
template <bool to_hybrid>
CUDF_HOST_DEVICE constexpr int64_t rebase_days(int64_t days)

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.

The direction only shows up as a bool at the call sites (rebase_days<true> in the writer, rebase_days<false> in the reader), so you have to come here to find out what true means. Could we expose named wrappers like proleptic_to_hybrid_days / hybrid_to_proleptic_days, and do the same for is_representable_date?

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.

Added.

{
if (days >= calendar_cutover) { return days; }

constexpr int64_t gregorian_epoch_offset = 719'468;
constexpr int64_t julian_epoch_offset = 719'470;
constexpr int64_t gregorian_era_days = 146'097;
constexpr int64_t julian_era_days = 1'461;
constexpr int64_t gregorian_century_days = gregorian_era_days / 4;
constexpr int64_t five_month_days = 153;
constexpr auto source_era_days = to_hybrid ? gregorian_era_days : julian_era_days;
auto const z = days + (to_hybrid ? gregorian_epoch_offset : julian_epoch_offset);
auto const era = floor_div(z, source_era_days);
auto const doe = z - era * source_era_days;
// Discount leap days to recover the year within the source calendar's era.
auto const yoe = to_hybrid ? (doe - doe / (julian_era_days - 1) + doe / gregorian_century_days -
doe / (gregorian_era_days - 1)) /
365
: (doe == julian_era_days - 1 ? 3 : doe / 365);
auto const year = yoe + era * (to_hybrid ? 400 : 4);
auto const doy = doe - (365 * yoe + (to_hybrid ? yoe / 4 - yoe / 100 : 0));
// March starts the year so the leap day is last. Five-month blocks contain 153 days;
// mp maps March through February to 0 through 11, with doy measured from March 1.
auto const mp = (5 * doy + 2) / five_month_days;
auto const day = doy - (five_month_days * mp + 2) / 5 + 1;

if constexpr (to_hybrid) {
return 365 * year + floor_div(year, 4) + (five_month_days * mp + 2) / 5 + day - 1 -
julian_epoch_offset;
} else {
return 365 * year + floor_div(year, 4) - floor_div(year, 100) + floor_div(year, 400) +
(five_month_days * mp + 2) / 5 + day - 1 - gregorian_epoch_offset;
}
}

template <bool to_hybrid>
CUDF_HOST_DEVICE constexpr bool is_representable_date(int64_t original, int64_t rebased)
{
// The hybrid cutover gap and Julian-only leap days have no lossless counterpart.
return rebased >= INT32_MIN and rebased <= INT32_MAX and
rebase_days<!to_hybrid>(rebased) == original;
}

} // namespace cudf::io::orc::detail
32 changes: 32 additions & 0 deletions cpp/src/io/orc/decode_error.cuh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include "io/utilities/kernel_error.hpp"

#include <cuda/atomic>

namespace cudf::io::orc::detail {

enum class decode_error : kernel_error::value_type {
INVALID_DATA_STREAM = 1 << 0,
UNREPRESENTABLE_DATE = 1 << 1,
};

/**
* @brief Record a decoding failure without a shared error count wrapping back to zero.
*
* The reader checks for any error after synchronizing the decoding stream. Columns and
* stripes share the same status, so their combined number of failures is not row-count bounded.
*/
inline __device__ void set_decode_error(kernel_error::pointer status,
decode_error kind = decode_error::INVALID_DATA_STREAM)
{
cuda::atomic_ref<kernel_error::value_type, cuda::thread_scope_device> error{*status};
error.fetch_or(static_cast<kernel_error::value_type>(kind), cuda::std::memory_order_relaxed);
}

} // namespace cudf::io::orc::detail
4 changes: 3 additions & 1 deletion cpp/src/io/orc/orc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ void protobuf_reader::read(Footer& s, size_t maxlen)
field_reader(6, s.numberOfRows),
raw_field_reader(7, s.statistics),
field_reader(8, s.rowIndexStride),
field_reader(9, s.writer));
field_reader(9, s.writer),
field_reader(11, s.calendar));
function_builder(s, maxlen, op);
}

Expand Down Expand Up @@ -320,6 +321,7 @@ size_t protobuf_writer::write(Footer const& s)
w.field_repeated_struct_blob(7, s.statistics);
w.field_uint(8, s.rowIndexStride);
if (s.writer) w.field_uint(9, *s.writer);
if (s.calendar) w.field_uint(11, static_cast<uint32_t>(*s.calendar));
return w.value();
}

Expand Down
1 change: 1 addition & 0 deletions cpp/src/io/orc/orc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ struct Footer {
std::vector<col_stats_blob> statistics; // Column statistics blobs
uint32_t rowIndexStride = 0; // the maximum number of rows in each index entry
std::optional<uint32_t> writer; // Writer code
std::optional<CalendarKind> calendar; // Calendar used to encode dates and timestamps
};

struct Stream {
Expand Down
Loading
Loading