fresh-borzoni commented on code in PR #214: URL: https://github.com/apache/fluss-rust/pull/214#discussion_r2726191296
########## crates/fluss/src/util/partition.rs: ########## @@ -0,0 +1,678 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Utils for partition. + +#![allow(dead_code)] + +use crate::error::Error::IllegalArgument; +use crate::error::{Error, Result}; +use crate::metadata::{DataType, PartitionSpec, ResolvedPartitionSpec, TablePath}; +use crate::row::{Date, Datum, Time, TimestampLtz, TimestampNtz}; +use jiff::ToSpan; +use jiff::Zoned; +use jiff::civil::DateTime; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AutoPartitionTimeUnit { + Year, + Quarter, + Month, + Day, + Hour, +} + +pub fn validate_partition_spec( + table_path: &TablePath, + partition_keys: &[String], + partition_spec: &PartitionSpec, + is_create: bool, +) -> Result<()> { + let partition_spec_map = partition_spec.get_spec_map(); + if partition_keys.len() != partition_spec_map.len() { + return Err(Error::InvalidPartition { + message: format!( + "PartitionSpec size is not equal to partition keys size for partitioned table {}.", + table_path + ), + }); + } + + let mut reordered_partition_values: Vec<&str> = Vec::with_capacity(partition_keys.len()); + for partition_key in partition_keys { + if let Some(value) = partition_spec_map.get(partition_key) { + reordered_partition_values.push(value); + } else { + return Err(Error::InvalidPartition { + message: format!( + "PartitionSpec {} does not contain partition key '{}' for partitioned table {}.", + partition_spec, partition_key, table_path + ), + }); + } + } + + validate_partition_values(&reordered_partition_values, is_create) +} + +fn validate_partition_values(partition_values: &[&str], is_create: bool) -> Result<()> { + for value in partition_values { + let invalid_name_error = TablePath::detect_invalid_name(value); + let prefix_error = if is_create { + TablePath::validate_prefix(value) + } else { + None + }; + + if invalid_name_error.is_some() || prefix_error.is_some() { + let error_msg = invalid_name_error.unwrap_or_else(|| prefix_error.unwrap()); + return Err(Error::InvalidPartition { + message: format!("The partition value {} is invalid: {}", value, error_msg), + }); + } + } + Ok(()) +} + +/// Generate [`ResolvedPartitionSpec`] for auto partition in server. When we auto creating a +/// partition, we need to first generate a [`ResolvedPartitionSpec`]. +/// +/// The value is the formatted time with the specified time unit. +pub fn generate_auto_partition<'a>( + partition_keys: &'a [String], + current: &Zoned, + offset: i32, + time_unit: AutoPartitionTimeUnit, +) -> Result<ResolvedPartitionSpec<'a>> { + let auto_partition_field_spec = generate_auto_partition_time(current, offset, time_unit)?; + Ok(ResolvedPartitionSpec::from_partition_name( + partition_keys, + auto_partition_field_spec.as_str(), + )) +} + +pub fn generate_auto_partition_time( + current: &Zoned, + offset: i32, + time_unit: AutoPartitionTimeUnit, +) -> Result<String> { + match time_unit { + AutoPartitionTimeUnit::Year => { + let adjusted = current + .checked_add(jiff::Span::new().years(offset)) + .map_err(|_| IllegalArgument { + message: "Year offset would cause overflow".to_string(), + })?; + Ok(format!("{}", adjusted.year())) + } + AutoPartitionTimeUnit::Quarter => { + let adjusted = current + .checked_add(jiff::Span::new().months(offset * 3)) + .map_err(|_| IllegalArgument { + message: "Quarter offset would cause overflow".to_string(), + })?; + let quarter = (adjusted.month() as i32 - 1) / 3 + 1; + Ok(format!("{}{}", adjusted.year(), quarter)) + } + AutoPartitionTimeUnit::Month => { + let adjusted = current + .checked_add(jiff::Span::new().months(offset)) + .map_err(|_| IllegalArgument { + message: "Month offset would cause overflow".to_string(), + })?; + Ok(format!("{}{:02}", adjusted.year(), adjusted.month())) + } + AutoPartitionTimeUnit::Day => { + let adjusted = current + .checked_add(jiff::Span::new().days(offset)) + .map_err(|_| IllegalArgument { + message: "Day offset would cause overflow".to_string(), + })?; + Ok(format!( + "{}{:02}{:02}", + adjusted.year(), + adjusted.month(), + adjusted.day() + )) + } + AutoPartitionTimeUnit::Hour => { + let adjusted = current + .checked_add(jiff::Span::new().hours(offset)) + .map_err(|_| IllegalArgument { + message: "Hour offset would cause overflow".to_string(), + })?; + Ok(format!( + "{}{:02}{:02}{:02}", + adjusted.year(), + adjusted.month(), + adjusted.day(), + adjusted.hour() + )) + } + } +} + +fn hex_string(bytes: &[u8]) -> String { + let mut hex = String::with_capacity(bytes.len() * 2); + for &b in bytes { + let h = format!("{:02x}", b); + hex.push_str(&h); + } + hex +} + +fn reformat_float(value: f32) -> String { + if value.is_nan() { + "NaN".to_string() + } else if value.is_infinite() { + if value > 0.0 { + "Inf".to_string() + } else { + "-Inf".to_string() + } + } else { + value.to_string().replace('.', "_") + } +} + +fn reformat_double(value: f64) -> String { + if value.is_nan() { + "NaN".to_string() + } else if value.is_infinite() { + if value > 0.0 { + "Inf".to_string() + } else { + "-Inf".to_string() + } + } else { + value.to_string().replace('.', "_") + } +} + +const UNIX_EPOCH_DATE: jiff::civil::Date = jiff::civil::date(1970, 1, 1); + +fn day_to_string(days: i32) -> String { + let date = UNIX_EPOCH_DATE + days.days(); + format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day()) +} + +fn date_to_string(date: Date) -> String { + day_to_string(date.get_inner()) +} + +const NANOS_PER_MILLIS: i64 = 1_000_000; +const MILLIS_PER_SECOND: i64 = 1_000; +const MILLIS_PER_MINUTE: i64 = 60 * MILLIS_PER_SECOND; +const MILLIS_PER_HOUR: i64 = 60 * MILLIS_PER_MINUTE; + +fn milli_to_string(milli: i32) -> String { + let hour = milli.div_euclid(MILLIS_PER_HOUR as i32); + let min = milli + .rem_euclid(MILLIS_PER_HOUR as i32) + .div_euclid(MILLIS_PER_MINUTE as i32); + let sec = milli + .rem_euclid(MILLIS_PER_MINUTE as i32) + .div_euclid(MILLIS_PER_SECOND as i32); + let ms = milli.rem_euclid(MILLIS_PER_SECOND as i32); + + format!("{:02}-{:02}-{:02}_{:03}", hour, min, sec, ms) +} + +fn time_to_string(time: Time) -> String { + milli_to_string(time.get_inner()) +} + +/// Always add nanoseconds whether TimestampNtz and TimestampLtz are compact or not. +fn timestamp_ntz_to_string(ts: TimestampNtz) -> String { + let millis = ts.get_millisecond(); + let nano_of_milli = ts.get_nano_of_millisecond(); + + let total_nanos = (millis % MILLIS_PER_SECOND) * NANOS_PER_MILLIS + (nano_of_milli as i64); + let total_secs = millis / MILLIS_PER_SECOND; + + let epoch = jiff::Timestamp::UNIX_EPOCH; + let ts_jiff = epoch + jiff::Span::new().seconds(total_secs); + let dt = ts_jiff.to_zoned(jiff::tz::TimeZone::UTC).datetime(); + + format_date_time(total_nanos, dt) +} + +fn timestamp_ltz_to_string(ts: TimestampLtz) -> String { + let millis = ts.get_epoch_millisecond(); + let nano_of_milli = ts.get_nano_of_millisecond(); + + let total_nanos = (millis % MILLIS_PER_SECOND) * NANOS_PER_MILLIS + (nano_of_milli as i64); + let total_secs = millis / MILLIS_PER_SECOND; + + let epoch = jiff::Timestamp::UNIX_EPOCH; + let ts_jiff = epoch + jiff::Span::new().seconds(total_secs); + let dt = ts_jiff.to_zoned(jiff::tz::TimeZone::UTC).datetime(); + + format_date_time(total_nanos, dt) +} + +fn format_date_time(total_nanos: i64, dt: DateTime) -> String { + if total_nanos > 0 { Review Comment: What happens in Java and Rust when nanos are 0? I see Java uses optional blocks, so wouldn't it return trailing `_` if no nanos? ########## crates/fluss/src/util/partition.rs: ########## @@ -0,0 +1,678 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Utils for partition. + +#![allow(dead_code)] + +use crate::error::Error::IllegalArgument; +use crate::error::{Error, Result}; +use crate::metadata::{DataType, PartitionSpec, ResolvedPartitionSpec, TablePath}; +use crate::row::{Date, Datum, Time, TimestampLtz, TimestampNtz}; +use jiff::ToSpan; +use jiff::Zoned; +use jiff::civil::DateTime; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AutoPartitionTimeUnit { + Year, + Quarter, + Month, + Day, + Hour, +} + +pub fn validate_partition_spec( + table_path: &TablePath, + partition_keys: &[String], + partition_spec: &PartitionSpec, + is_create: bool, +) -> Result<()> { + let partition_spec_map = partition_spec.get_spec_map(); + if partition_keys.len() != partition_spec_map.len() { + return Err(Error::InvalidPartition { + message: format!( + "PartitionSpec size is not equal to partition keys size for partitioned table {}.", + table_path + ), + }); + } + + let mut reordered_partition_values: Vec<&str> = Vec::with_capacity(partition_keys.len()); + for partition_key in partition_keys { + if let Some(value) = partition_spec_map.get(partition_key) { + reordered_partition_values.push(value); + } else { + return Err(Error::InvalidPartition { + message: format!( + "PartitionSpec {} does not contain partition key '{}' for partitioned table {}.", + partition_spec, partition_key, table_path + ), + }); + } + } + + validate_partition_values(&reordered_partition_values, is_create) +} + +fn validate_partition_values(partition_values: &[&str], is_create: bool) -> Result<()> { + for value in partition_values { + let invalid_name_error = TablePath::detect_invalid_name(value); + let prefix_error = if is_create { + TablePath::validate_prefix(value) + } else { + None + }; + + if invalid_name_error.is_some() || prefix_error.is_some() { + let error_msg = invalid_name_error.unwrap_or_else(|| prefix_error.unwrap()); + return Err(Error::InvalidPartition { + message: format!("The partition value {} is invalid: {}", value, error_msg), + }); + } + } + Ok(()) +} + +/// Generate [`ResolvedPartitionSpec`] for auto partition in server. When we auto creating a +/// partition, we need to first generate a [`ResolvedPartitionSpec`]. +/// +/// The value is the formatted time with the specified time unit. +pub fn generate_auto_partition<'a>( + partition_keys: &'a [String], + current: &Zoned, + offset: i32, + time_unit: AutoPartitionTimeUnit, +) -> Result<ResolvedPartitionSpec<'a>> { + let auto_partition_field_spec = generate_auto_partition_time(current, offset, time_unit)?; + Ok(ResolvedPartitionSpec::from_partition_name( + partition_keys, + auto_partition_field_spec.as_str(), + )) +} + +pub fn generate_auto_partition_time( + current: &Zoned, + offset: i32, + time_unit: AutoPartitionTimeUnit, +) -> Result<String> { + match time_unit { + AutoPartitionTimeUnit::Year => { + let adjusted = current + .checked_add(jiff::Span::new().years(offset)) + .map_err(|_| IllegalArgument { + message: "Year offset would cause overflow".to_string(), + })?; + Ok(format!("{}", adjusted.year())) + } + AutoPartitionTimeUnit::Quarter => { + let adjusted = current + .checked_add(jiff::Span::new().months(offset * 3)) + .map_err(|_| IllegalArgument { + message: "Quarter offset would cause overflow".to_string(), + })?; + let quarter = (adjusted.month() as i32 - 1) / 3 + 1; + Ok(format!("{}{}", adjusted.year(), quarter)) + } + AutoPartitionTimeUnit::Month => { + let adjusted = current + .checked_add(jiff::Span::new().months(offset)) + .map_err(|_| IllegalArgument { + message: "Month offset would cause overflow".to_string(), + })?; + Ok(format!("{}{:02}", adjusted.year(), adjusted.month())) + } + AutoPartitionTimeUnit::Day => { + let adjusted = current + .checked_add(jiff::Span::new().days(offset)) + .map_err(|_| IllegalArgument { + message: "Day offset would cause overflow".to_string(), + })?; + Ok(format!( + "{}{:02}{:02}", + adjusted.year(), + adjusted.month(), + adjusted.day() + )) + } + AutoPartitionTimeUnit::Hour => { + let adjusted = current + .checked_add(jiff::Span::new().hours(offset)) + .map_err(|_| IllegalArgument { + message: "Hour offset would cause overflow".to_string(), + })?; + Ok(format!( + "{}{:02}{:02}{:02}", + adjusted.year(), + adjusted.month(), + adjusted.day(), + adjusted.hour() + )) + } + } +} + +fn hex_string(bytes: &[u8]) -> String { + let mut hex = String::with_capacity(bytes.len() * 2); + for &b in bytes { + let h = format!("{:02x}", b); + hex.push_str(&h); + } + hex +} + +fn reformat_float(value: f32) -> String { + if value.is_nan() { + "NaN".to_string() + } else if value.is_infinite() { + if value > 0.0 { + "Inf".to_string() + } else { + "-Inf".to_string() + } + } else { + value.to_string().replace('.', "_") + } +} + +fn reformat_double(value: f64) -> String { + if value.is_nan() { + "NaN".to_string() + } else if value.is_infinite() { + if value > 0.0 { + "Inf".to_string() + } else { + "-Inf".to_string() + } + } else { + value.to_string().replace('.', "_") + } +} + +const UNIX_EPOCH_DATE: jiff::civil::Date = jiff::civil::date(1970, 1, 1); + +fn day_to_string(days: i32) -> String { + let date = UNIX_EPOCH_DATE + days.days(); + format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day()) +} + +fn date_to_string(date: Date) -> String { + day_to_string(date.get_inner()) +} + +const NANOS_PER_MILLIS: i64 = 1_000_000; +const MILLIS_PER_SECOND: i64 = 1_000; +const MILLIS_PER_MINUTE: i64 = 60 * MILLIS_PER_SECOND; +const MILLIS_PER_HOUR: i64 = 60 * MILLIS_PER_MINUTE; + +fn milli_to_string(milli: i32) -> String { + let hour = milli.div_euclid(MILLIS_PER_HOUR as i32); + let min = milli + .rem_euclid(MILLIS_PER_HOUR as i32) + .div_euclid(MILLIS_PER_MINUTE as i32); + let sec = milli + .rem_euclid(MILLIS_PER_MINUTE as i32) + .div_euclid(MILLIS_PER_SECOND as i32); + let ms = milli.rem_euclid(MILLIS_PER_SECOND as i32); + + format!("{:02}-{:02}-{:02}_{:03}", hour, min, sec, ms) +} + +fn time_to_string(time: Time) -> String { + milli_to_string(time.get_inner()) +} + +/// Always add nanoseconds whether TimestampNtz and TimestampLtz are compact or not. +fn timestamp_ntz_to_string(ts: TimestampNtz) -> String { + let millis = ts.get_millisecond(); + let nano_of_milli = ts.get_nano_of_millisecond(); + + let total_nanos = (millis % MILLIS_PER_SECOND) * NANOS_PER_MILLIS + (nano_of_milli as i64); Review Comment: what would happen with negative timestamp? ########## crates/fluss/src/util/partition.rs: ########## @@ -0,0 +1,678 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Utils for partition. + +#![allow(dead_code)] + +use crate::error::Error::IllegalArgument; +use crate::error::{Error, Result}; +use crate::metadata::{DataType, PartitionSpec, ResolvedPartitionSpec, TablePath}; +use crate::row::{Date, Datum, Time, TimestampLtz, TimestampNtz}; +use jiff::ToSpan; +use jiff::Zoned; +use jiff::civil::DateTime; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AutoPartitionTimeUnit { + Year, + Quarter, + Month, + Day, + Hour, +} + +pub fn validate_partition_spec( + table_path: &TablePath, + partition_keys: &[String], + partition_spec: &PartitionSpec, + is_create: bool, +) -> Result<()> { + let partition_spec_map = partition_spec.get_spec_map(); + if partition_keys.len() != partition_spec_map.len() { + return Err(Error::InvalidPartition { + message: format!( + "PartitionSpec size is not equal to partition keys size for partitioned table {}.", + table_path + ), + }); + } + + let mut reordered_partition_values: Vec<&str> = Vec::with_capacity(partition_keys.len()); + for partition_key in partition_keys { + if let Some(value) = partition_spec_map.get(partition_key) { + reordered_partition_values.push(value); + } else { + return Err(Error::InvalidPartition { + message: format!( + "PartitionSpec {} does not contain partition key '{}' for partitioned table {}.", + partition_spec, partition_key, table_path + ), + }); + } + } + + validate_partition_values(&reordered_partition_values, is_create) +} + +fn validate_partition_values(partition_values: &[&str], is_create: bool) -> Result<()> { + for value in partition_values { + let invalid_name_error = TablePath::detect_invalid_name(value); + let prefix_error = if is_create { + TablePath::validate_prefix(value) + } else { + None + }; + + if invalid_name_error.is_some() || prefix_error.is_some() { + let error_msg = invalid_name_error.unwrap_or_else(|| prefix_error.unwrap()); + return Err(Error::InvalidPartition { + message: format!("The partition value {} is invalid: {}", value, error_msg), + }); + } + } + Ok(()) +} + +/// Generate [`ResolvedPartitionSpec`] for auto partition in server. When we auto creating a +/// partition, we need to first generate a [`ResolvedPartitionSpec`]. +/// +/// The value is the formatted time with the specified time unit. +pub fn generate_auto_partition<'a>( + partition_keys: &'a [String], + current: &Zoned, + offset: i32, + time_unit: AutoPartitionTimeUnit, +) -> Result<ResolvedPartitionSpec<'a>> { + let auto_partition_field_spec = generate_auto_partition_time(current, offset, time_unit)?; + Ok(ResolvedPartitionSpec::from_partition_name( + partition_keys, + auto_partition_field_spec.as_str(), + )) +} + +pub fn generate_auto_partition_time( + current: &Zoned, + offset: i32, + time_unit: AutoPartitionTimeUnit, +) -> Result<String> { + match time_unit { + AutoPartitionTimeUnit::Year => { + let adjusted = current + .checked_add(jiff::Span::new().years(offset)) + .map_err(|_| IllegalArgument { + message: "Year offset would cause overflow".to_string(), + })?; + Ok(format!("{}", adjusted.year())) + } + AutoPartitionTimeUnit::Quarter => { + let adjusted = current + .checked_add(jiff::Span::new().months(offset * 3)) + .map_err(|_| IllegalArgument { + message: "Quarter offset would cause overflow".to_string(), + })?; + let quarter = (adjusted.month() as i32 - 1) / 3 + 1; + Ok(format!("{}{}", adjusted.year(), quarter)) + } + AutoPartitionTimeUnit::Month => { + let adjusted = current + .checked_add(jiff::Span::new().months(offset)) + .map_err(|_| IllegalArgument { + message: "Month offset would cause overflow".to_string(), + })?; + Ok(format!("{}{:02}", adjusted.year(), adjusted.month())) + } + AutoPartitionTimeUnit::Day => { + let adjusted = current + .checked_add(jiff::Span::new().days(offset)) + .map_err(|_| IllegalArgument { + message: "Day offset would cause overflow".to_string(), + })?; + Ok(format!( + "{}{:02}{:02}", + adjusted.year(), + adjusted.month(), + adjusted.day() + )) + } + AutoPartitionTimeUnit::Hour => { + let adjusted = current + .checked_add(jiff::Span::new().hours(offset)) + .map_err(|_| IllegalArgument { + message: "Hour offset would cause overflow".to_string(), + })?; + Ok(format!( + "{}{:02}{:02}{:02}", + adjusted.year(), + adjusted.month(), + adjusted.day(), + adjusted.hour() + )) + } + } +} + +fn hex_string(bytes: &[u8]) -> String { + let mut hex = String::with_capacity(bytes.len() * 2); + for &b in bytes { + let h = format!("{:02x}", b); + hex.push_str(&h); + } + hex +} + +fn reformat_float(value: f32) -> String { + if value.is_nan() { + "NaN".to_string() + } else if value.is_infinite() { + if value > 0.0 { + "Inf".to_string() + } else { + "-Inf".to_string() + } + } else { + value.to_string().replace('.', "_") + } +} + +fn reformat_double(value: f64) -> String { + if value.is_nan() { + "NaN".to_string() + } else if value.is_infinite() { + if value > 0.0 { + "Inf".to_string() + } else { + "-Inf".to_string() + } + } else { + value.to_string().replace('.', "_") + } +} + +const UNIX_EPOCH_DATE: jiff::civil::Date = jiff::civil::date(1970, 1, 1); + +fn day_to_string(days: i32) -> String { + let date = UNIX_EPOCH_DATE + days.days(); + format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day()) +} + +fn date_to_string(date: Date) -> String { + day_to_string(date.get_inner()) +} + +const NANOS_PER_MILLIS: i64 = 1_000_000; +const MILLIS_PER_SECOND: i64 = 1_000; +const MILLIS_PER_MINUTE: i64 = 60 * MILLIS_PER_SECOND; +const MILLIS_PER_HOUR: i64 = 60 * MILLIS_PER_MINUTE; + +fn milli_to_string(milli: i32) -> String { + let hour = milli.div_euclid(MILLIS_PER_HOUR as i32); + let min = milli + .rem_euclid(MILLIS_PER_HOUR as i32) + .div_euclid(MILLIS_PER_MINUTE as i32); + let sec = milli + .rem_euclid(MILLIS_PER_MINUTE as i32) + .div_euclid(MILLIS_PER_SECOND as i32); + let ms = milli.rem_euclid(MILLIS_PER_SECOND as i32); + + format!("{:02}-{:02}-{:02}_{:03}", hour, min, sec, ms) +} + +fn time_to_string(time: Time) -> String { + milli_to_string(time.get_inner()) +} + +/// Always add nanoseconds whether TimestampNtz and TimestampLtz are compact or not. +fn timestamp_ntz_to_string(ts: TimestampNtz) -> String { + let millis = ts.get_millisecond(); + let nano_of_milli = ts.get_nano_of_millisecond(); + + let total_nanos = (millis % MILLIS_PER_SECOND) * NANOS_PER_MILLIS + (nano_of_milli as i64); + let total_secs = millis / MILLIS_PER_SECOND; + + let epoch = jiff::Timestamp::UNIX_EPOCH; + let ts_jiff = epoch + jiff::Span::new().seconds(total_secs); + let dt = ts_jiff.to_zoned(jiff::tz::TimeZone::UTC).datetime(); + + format_date_time(total_nanos, dt) +} + +fn timestamp_ltz_to_string(ts: TimestampLtz) -> String { + let millis = ts.get_epoch_millisecond(); + let nano_of_milli = ts.get_nano_of_millisecond(); + + let total_nanos = (millis % MILLIS_PER_SECOND) * NANOS_PER_MILLIS + (nano_of_milli as i64); Review Comment: ditto -- 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]
