Skip to content

Commit 426b61c

Browse files
authored
Merge pull request #174 from estie-inc/perf/timestamp-decode-scale
perf(result-table): streamline timestamp decoding
2 parents 811e3d5 + 8d5a69f commit 426b61c

2 files changed

Lines changed: 63 additions & 40 deletions

File tree

src/result_table/decode.rs

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ impl FromCell for NaiveDateTime {
447447

448448
fn build_plan(column: &Column) -> Result<Self::Plan> {
449449
match column.ty() {
450-
ColumnType::TimestampNtz { scale } => TimestampPlan::new(i64::from(scale.unwrap_or(9)))
450+
ColumnType::TimestampNtz { scale } => TimestampPlan::from_metadata_scale(*scale)
451451
.map_err(|detail| incompatible_column::<Self>(column, Some(detail))),
452452
_ => Err(incompatible_column::<Self>(column, None)),
453453
}
@@ -474,10 +474,10 @@ impl FromCell for DateTime<Utc> {
474474

475475
fn build_plan(column: &Column) -> Result<Self::Plan> {
476476
match column.ty() {
477-
ColumnType::TimestampLtz { scale } => TimestampPlan::new(i64::from(scale.unwrap_or(9)))
477+
ColumnType::TimestampLtz { scale } => TimestampPlan::from_metadata_scale(*scale)
478478
.map(UtcTimestampPlan::Ltz)
479479
.map_err(|detail| incompatible_column::<Self>(column, Some(detail))),
480-
ColumnType::TimestampTz { scale } => TimestampPlan::new(i64::from(scale.unwrap_or(9)))
480+
ColumnType::TimestampTz { scale } => TimestampPlan::from_metadata_scale(*scale)
481481
.map(UtcTimestampPlan::Tz)
482482
.map_err(|detail| incompatible_column::<Self>(column, Some(detail))),
483483
_ => Err(incompatible_column::<Self>(column, None)),
@@ -499,7 +499,7 @@ impl FromCell for DateTime<FixedOffset> {
499499

500500
fn build_plan(column: &Column) -> Result<Self::Plan> {
501501
match column.ty() {
502-
ColumnType::TimestampTz { scale } => TimestampPlan::new(i64::from(scale.unwrap_or(9)))
502+
ColumnType::TimestampTz { scale } => TimestampPlan::from_metadata_scale(*scale)
503503
.map_err(|detail| incompatible_column::<Self>(column, Some(detail))),
504504
_ => Err(incompatible_column::<Self>(column, None)),
505505
}
@@ -568,7 +568,7 @@ impl TimePlan {
568568
/// Precomputed scale factors for a timestamp column, shared by the `NTZ` / `LTZ` / `TZ` decoders.
569569
#[derive(Clone, Copy)]
570570
pub struct TimestampPlan {
571-
scale: i64,
571+
scale: usize,
572572
/// `10^scale`, converting between the scaled integer wire value and whole seconds.
573573
scale_factor: i128,
574574
/// `10^(9 - scale)`, scaling the fractional remainder up to nanoseconds.
@@ -582,7 +582,11 @@ impl TimestampPlan {
582582
nanos_multiplier: 1_000_000_000,
583583
};
584584

585-
fn new(scale: i64) -> StdResult<Self, String> {
585+
fn from_metadata_scale(scale: Option<u8>) -> StdResult<Self, String> {
586+
Self::new(scale.unwrap_or(9))
587+
}
588+
589+
fn new(scale: u8) -> StdResult<Self, String> {
586590
let scale = validate_ts_scale(scale)?;
587591
// `scale` is `0..=9`, so neither power can overflow.
588592
Ok(Self {
@@ -593,11 +597,11 @@ impl TimestampPlan {
593597
}
594598
}
595599

596-
fn validate_ts_scale(scale: i64) -> StdResult<i64, String> {
597-
if !(0..=9).contains(&scale) {
600+
fn validate_ts_scale(scale: u8) -> StdResult<usize, String> {
601+
if scale > 9 {
598602
return Err(format!("invalid timestamp scale: {scale} (expected 0..=9)"));
599603
}
600-
Ok(scale)
604+
Ok(usize::from(scale))
601605
}
602606

603607
fn parse_scaled_decimal_i128(
@@ -611,52 +615,71 @@ fn parse_scaled_decimal_i128(
611615
return Err(format!("Could not decode {kind}: {s}"));
612616
}
613617

614-
let (negative, digits) = if let Some(rest) = s.strip_prefix('-') {
615-
(true, rest)
616-
} else if let Some(rest) = s.strip_prefix('+') {
617-
(false, rest)
618-
} else {
619-
(false, s)
618+
let bytes = s.as_bytes();
619+
let mut i = 0usize;
620+
let negative = match bytes[0] {
621+
b'-' => {
622+
i = 1;
623+
true
624+
}
625+
b'+' => {
626+
i = 1;
627+
false
628+
}
629+
_ => false,
620630
};
621-
if digits.is_empty() {
631+
if i == bytes.len() {
622632
return Err(format!("Could not decode {kind}: {s}"));
623633
}
624634

625-
let (whole_str, frac_str) = match digits.split_once('.') {
626-
Some((whole, frac)) if !frac.is_empty() => (whole, Some(frac)),
627-
Some(_) => return Err(format!("Could not decode {kind}: {s}")),
628-
None => (digits, None),
629-
};
630-
if whole_str.is_empty() || !whole_str.as_bytes().iter().all(|b| b.is_ascii_digit()) {
635+
let mut whole = 0i128;
636+
let whole_start = i;
637+
while i < bytes.len() {
638+
let b = bytes[i];
639+
if !b.is_ascii_digit() {
640+
break;
641+
}
642+
whole = whole
643+
.checked_mul(10)
644+
.and_then(|value| value.checked_add(i128::from(b - b'0')))
645+
.ok_or_else(|| format!("Could not decode {kind}: {s}"))?;
646+
i += 1;
647+
}
648+
if i == whole_start {
631649
return Err(format!("Could not decode {kind}: {s}"));
632650
}
633651

634-
let whole = whole_str
635-
.parse::<i128>()
636-
.map_err(|_| format!("Could not decode {kind}: {s}"))?;
637652
let mut scaled = whole
638653
.checked_mul(plan.scale_factor)
639654
.ok_or_else(|| format!("Could not decode {kind}: {s}"))?;
640655

641-
if let Some(frac_str) = frac_str {
642-
if scale == 0 {
656+
if i < bytes.len() {
657+
if bytes[i] != b'.' {
643658
return Err(format!("Could not decode {kind}: {s}"));
644659
}
645-
if !frac_str.as_bytes().iter().all(|b| b.is_ascii_digit()) {
660+
i += 1;
661+
if i == bytes.len() {
646662
return Err(format!("Could not decode {kind}: {s}"));
647663
}
648-
if frac_str.len() > scale as usize {
664+
if scale == 0 {
649665
return Err(format!("Could not decode {kind}: {s}"));
650666
}
651667

652668
let mut frac = 0i128;
653-
for b in frac_str.bytes() {
669+
let mut frac_len = 0usize;
670+
while i < bytes.len() {
671+
let b = bytes[i];
672+
if !b.is_ascii_digit() || frac_len >= scale {
673+
return Err(format!("Could not decode {kind}: {s}"));
674+
}
654675
frac = frac
655676
.checked_mul(10)
656677
.and_then(|value| value.checked_add(i128::from(b - b'0')))
657678
.ok_or_else(|| format!("Could not decode {kind}: {s}"))?;
679+
frac_len += 1;
680+
i += 1;
658681
}
659-
for _ in frac_str.len()..scale as usize {
682+
for _ in frac_len..scale {
660683
frac = frac
661684
.checked_mul(10)
662685
.ok_or_else(|| format!("Could not decode {kind}: {s}"))?;
@@ -706,7 +729,7 @@ pub(crate) fn parse_timestamp_epoch_with(
706729
parse_timestamp_epoch_scaled(scaled, plan, s, "timestamp")
707730
}
708731

709-
pub(crate) fn parse_timestamp_epoch(s: &str, scale: i64) -> StdResult<DateTime<Utc>, String> {
732+
pub(crate) fn parse_timestamp_epoch(s: &str, scale: u8) -> StdResult<DateTime<Utc>, String> {
710733
parse_timestamp_epoch_with(s, &TimestampPlan::new(scale)?)
711734
}
712735

@@ -794,7 +817,7 @@ pub(crate) fn parse_timestamp_tz_with_offset_with(
794817

795818
pub(crate) fn parse_timestamp_tz_with_offset(
796819
s: &str,
797-
scale: i64,
820+
scale: u8,
798821
) -> StdResult<DateTime<FixedOffset>, String> {
799822
parse_timestamp_tz_with_offset_with(s, &TimestampPlan::new(scale)?)
800823
}
@@ -930,16 +953,16 @@ mod tests {
930953
rowset::parser::parse_inline_result_table,
931954
};
932955

933-
fn timestamp_scale(ty: &ColumnType) -> Option<i64> {
956+
fn timestamp_scale(ty: &ColumnType) -> Option<u8> {
934957
match ty {
935958
ColumnType::TimestampNtz { scale }
936959
| ColumnType::TimestampLtz { scale }
937-
| ColumnType::TimestampTz { scale } => scale.map(i64::from),
960+
| ColumnType::TimestampTz { scale } => *scale,
938961
_ => None,
939962
}
940963
}
941964

942-
fn parse_timestamp_tz_as_utc(s: &str, scale: i64) -> StdResult<DateTime<Utc>, String> {
965+
fn parse_timestamp_tz_as_utc(s: &str, scale: u8) -> StdResult<DateTime<Utc>, String> {
943966
parse_timestamp_tz_as_utc_with(s, &TimestampPlan::new(scale)?)
944967
}
945968

@@ -1012,7 +1035,7 @@ mod tests {
10121035
/// Snowflake documents timestamp scales in the `0..=9` range.
10131036
#[test]
10141037
fn parse_timestamp_epoch_rejects_out_of_range_scale() {
1015-
for scale in [-1, 10] {
1038+
for scale in [10, u8::MAX] {
10161039
let err = parse_timestamp_epoch("0.1", scale).unwrap_err();
10171040
assert!(err.contains("invalid timestamp scale"), "actual: {err}");
10181041
}

src/result_table/dynamic.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,19 +338,19 @@ fn decode_dynamic(cell: CellRef<'_>) -> CellDecodeResult<CellValue> {
338338
Ok(CellValue::Time(t))
339339
}
340340
ColumnType::TimestampNtz { scale } => {
341-
let scale = i64::from(scale.unwrap_or(9));
341+
let scale = scale.unwrap_or(9);
342342
let dt = parse_timestamp_epoch(raw, scale)
343343
.map_err(|m| CellConversionError::builder(m).build())?;
344344
Ok(CellValue::TimestampNtz(dt.naive_utc()))
345345
}
346346
ColumnType::TimestampLtz { scale } => {
347-
let scale = i64::from(scale.unwrap_or(9));
347+
let scale = scale.unwrap_or(9);
348348
let dt = parse_timestamp_epoch(raw, scale)
349349
.map_err(|m| CellConversionError::builder(m).build())?;
350350
Ok(CellValue::TimestampLtz(dt))
351351
}
352352
ColumnType::TimestampTz { scale } => {
353-
let scale = i64::from(scale.unwrap_or(9));
353+
let scale = scale.unwrap_or(9);
354354
let dt = parse_timestamp_tz_with_offset(raw, scale)
355355
.map_err(|m| CellConversionError::builder(m).build())?;
356356
Ok(CellValue::TimestampTz(dt))

0 commit comments

Comments
 (0)