Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Changelog
- v5.1.1
- Fixed CRL validation to reject newly downloaded CRLs if their NextUpdate has already expired.
- Fixed TIMESTAMP_LTZ datatype to honor session TIMEZONE parameter (ALTER SESSION SET TIMEZONE) instead of using local machine timezone.
- v5.1.0
- Added `APPLICATION_PATH` to `CLIENT_ENVIRONMENT` sent during authentication to identify the application connecting to Snowflake.
- Renew idle sessions in the pool if keep alive is enabled.
Expand Down
104 changes: 104 additions & 0 deletions Snowflake.Data.Tests/IntegrationTests/SFDbDataReaderIT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1667,6 +1667,110 @@ public void TestDataTableLoadOnSemiStructuredColumn(string type)
}
}

[Test]
public void TestTimestampLtzHonorsSessionTimezone()
{
using (var conn = CreateAndOpenConnection())
{
CreateOrReplaceTable(conn, "test_timestamp_ltz_timezone", new[] { "val TIMESTAMP_LTZ" });

using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'";
cmd.ExecuteNonQuery();

cmd.CommandText = "INSERT INTO test_timestamp_ltz_timezone VALUES('2023-08-09 10:00:00')";
cmd.ExecuteNonQuery();

cmd.CommandText = "SELECT * FROM test_timestamp_ltz_timezone";
using (var reader = cmd.ExecuteReader())
{
Assert.IsTrue(reader.Read(), "Should read a record");
var timestamp1 = reader.GetDateTime(0);

var warsawTz = TimeZoneConverter.TZConvert.GetTimeZoneInfo("Europe/Warsaw");
var expectedTime1 = new DateTime(2023, 8, 9, 10, 0, 0, DateTimeKind.Unspecified);
var expectedUtc1 = TimeZoneInfo.ConvertTimeToUtc(expectedTime1, warsawTz);
var expectedInWarsaw = TimeZoneInfo.ConvertTimeFromUtc(expectedUtc1, warsawTz);

Assert.AreEqual(expectedInWarsaw, timestamp1,
$"Timestamp should be returned in Warsaw timezone. Expected: {expectedInWarsaw}, Got: {timestamp1}");
}

cmd.CommandText = "ALTER SESSION SET TIMEZONE = 'Pacific/Honolulu'";
cmd.ExecuteNonQuery();

cmd.CommandText = "SELECT * FROM test_timestamp_ltz_timezone";
using (var reader = cmd.ExecuteReader())
{
Assert.IsTrue(reader.Read(), "Should read a record");
var timestamp2 = reader.GetDateTime(0);

var honoluluTz = TimeZoneConverter.TZConvert.GetTimeZoneInfo("Pacific/Honolulu");
var warsawTz = TimeZoneConverter.TZConvert.GetTimeZoneInfo("Europe/Warsaw");

var originalTimeInWarsaw = new DateTime(2023, 8, 9, 10, 0, 0, DateTimeKind.Unspecified);
var utcTime = TimeZoneInfo.ConvertTimeToUtc(originalTimeInWarsaw, warsawTz);
var expectedInHonolulu = TimeZoneInfo.ConvertTimeFromUtc(utcTime, honoluluTz);

Assert.AreEqual(expectedInHonolulu, timestamp2,
$"Timestamp should be returned in Honolulu timezone. Expected: {expectedInHonolulu}, Got: {timestamp2}");
}
}

CloseConnection(conn);
}
}

[Test]
public void TestTimestampLtzWithMultipleSessionTimezones()
{
using (var conn = CreateAndOpenConnection())
{
CreateOrReplaceTable(conn, "test_ltz_multi_tz", new[] { "val TIMESTAMP_LTZ" });

using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "ALTER SESSION SET TIMEZONE = 'UTC'";
cmd.ExecuteNonQuery();

cmd.CommandText = "INSERT INTO test_ltz_multi_tz VALUES('2024-01-01 00:00:00')";
cmd.ExecuteNonQuery();

var utcBase = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);

// Test reading with different timezones
var timezones = new[]
{
"Europe/Warsaw",
"Asia/Tokyo",
"America/Los_Angeles"
};

foreach (var tzName in timezones)
{
cmd.CommandText = $"ALTER SESSION SET TIMEZONE = '{tzName}'";
cmd.ExecuteNonQuery();

cmd.CommandText = "SELECT val FROM test_ltz_multi_tz";
using (var reader = cmd.ExecuteReader())
{
Assert.IsTrue(reader.Read());
var timestamp = reader.GetDateTime(0);

var tz = TimeZoneConverter.TZConvert.GetTimeZoneInfo(tzName);
var expected = TimeZoneInfo.ConvertTimeFromUtc(utcBase, tz);

Assert.AreEqual(expected, timestamp,
$"TIMESTAMP_LTZ should be in {tzName} timezone");
}
}
}

CloseConnection(conn);
}
}

private DbConnection CreateAndOpenConnection()
{
var conn = new SnowflakeDbConnection(ConnectionString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,5 +549,55 @@ private void SetTimePrecision(SnowflakeDbConnection connection, int precision)
command.ExecuteNonQuery();
}
}

