Skip to content

Commit 0363612

Browse files
committed
SNOW-2216772 Honour session timezone for timestamp_ltz
1 parent e4d8466 commit 0363612

File tree

13 files changed

+347
-113
lines changed

13 files changed

+347
-113
lines changed

Snowflake.Data.Tests/IntegrationTests/SFDbDataReaderIT.cs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,129 @@ public void TestDataTableLoadOnSemiStructuredColumn(string type)
16671667
}
16681668
}
16691669

1670+
[Test]
1671+
public void TestTimestampLtzHonorsSessionTimezone()
1672+
{
1673+
using (var conn = CreateAndOpenConnection())
1674+
{
1675+
using (var cmd = conn.CreateCommand())
1676+
{
1677+
cmd.CommandText = "CREATE OR REPLACE TABLE test_timestamp_ltz_timezone (val TIMESTAMP_LTZ)";
1678+
cmd.ExecuteNonQuery();
1679+
1680+
try
1681+
{
1682+
cmd.CommandText = "ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'";
1683+
cmd.ExecuteNonQuery();
1684+
1685+
cmd.CommandText = "INSERT INTO test_timestamp_ltz_timezone VALUES('2023-08-09 10:00:00')";
1686+
cmd.ExecuteNonQuery();
1687+
1688+
cmd.CommandText = "SELECT * FROM test_timestamp_ltz_timezone";
1689+
using (var reader = cmd.ExecuteReader())
1690+
{
1691+
Assert.IsTrue(reader.Read(), "Should read a record");
1692+
var timestamp1 = reader.GetDateTime(0);
1693+
1694+
var warsawTz = TimeZoneConverter.TZConvert.GetTimeZoneInfo("Europe/Warsaw");
1695+
var expectedTime1 = new DateTime(2023, 8, 9, 10, 0, 0, DateTimeKind.Unspecified);
1696+
var expectedUtc1 = TimeZoneInfo.ConvertTimeToUtc(expectedTime1, warsawTz);
1697+
var expectedInWarsaw = TimeZoneInfo.ConvertTimeFromUtc(expectedUtc1, warsawTz);
1698+
1699+
Assert.AreEqual(expectedInWarsaw, timestamp1,
1700+
$"Timestamp should be returned in Warsaw timezone. Expected: {expectedInWarsaw}, Got: {timestamp1}");
1701+
}
1702+
1703+
cmd.CommandText = "ALTER SESSION SET TIMEZONE = 'Pacific/Honolulu'";
1704+
cmd.ExecuteNonQuery();
1705+
1706+
cmd.CommandText = "SELECT * FROM test_timestamp_ltz_timezone";
1707+
using (var reader = cmd.ExecuteReader())
1708+
{
1709+
Assert.IsTrue(reader.Read(), "Should read a record");
1710+
var timestamp2 = reader.GetDateTime(0);
1711+
1712+
var honoluluTz = TimeZoneConverter.TZConvert.GetTimeZoneInfo("Pacific/Honolulu");
1713+
var warsawTz = TimeZoneConverter.TZConvert.GetTimeZoneInfo("Europe/Warsaw");
1714+
1715+
var originalTimeInWarsaw = new DateTime(2023, 8, 9, 10, 0, 0, DateTimeKind.Unspecified);
1716+
var utcTime = TimeZoneInfo.ConvertTimeToUtc(originalTimeInWarsaw, warsawTz);
1717+
var expectedInHonolulu = TimeZoneInfo.ConvertTimeFromUtc(utcTime, honoluluTz);
1718+
1719+
Assert.AreEqual(expectedInHonolulu, timestamp2,
1720+
$"Timestamp should be returned in Honolulu timezone. Expected: {expectedInHonolulu}, Got: {timestamp2}");
1721+
}
1722+
}
1723+
finally
1724+
{
1725+
// Cleanup
1726+
cmd.CommandText = "DROP TABLE IF EXISTS test_timestamp_ltz_timezone";
1727+
cmd.ExecuteNonQuery();
1728+
}
1729+
}
1730+
1731+
CloseConnection(conn);
1732+
}
1733+
}
1734+
1735+
[Test]
1736+
public void TestTimestampLtzWithMultipleSessionTimezones()
1737+
{
1738+
using (var conn = CreateAndOpenConnection())
1739+
{
1740+
using (var cmd = conn.CreateCommand())
1741+
{
1742+
cmd.CommandText = "CREATE OR REPLACE TABLE test_ltz_multi_tz (val TIMESTAMP_LTZ)";
1743+
cmd.ExecuteNonQuery();
1744+
1745+
try
1746+
{
1747+
cmd.CommandText = "ALTER SESSION SET TIMEZONE = 'UTC'";
1748+
cmd.ExecuteNonQuery();
1749+
1750+
cmd.CommandText = "INSERT INTO test_ltz_multi_tz VALUES('2024-01-01 00:00:00')";
1751+
cmd.ExecuteNonQuery();
1752+
1753+
var utcBase = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
1754+
1755+
// Test reading with different timezones
1756+
var timezones = new[]
1757+
{
1758+
"Europe/Warsaw",
1759+
"Asia/Tokyo",
1760+
"America/Los_Angeles"
1761+
};
1762+
1763+
foreach (var tzName in timezones)
1764+
{
1765+
cmd.CommandText = $"ALTER SESSION SET TIMEZONE = '{tzName}'";
1766+
cmd.ExecuteNonQuery();
1767+
1768+
cmd.CommandText = "SELECT val FROM test_ltz_multi_tz";
1769+
using (var reader = cmd.ExecuteReader())
1770+
{
1771+
Assert.IsTrue(reader.Read());
1772+
var timestamp = reader.GetDateTime(0);
1773+
1774+
var tz = TimeZoneConverter.TZConvert.GetTimeZoneInfo(tzName);
1775+
var expected = TimeZoneInfo.ConvertTimeFromUtc(utcBase, tz);
1776+
1777+
Assert.AreEqual(expected, timestamp,
1778+
$"TIMESTAMP_LTZ should be in {tzName} timezone");
1779+
}
1780+
}
1781+
}
1782+
finally
1783+
{
1784+
cmd.CommandText = "DROP TABLE IF EXISTS test_ltz_multi_tz";
1785+
cmd.ExecuteNonQuery();
1786+
}
1787+
}
1788+
1789+
CloseConnection(conn);
1790+
}
1791+
}
1792+
16701793
private DbConnection CreateAndOpenConnection()
16711794
{
16721795
var conn = new SnowflakeDbConnection(ConnectionString);

Snowflake.Data.Tests/IntegrationTests/StructuredTypesWithEmbeddedUnstructuredIT.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,5 +549,55 @@ private void SetTimePrecision(SnowflakeDbConnection connection, int precision)
549549
command.ExecuteNonQuery();
550550
}
551551
}
552+
553+
[Test]
554+
public void TestStructuredTypeWithTimestampLtzHonorsSessionTimezone()
555+
{
556+
using (var connection = new SnowflakeDbConnection(ConnectionString))
557+
{
558+
connection.Open();
559+
560+
using (var command = connection.CreateCommand())
561+
{
562+
command.CommandText = "ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'";
563+
command.ExecuteNonQuery();
564+
}
565+
566+
CreateOrReplaceTable(connection, "test_struct_ltz", new[]
567+
{
568+
"id INT",
569+
"data OBJECT(timestamp_value TIMESTAMP_LTZ)"
570+
});
571+
572+
using (var command = connection.CreateCommand())
573+
{
574+
command.CommandText = "INSERT INTO test_struct_ltz SELECT 1, {'timestamp_value': '2024-03-20 15:45:30'::TIMESTAMP_LTZ}";
575+
command.ExecuteNonQuery();
576+
577+
command.CommandText = "SELECT data FROM test_struct_ltz";
578+
using (var reader = (SnowflakeDbDataReader)command.ExecuteReader())
579+
{
580+
Assert.IsTrue(reader.Read());
581+
582+
var obj = reader.GetObject<TestObjectWithTimestampLtz>(0);
583+
584+
var warsawTz = TimeZoneConverter.TZConvert.GetTimeZoneInfo("Europe/Warsaw");
585+
var inputTime = new DateTime(2024, 3, 20, 15, 45, 30, DateTimeKind.Unspecified);
586+
var utcTime = TimeZoneInfo.ConvertTimeToUtc(inputTime, warsawTz);
587+
var expectedInWarsaw = TimeZoneInfo.ConvertTimeFromUtc(utcTime, warsawTz);
588+
589+
Assert.AreEqual(expectedInWarsaw, obj.TimestampValue,
590+
"Structured type TIMESTAMP_LTZ should honor session timezone");
591+
}
592+
}
593+
}
594+
}
595+
596+
[SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.PROPERTIES_NAMES)]
597+
private class TestObjectWithTimestampLtz
598+
{
599+
[SnowflakeColumn(Name = "timestamp_value")]
600+
public DateTime TimestampValue { get; set; }
601+
}
552602
}
553603
}

