Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 7 additions & 2 deletions Plugins/SnowflakeDriverPlugin/SnowflakeValueDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ enum SnowflakeValueDecoder {
let fields = raw.split(separator: " ", omittingEmptySubsequences: true)
guard fields.count == 2, let biased = Int(fields[1]) else { return .text(raw) }
let offsetMinutes = biased - offsetBias
guard abs(offsetMinutes) < minutesPerDay else { return .text(raw) }
guard abs(offsetMinutes) <= maximumOffsetMinutes else { return .text(raw) }
return timestamp(
fromEpochSeconds: String(fields[0]),
scale: scale,
Expand Down Expand Up @@ -117,8 +117,13 @@ enum SnowflakeValueDecoder {
// MARK: - Constants

private static let secondsPerDay = 86_400
private static let minutesPerDay = 1_440
private static let offsetBias = 1_440

/// `TimeZone(secondsFromGMT:)` returns nil beyond eighteen hours, and `DatabaseDateParser` reads
/// that nil as GMT, so a `+18:30` suffix would come back as an instant eighteen and a half hours
/// from the one Snowflake sent. A payload that cannot survive the round trip keeps its raw text
/// instead of being written in a spelling the reader will misread.
private static let maximumOffsetMinutes = 1_080
}

/// How a decoded timestamp names the zone it was read in. `TIMESTAMP_NTZ` genuinely has none, so it
Expand Down
11 changes: 11 additions & 0 deletions TableProTests/Plugins/SnowflakeValueDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ struct SnowflakeValueDecoderTests {
#expect(decodedText("1616173619.000000000 9999", "timestamp_tz", scale: 0) == "1616173619.000000000 9999")
}

/// `TimeZone(secondsFromGMT:)` is nil beyond eighteen hours and the shared parser reads that nil
/// as GMT, so emitting `+18:30` would move the instant by eighteen and a half hours. Eighteen
/// hours exactly is the last offset that survives the round trip.
@Test("An offset the shared parser cannot represent keeps its raw text")
func testOutOfRangeOffsetIsPreserved() {
#expect(decodedText("0.000000000 2520", "timestamp_tz", scale: 0) == "1970-01-01 18:00:00+18:00")
#expect(decodedText("0.000000000 360", "timestamp_tz", scale: 0) == "1969-12-31 06:00:00-18:00")
#expect(decodedText("0.000000000 2550", "timestamp_tz", scale: 0) == "0.000000000 2550")
#expect(decodedText("0.000000000 330", "timestamp_tz", scale: 0) == "0.000000000 330")
}

/// The wire writes nine decimal places, so the value carries more significant digits than a
/// `Double` holds. Parsing it as a floating-point number drops the last nanoseconds silently.
@Test("Nanosecond precision survives decoding")
Expand Down
Loading