mbutrovich commented on code in PR #3574:
URL: https://github.com/apache/iceberg-rust/pull/3574#discussion_r4419570383
##########
crates/iceberg/src/spec/values/datum.rs:
##########
@@ -1312,3 +1306,104 @@ fn human_float<T: Float + LowerExp + FromStr>(value: T)
-> String {
format!("{sign}{}.{fraction}E{exponent}", &digits[..1])
}
}
+
+const MICROS_PER_DAY: i64 = 24 * 60 * 60 * 1_000_000;
+const NANOS_PER_MICRO: i64 = 1000;
+const NANOS_PER_DAY: i64 = NANOS_PER_MICRO * MICROS_PER_DAY;
+
+/// Converts an int literal between two [PrimitiveType]s.
+fn convert_int(
+ val: i32,
+ source_type: &PrimitiveType,
+ target_type: &PrimitiveType,
+) -> Result<Datum> {
+ let datum = match (source_type, target_type) {
+ (_, PrimitiveType::Int) => Datum::int(val),
+ (_, PrimitiveType::Date) => Datum::date(val),
+ (_, PrimitiveType::Long) => Datum::long(val),
+ (PrimitiveType::Date, PrimitiveType::Timestamp) =>
Datum::timestamp_micros(
+ (val as i64)
+ .checked_mul(MICROS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::Timestamptz) =>
Datum::timestamptz_micros(
+ (val as i64)
+ .checked_mul(MICROS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::TimestampNs) =>
Datum::timestamp_nanos(
+ (val as i64)
+ .checked_mul(NANOS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::TimestamptzNs) =>
Datum::timestamptz_nanos(
+ (val as i64)
+ .checked_mul(NANOS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ _ => {
+ return Err(invalid_data!(
+ "Can't convert datum from {source_type} type to {target_type}
type."
+ ));
+ }
+ };
+
+ Ok(datum)
+}
+
+/// Converts a long literal between two [PrimitiveType]s.
+fn convert_long(
+ val: i64,
+ source_type: &PrimitiveType,
+ target_type: &PrimitiveType,
+) -> Result<Datum> {
+ let datum = match (source_type, target_type) {
+ (_, PrimitiveType::Int) => Datum::i64_to_i32(val),
+ (_, PrimitiveType::Long) => Datum::long(val),
Review Comment:
Could this arm go? `(_, PrimitiveType::Long)` is new for non-`long` sources,
so `Datum::timestamp_micros(5).to(Long)` and
`Datum::timestamp_nanos(5).to(Long)` both return `Ok(Datum::long(5))` on the
head commit. On `main` they return an error. The result depends on the unit of
the source type, and `long` to `long` is already handled by the `self_type ==
target_type` arm at line 1135.
##########
crates/iceberg/src/spec/values/datum.rs:
##########
@@ -1312,3 +1306,104 @@ fn human_float<T: Float + LowerExp + FromStr>(value: T)
-> String {
format!("{sign}{}.{fraction}E{exponent}", &digits[..1])
}
}
+
+const MICROS_PER_DAY: i64 = 24 * 60 * 60 * 1_000_000;
+const NANOS_PER_MICRO: i64 = 1000;
+const NANOS_PER_DAY: i64 = NANOS_PER_MICRO * MICROS_PER_DAY;
+
+/// Converts an int literal between two [PrimitiveType]s.
+fn convert_int(
+ val: i32,
+ source_type: &PrimitiveType,
+ target_type: &PrimitiveType,
+) -> Result<Datum> {
+ let datum = match (source_type, target_type) {
+ (_, PrimitiveType::Int) => Datum::int(val),
+ (_, PrimitiveType::Date) => Datum::date(val),
+ (_, PrimitiveType::Long) => Datum::long(val),
+ (PrimitiveType::Date, PrimitiveType::Timestamp) =>
Datum::timestamp_micros(
+ (val as i64)
+ .checked_mul(MICROS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::Timestamptz) =>
Datum::timestamptz_micros(
+ (val as i64)
+ .checked_mul(MICROS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::TimestampNs) =>
Datum::timestamp_nanos(
+ (val as i64)
+ .checked_mul(NANOS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::TimestamptzNs) =>
Datum::timestamptz_nanos(
+ (val as i64)
+ .checked_mul(NANOS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ _ => {
+ return Err(invalid_data!(
+ "Can't convert datum from {source_type} type to {target_type}
type."
+ ));
+ }
+ };
+
+ Ok(datum)
+}
+
+/// Converts a long literal between two [PrimitiveType]s.
+fn convert_long(
+ val: i64,
+ source_type: &PrimitiveType,
+ target_type: &PrimitiveType,
+) -> Result<Datum> {
+ let datum = match (source_type, target_type) {
+ (_, PrimitiveType::Int) => Datum::i64_to_i32(val),
+ (_, PrimitiveType::Long) => Datum::long(val),
+ (
+ PrimitiveType::Long | PrimitiveType::Timestamp |
PrimitiveType::Timestamptz,
+ PrimitiveType::Timestamp,
+ ) => Datum::timestamp_micros(val),
+ (
+ PrimitiveType::Long | PrimitiveType::Timestamp |
PrimitiveType::Timestamptz,
+ PrimitiveType::Timestamptz,
+ ) => Datum::timestamptz_micros(val),
+ (
+ PrimitiveType::Long | PrimitiveType::TimestampNs |
PrimitiveType::TimestamptzNs,
+ PrimitiveType::TimestampNs,
+ ) => Datum::timestamp_nanos(val),
+ (
+ PrimitiveType::Long | PrimitiveType::TimestampNs |
PrimitiveType::TimestamptzNs,
+ PrimitiveType::TimestamptzNs,
+ ) => Datum::timestamptz_nanos(val),
+ (PrimitiveType::TimestampNs | PrimitiveType::TimestamptzNs,
PrimitiveType::Timestamp) => {
+ Datum::timestamp_micros(val.div_euclid(NANOS_PER_MICRO))
+ }
+ (PrimitiveType::TimestampNs | PrimitiveType::TimestamptzNs,
PrimitiveType::Timestamptz) => {
+ Datum::timestamptz_micros(val.div_euclid(NANOS_PER_MICRO))
+ }
+ (PrimitiveType::Timestamp | PrimitiveType::Timestamptz,
PrimitiveType::TimestampNs) => {
+
Datum::timestamp_nanos(val.checked_mul(NANOS_PER_MICRO).ok_or_else(overflow)?)
+ }
+ (PrimitiveType::Timestamp | PrimitiveType::Timestamptz,
PrimitiveType::TimestamptzNs) => {
+
Datum::timestamptz_nanos(val.checked_mul(NANOS_PER_MICRO).ok_or_else(overflow)?)
+ }
+ (PrimitiveType::Timestamp | PrimitiveType::Timestamptz,
PrimitiveType::Date) => {
+ Datum::date(val.div_euclid(MICROS_PER_DAY) as i32)
+ }
+ (PrimitiveType::TimestampNs | PrimitiveType::TimestamptzNs,
PrimitiveType::Date) => {
+ Datum::date(val.div_euclid(NANOS_PER_DAY) as i32)
+ }
Review Comment:
Should these narrowing arms (`timestamp_ns` to `timestamp`, and any
timestamp to `date`) return an error instead? `Datum::to` is only called from
predicate binding
([predicate.rs#L419](https://github.com/apache/iceberg-rust/blob/a321be51a9714fccc2efacb704482f4262d6e368/crates/iceberg/src/expr/predicate.rs#L419)
and
[#L462](https://github.com/apache/iceberg-rust/blob/a321be51a9714fccc2efacb704482f4262d6e368/crates/iceberg/src/expr/predicate.rs#L462)),
and flooring the literal changes the predicate. On the head commit, binding
`Reference::new("d").less_than(Datum::timestamp_micros(1_672_920_000_000_000))`
(2023-01-05 12:00) to a `date` column gives `d < 2023-01-05` (`Int(19362)`). A
row with `d = 2023-01-05` matches the original predicate, because midnight is
before noon, but not the bound one. The same holds for `ts < 1500ns` bound to a
`timestamp` column, which becomes `ts < 1us` and drops `ts = 1us`. The
`datafusion-iceberg` description for apache/datafusion-iceberg#91 describes
the same result for this query.
The [expressions
spec](https://github.com/apache/iceberg/blob/c71e7dfca17633ae2bce15c486278e8e07629cea/format/expressions-spec.md?plain=1#L127)
covers this case:
> If the types cannot be aligned according to type promotion rules (for
instance, `"goats" > -Infinity`), the predicate cannot be evaluated and
implementations must fail.
and
[L98](https://github.com/apache/iceberg/blob/c71e7dfca17633ae2bce15c486278e8e07629cea/format/expressions-spec.md?plain=1#L98)
says "implementations must fail rather than insert unsafe casts". Timestamp to
date isn't a promotion. The aligned comparison would promote the date column to
a timestamp, which binding can't express because it converts the literal to the
column type. Java floors here
([Literals.java#L445-L455](https://github.com/apache/iceberg/blob/c71e7dfca17633ae2bce15c486278e8e07629cea/api/src/main/java/org/apache/iceberg/expressions/Literals.java#L445-L455),
[#L471-L480](https://github.com/apache/iceberg/blob/c71e7dfca17633ae2bce15c486278e8e07629cea/api/src/main/java/org/apache/iceberg/expressions/Literals.java#L471-L480)),
which as I read it contradicts the spec.
Returning the error here also fixes the `main` bug, so the user-visible
change becomes an error instead of a wrong answer. Once
apache/datafusion-iceberg#91 lands, the query in the description no longer
pushes the cast down, so it doesn't reach this code. Is there another caller
that needs the timestamp to date conversion?
##########
crates/iceberg/src/spec/values/datum.rs:
##########
@@ -1312,3 +1306,104 @@ fn human_float<T: Float + LowerExp + FromStr>(value: T)
-> String {
format!("{sign}{}.{fraction}E{exponent}", &digits[..1])
}
}
+
+const MICROS_PER_DAY: i64 = 24 * 60 * 60 * 1_000_000;
+const NANOS_PER_MICRO: i64 = 1000;
+const NANOS_PER_DAY: i64 = NANOS_PER_MICRO * MICROS_PER_DAY;
+
+/// Converts an int literal between two [PrimitiveType]s.
+fn convert_int(
+ val: i32,
+ source_type: &PrimitiveType,
+ target_type: &PrimitiveType,
+) -> Result<Datum> {
+ let datum = match (source_type, target_type) {
+ (_, PrimitiveType::Int) => Datum::int(val),
+ (_, PrimitiveType::Date) => Datum::date(val),
+ (_, PrimitiveType::Long) => Datum::long(val),
+ (PrimitiveType::Date, PrimitiveType::Timestamp) =>
Datum::timestamp_micros(
+ (val as i64)
+ .checked_mul(MICROS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::Timestamptz) =>
Datum::timestamptz_micros(
+ (val as i64)
+ .checked_mul(MICROS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::TimestampNs) =>
Datum::timestamp_nanos(
+ (val as i64)
+ .checked_mul(NANOS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::TimestamptzNs) =>
Datum::timestamptz_nanos(
+ (val as i64)
+ .checked_mul(NANOS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ _ => {
+ return Err(invalid_data!(
+ "Can't convert datum from {source_type} type to {target_type}
type."
+ ));
+ }
+ };
+
+ Ok(datum)
+}
+
+/// Converts a long literal between two [PrimitiveType]s.
+fn convert_long(
+ val: i64,
+ source_type: &PrimitiveType,
+ target_type: &PrimitiveType,
+) -> Result<Datum> {
+ let datum = match (source_type, target_type) {
+ (_, PrimitiveType::Int) => Datum::i64_to_i32(val),
+ (_, PrimitiveType::Long) => Datum::long(val),
+ (
+ PrimitiveType::Long | PrimitiveType::Timestamp |
PrimitiveType::Timestamptz,
+ PrimitiveType::Timestamp,
+ ) => Datum::timestamp_micros(val),
+ (
+ PrimitiveType::Long | PrimitiveType::Timestamp |
PrimitiveType::Timestamptz,
+ PrimitiveType::Timestamptz,
+ ) => Datum::timestamptz_micros(val),
+ (
+ PrimitiveType::Long | PrimitiveType::TimestampNs |
PrimitiveType::TimestamptzNs,
+ PrimitiveType::TimestampNs,
+ ) => Datum::timestamp_nanos(val),
+ (
+ PrimitiveType::Long | PrimitiveType::TimestampNs |
PrimitiveType::TimestamptzNs,
+ PrimitiveType::TimestamptzNs,
+ ) => Datum::timestamptz_nanos(val),
Review Comment:
Should a `long` literal bound to a `timestamp_ns` column return an error
instead? With these arms, `Datum::long(1).to(TimestampNs)` returns 1 ns. Java
reads the long as micros and returns 1000 ns
([Literals.java#L309-L311](https://github.com/apache/iceberg/blob/c71e7dfca17633ae2bce15c486278e8e07629cea/api/src/main/java/org/apache/iceberg/expressions/Literals.java#L309-L311)).
`long` isn't promotable to any timestamp type, so as I read the spec the error
is the answer (see the comment on lines 1379-1396). If the arm stays, I'd
follow Java's unit.
The comment at line 1148 ("Let's wait with nano's until this clears up",
pointing at apache/iceberg#11775) is stale either way. That PR was closed, and
this PR adds the nano conversions. Could we remove it?
##########
crates/iceberg/src/spec/values/datum.rs:
##########
@@ -1312,3 +1306,104 @@ fn human_float<T: Float + LowerExp + FromStr>(value: T)
-> String {
format!("{sign}{}.{fraction}E{exponent}", &digits[..1])
}
}
+
+const MICROS_PER_DAY: i64 = 24 * 60 * 60 * 1_000_000;
+const NANOS_PER_MICRO: i64 = 1000;
+const NANOS_PER_DAY: i64 = NANOS_PER_MICRO * MICROS_PER_DAY;
+
+/// Converts an int literal between two [PrimitiveType]s.
+fn convert_int(
+ val: i32,
+ source_type: &PrimitiveType,
+ target_type: &PrimitiveType,
+) -> Result<Datum> {
+ let datum = match (source_type, target_type) {
+ (_, PrimitiveType::Int) => Datum::int(val),
+ (_, PrimitiveType::Date) => Datum::date(val),
+ (_, PrimitiveType::Long) => Datum::long(val),
+ (PrimitiveType::Date, PrimitiveType::Timestamp) =>
Datum::timestamp_micros(
+ (val as i64)
+ .checked_mul(MICROS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::Timestamptz) =>
Datum::timestamptz_micros(
+ (val as i64)
+ .checked_mul(MICROS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::TimestampNs) =>
Datum::timestamp_nanos(
+ (val as i64)
+ .checked_mul(NANOS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
+ (PrimitiveType::Date, PrimitiveType::TimestamptzNs) =>
Datum::timestamptz_nanos(
+ (val as i64)
+ .checked_mul(NANOS_PER_DAY)
+ .ok_or_else(overflow)?,
+ ),
Review Comment:
Should date to `timestamptz` and `timestamptz_ns` stay unsupported? The
[type promotion
table](https://github.com/apache/iceberg/blob/c71e7dfca17633ae2bce15c486278e8e07629cea/format/spec.md?plain=1#L396-L404)
allows `date` to `timestamp` and `timestamp_ns`, and says "Promotion to
`timestamptz` or `timestamptz_ns` is **not** allowed". These arms read the date
as midnight UTC, but a SQL engine reads `DATE '2023-01-05'` compared with a
`timestamptz` as midnight in the session time zone. I'd keep the two
`timestamp` arms and drop the two `timestamptz` arms.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]