Snowflake.Data/Client/SnowflakeDbDataReader.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,8 @@ public T GetObject<T>(int ordinal)
264264
}
265265
var stringValue = GetString(ordinal);
266266
var json = stringValue == null ? null : JObject.Parse(stringValue);
267-
return JsonToStructuredTypeConverter.ConvertObject<T>(fields, json);
267+
var sessionTimezone = resultSet.sfStatement.SfSession.GetSessionTimezone();
268+
return JsonToStructuredTypeConverter.ConvertObject<T>(fields, json, sessionTimezone);
268269
}
269270
catch (Exception e)
270271
{
@@ -289,7 +290,8 @@ public T[] GetArray<T>(int ordinal)
289290

290291
var stringValue = GetString(ordinal);
291292
var json = stringValue == null ? null : JArray.Parse(stringValue);
292-
return JsonToStructuredTypeConverter.ConvertArray<T>(fields, json);
293+
var sessionTimezone = resultSet.sfStatement.SfSession.GetSessionTimezone();
294+
return JsonToStructuredTypeConverter.ConvertArray<T>(fields, json, sessionTimezone);
293295
}
294296
catch (Exception e)
295297
{
@@ -312,7 +314,8 @@ public Dictionary<TKey, TValue> GetMap<TKey, TValue>(int ordinal)
312314

313315
var stringValue = GetString(ordinal);
314316
var json = stringValue == null ? null : JObject.Parse(stringValue);
315-
return JsonToStructuredTypeConverter.ConvertMap<TKey, TValue>(fields, json);
317+
var sessionTimezone = resultSet.sfStatement.SfSession.GetSessionTimezone();
318+
return JsonToStructuredTypeConverter.ConvertMap<TKey, TValue>(fields, json, sessionTimezone);
316319
}
317320
catch (Exception e)
318321
{

Snowflake.Data/Core/ArrowResultChunk.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Text;
44
using Apache.Arrow;
5+
using Snowflake.Data.Client;
56

67
namespace Snowflake.Data.Core
78
{
@@ -139,7 +140,7 @@ public override UTF8Buffer ExtractCell(int columnIndex)
139140
throw new NotSupportedException();
140141
}
141142

142-
public object ExtractCell(int columnIndex, SFDataType srcType, long scale)
143+
public object ExtractCell(int columnIndex, SFDataType srcType, long scale, TimeZoneInfo sessionTimezone)
143144
{
144145
var column = RecordBatch[_currentBatchIndex].Column(columnIndex);
145146

@@ -313,6 +314,12 @@ public object ExtractCell(int columnIndex, SFDataType srcType, long scale)
313314
}
314315

315316
case SFDataType.TIMESTAMP_LTZ:
317+
if (sessionTimezone == null)
318+
{
319+
throw new SnowflakeDbException(SFError.INTERNAL_ERROR,
320+
"Session timezone is required for TIMESTAMP_LTZ conversion");
321+
}
322+
316323
if (column.GetType() == typeof(StructArray))
317324
{
318325
if (_long[columnIndex] == null)
@@ -321,7 +328,8 @@ public object ExtractCell(int columnIndex, SFDataType srcType, long scale)
321328
_fraction[columnIndex] = ((Int32Array)((StructArray)column).Fields[1]).Values.ToArray();
322329
var epoch = _long[columnIndex][_currentRecordIndex];
323330
var fraction = _fraction[columnIndex][_currentRecordIndex];
324-
return s_epochDate.AddSeconds(epoch).AddTicks(fraction / 100).ToLocalTime();
331+
var utcDateTime = s_epochDate.AddSeconds(epoch).AddTicks(fraction / 100);
332+
return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime.UtcDateTime, sessionTimezone);
325333
}
326334
else
327335
{
@@ -331,7 +339,8 @@ public object ExtractCell(int columnIndex, SFDataType srcType, long scale)
331339
var value = _long[columnIndex][_currentRecordIndex];
332340
var epoch = ExtractEpoch(value, scale);
333341
var fraction = ExtractFraction(value, scale);
334-
return s_epochDate.AddSeconds(epoch).AddTicks(fraction / 100).ToLocalTime();
342+
var utcDateTime = s_epochDate.AddSeconds(epoch).AddTicks(fraction / 100);
343+
return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime.UtcDateTime, sessionTimezone);
335344
}
336345

337346
case SFDataType.TIMESTAMP_NTZ:

Snowflake.Data/Core/ArrowResultSet.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,9 @@ private object GetObjectInternal(int ordinal)
158158

159159
var type = sfResultSetMetaData.GetTypesByIndex(ordinal).Item1;
160160
var scale = sfResultSetMetaData.GetScaleByIndex(ordinal);
161+
var sessionTimezone = sfStatement.SfSession.GetSessionTimezone();
161162

162-
var value = ((ArrowResultChunk)_currentChunk).ExtractCell(ordinal, type, (int)scale);
163+
var value = ((ArrowResultChunk)_currentChunk).ExtractCell(ordinal, type, (int)scale, sessionTimezone);
163164

164165
return value ?? DBNull.Value;
165166

0 commit comments

Comments
 (0)