[Test]
public void TestStructuredTypeWithTimestampLtzHonorsSessionTimezone()
{
using (var connection = new SnowflakeDbConnection(ConnectionString))
{
connection.Open();

using (var command = connection.CreateCommand())
{
command.CommandText = "ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'";
command.ExecuteNonQuery();
}

CreateOrReplaceTable(connection, "test_struct_ltz", new[]
{
"id INT",
"data OBJECT(timestamp_value TIMESTAMP_LTZ)"
});

using (var command = connection.CreateCommand())
{
command.CommandText = "INSERT INTO test_struct_ltz SELECT 1, {'timestamp_value': '2024-03-20 15:45:30'::TIMESTAMP_LTZ}";
command.ExecuteNonQuery();

command.CommandText = "SELECT data FROM test_struct_ltz";
using (var reader = (SnowflakeDbDataReader)command.ExecuteReader())
{
Assert.IsTrue(reader.Read());

var obj = reader.GetObject<TestObjectWithTimestampLtz>(0);

var warsawTz = TimeZoneConverter.TZConvert.GetTimeZoneInfo("Europe/Warsaw");
var inputTime = new DateTime(2024, 3, 20, 15, 45, 30, DateTimeKind.Unspecified);
var utcTime = TimeZoneInfo.ConvertTimeToUtc(inputTime, warsawTz);
var expectedInWarsaw = TimeZoneInfo.ConvertTimeFromUtc(utcTime, warsawTz);

Assert.AreEqual(expectedInWarsaw, obj.TimestampValue,
"Structured type TIMESTAMP_LTZ should honor session timezone");
}
}
}
}

[SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.PROPERTIES_NAMES)]
private class TestObjectWithTimestampLtz
{
[SnowflakeColumn(Name = "timestamp_value")]
public DateTime TimestampValue { get; set; }
}
}
}
9 changes: 6 additions & 3 deletions Snowflake.Data/Client/SnowflakeDbDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ public T GetObject<T>(int ordinal)
}
var stringValue = GetString(ordinal);
var json = stringValue == null ? null : JObject.Parse(stringValue);
return JsonToStructuredTypeConverter.ConvertObject<T>(fields, json);
var sessionTimezone = resultSet.sfStatement.SfSession.GetSessionTimezone();
return JsonToStructuredTypeConverter.ConvertObject<T>(fields, json, sessionTimezone);
}
catch (Exception e)
{
Expand All @@ -289,7 +290,8 @@ public T[] GetArray<T>(int ordinal)

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

var stringValue = GetString(ordinal);
var json = stringValue == null ? null : JObject.Parse(stringValue);
return JsonToStructuredTypeConverter.ConvertMap<TKey, TValue>(fields, json);
var sessionTimezone = resultSet.sfStatement.SfSession.GetSessionTimezone();
return JsonToStructuredTypeConverter.ConvertMap<TKey, TValue>(fields, json, sessionTimezone);
}
catch (Exception e)
{
Expand Down
15 changes: 12 additions & 3 deletions Snowflake.Data/Core/ArrowResultChunk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Text;
using Apache.Arrow;
using Snowflake.Data.Client;

namespace Snowflake.Data.Core
{
Expand Down Expand Up @@ -139,7 +140,7 @@ public override UTF8Buffer ExtractCell(int columnIndex)
throw new NotSupportedException();
}

public object ExtractCell(int columnIndex, SFDataType srcType, long scale)
public object ExtractCell(int columnIndex, SFDataType srcType, long scale, TimeZoneInfo sessionTimezone)
{
var column = RecordBatch[_currentBatchIndex].Column(columnIndex);

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

case SFDataType.TIMESTAMP_LTZ:
if (sessionTimezone == null)
{
throw new SnowflakeDbException(SFError.INTERNAL_ERROR,
"Session timezone is required for TIMESTAMP_LTZ conversion");
}

if (column.GetType() == typeof(StructArray))
{
if (_long[columnIndex] == null)
Expand All @@ -321,7 +328,8 @@ public object ExtractCell(int columnIndex, SFDataType srcType, long scale)
_fraction[columnIndex] = ((Int32Array)((StructArray)column).Fields[1]).Values.ToArray();
var epoch = _long[columnIndex][_currentRecordIndex];
var fraction = _fraction[columnIndex][_currentRecordIndex];
return s_epochDate.AddSeconds(epoch).AddTicks(fraction / 100).ToLocalTime();
var utcDateTime = s_epochDate.AddSeconds(epoch).AddTicks(fraction / 100);
return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime.UtcDateTime, sessionTimezone);
}
else
{
Expand All @@ -331,7 +339,8 @@ public object ExtractCell(int columnIndex, SFDataType srcType, long scale)
var value = _long[columnIndex][_currentRecordIndex];
var epoch = ExtractEpoch(value, scale);
var fraction = ExtractFraction(value, scale);
return s_epochDate.AddSeconds(epoch).AddTicks(fraction / 100).ToLocalTime();
var utcDateTime = s_epochDate.AddSeconds(epoch).AddTicks(fraction / 100);
return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime.UtcDateTime, sessionTimezone);
}

case SFDataType.TIMESTAMP_NTZ:
Expand Down
3 changes: 2 additions & 1 deletion Snowflake.Data/Core/ArrowResultSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ private object GetObjectInternal(int ordinal)

var type = sfResultSetMetaData.GetTypesByIndex(ordinal).Item1;
var scale = sfResultSetMetaData.GetScaleByIndex(ordinal);
var sessionTimezone = sfStatement.SfSession.GetSessionTimezone();

var value = ((ArrowResultChunk)_currentChunk).ExtractCell(ordinal, type, (int)scale);
var value = ((ArrowResultChunk)_currentChunk).ExtractCell(ordinal, type, (int)scale, sessionTimezone);

return value ?? DBNull.Value;

Expand Down
Loading
Loading