Skip to content
Open
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.3.0
- Introduced shared library for extended telemetry to identify and prepare testing platform for native rust extensions.
- Fixed TIMESTAMP_LTZ datatype to honor session TIMEZONE parameter (ALTER SESSION SET TIMEZONE) instead of using local machine timezone.
- v5.2.1
- Bug fix: Fix the extremely rare case where intermittent network issues during uploads to Azure Blob Storage prevent metadata updates
- v5.2.0
Expand Down
134 changes: 123 additions & 11 deletions Snowflake.Data.Tests/IntegrationTests/SFDbDataReaderIT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,13 @@ public void TestGetTimestampLTZ()
{
using (var conn = CreateAndOpenConnection())
{
IDbCommand setTimezoneCmd = conn.CreateCommand();
setTimezoneCmd.CommandText = "ALTER SESSION SET TIMEZONE = 'America/Los_Angeles'";
setTimezoneCmd.ExecuteNonQuery();

CreateOrReplaceTable(conn, TableName, new[] { "cola TIMESTAMP_LTZ" });

DateTimeOffset now = DateTimeOffset.Now;
DateTimeOffset insertValue = new DateTimeOffset(2024, 1, 15, 18, 30, 45, 123, TimeSpan.Zero);

IDbCommand cmd = conn.CreateCommand();

Expand All @@ -544,7 +548,7 @@ public void TestGetTimestampLTZ()

var p1 = (SnowflakeDbParameter)cmd.CreateParameter();
p1.ParameterName = "1";
p1.Value = now;
p1.Value = insertValue;
p1.DbType = DbType.DateTimeOffset;
p1.SFDataType = Core.SFDataType.TIMESTAMP_LTZ;
cmd.Parameters.Add(p1);
Expand All @@ -561,8 +565,8 @@ public void TestGetTimestampLTZ()
DateTimeOffset dtOffset = (DateTimeOffset)reader.GetValue(0);
reader.Close();

Assert.AreEqual(now, dtOffset);
Assert.AreEqual(now.Offset, dtOffset.Offset);
Assert.AreEqual(insertValue.UtcDateTime, dtOffset.UtcDateTime);
Assert.AreEqual(TimeSpan.FromHours(-8), dtOffset.Offset);

CloseConnection(conn);
}
Expand Down Expand Up @@ -1583,26 +1587,30 @@ public void TestTimestampTz(string testValue, int scale)
}

[Test]
[TestCase("2019-01-01 12:12:12.1234567 +0500", 7)]
[TestCase("2019-01-01 12:12:12.1234567 +1400", 7)]
[TestCase("0001-01-01 00:00:00.0000000 +0000", 9)]
[TestCase("9999-12-31 23:59:59.9999999 +0000", 9)]
public void TestTimestampLtz(string testValue, int scale)
[TestCase("2019-01-01 12:12:12.1234567 +0200", 7, "2019-01-01 02:12:12.1234567 -08:00")]
[TestCase("2019-01-01 12:12:12.1234567 +1400", 7, "2018-12-31 14:12:12.1234567 -08:00")]
[TestCase("1883-11-19 00:00:00.0000000 +0000", 9, "1883-11-18 16:00:00.0000000 -08:00")] // date when time zones were standardized
[TestCase("9999-12-31 23:59:59.9999999 +0000", 9, "9999-12-31 15:59:59.9999999 -08:00")]
[TestCase("2019-01-01 12:12:12.1234567", 7, "2019-01-01 12:12:12.1234567 -08:00")]
public void TestTimestampLtz(string testValue, int scale, string expectedValue)
{
using (var conn = CreateAndOpenConnection())
{
DbCommand cmd = conn.CreateCommand();

cmd.CommandText = "ALTER SESSION SET TIMEZONE = 'America/Los_Angeles'";
cmd.ExecuteNonQuery();

cmd.CommandText = $"select '{testValue}'::TIMESTAMP_LTZ({scale})";
using (SnowflakeDbDataReader reader = (SnowflakeDbDataReader)cmd.ExecuteReader())
{
ValidateResultFormat(reader);

reader.Read();

var expectedValue = DateTimeOffset.Parse(testValue).ToLocalTime();
var expected = DateTimeOffset.Parse(expectedValue);

Assert.AreEqual(expectedValue, reader.GetValue(0));
Assert.AreEqual(expected, reader.GetValue(0));
}

CloseConnection(conn);
Expand Down Expand Up @@ -1667,6 +1675,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 @@ -288,6 +288,9 @@ public void TestSelectDateTime(string dbValue, string dbType, DateTime? expected
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = "ALTER SESSION SET TIMEZONE = 'America/Los_Angeles'";
command.ExecuteNonQuery();

EnableStructuredTypes(connection);
SetTimePrecision(connection, 9);
var rawValueString = $"'{dbValue}'::{dbType}";
Expand Down Expand Up @@ -328,10 +331,10 @@ internal static IEnumerable<object[]> DateTimeConversionCases()
};
yield return new object[]
{
"2024-07-11 14:20:05 -7:00",
"2024-07-11 14:20:05",
SFDataType.TIMESTAMP_LTZ.ToString(),
null,
DateTime.Parse("2024-07-11 21:20:05").ToLocalTime()
DateTime.SpecifyKind(DateTime.Parse("2024-07-11 14:20:05"), DateTimeKind.Local)
};
yield return new object[]
{
Expand All @@ -356,10 +359,10 @@ internal static IEnumerable<object[]> DateTimeConversionCases()
};
yield return new object[]
{
"2024-07-11 14:20:05.123456789 -7:00",
"2024-07-11 14:20:05.123456789 -2:00",
SFDataType.TIMESTAMP_LTZ.ToString(),
null,
DateTime.Parse("2024-07-11 21:20:05.1234568").ToLocalTime()
DateTime.SpecifyKind(DateTime.Parse("2024-07-11 09:20:05.1234568"), DateTimeKind.Local)
};
yield return new object[]
{
Expand All @@ -377,10 +380,10 @@ internal static IEnumerable<object[]> DateTimeConversionCases()
};
yield return new object[]
{
"9999-12-31 23:59:59.999999 +13:00",
"1883-11-19 00:00:00.000000 -5:00",
SFDataType.TIMESTAMP_LTZ.ToString(),
null,
DateTime.Parse("9999-12-31 10:59:59.999999").ToLocalTime()
DateTime.Parse("1883-11-18 21:00:00.000000").ToLocalTime()
};
yield return new object[]
{
Expand Down Expand Up @@ -408,6 +411,9 @@ public void TestSelectDateTimeOffset(string dbValue, string dbType, DateTime? ex
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = "ALTER SESSION SET TIMEZONE = 'America/Los_Angeles'";
command.ExecuteNonQuery();

EnableStructuredTypes(connection);
SetTimePrecision(connection, 9);
var rawValueString = $"'{dbValue}'::{dbType}";
Expand Down Expand Up @@ -450,7 +456,7 @@ internal static IEnumerable<object[]> DateTimeOffsetConversionCases()
"2024-07-11 14:20:05 -7:00",
SFDataType.TIMESTAMP_LTZ.ToString(),
null,
DateTimeOffset.Parse("2024-07-11 14:20:05 -7:00").ToLocalTime()
DateTimeOffset.Parse("2024-07-11 14:20:05 -7:00")
};
yield return new object[]
{
Expand All @@ -475,10 +481,10 @@ internal static IEnumerable<object[]> DateTimeOffsetConversionCases()
};
yield return new object[]
{
"2024-07-11 14:20:05.123456789 -7:00",
"2024-07-11 14:20:05.123456789 -2:00",
SFDataType.TIMESTAMP_LTZ.ToString(),
null,
DateTimeOffset.Parse("2024-07-11 14:20:05.1234568 -7:00")
DateTimeOffset.Parse("2024-07-11 09:20:05.1234568 -7:00")
};
yield return new object[]
{
Expand Down
6 changes: 3 additions & 3 deletions Snowflake.Data.Tests/UnitTests/ArrowResultChunkTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public void TestExtractCellReturnsNull()
var chunk = pair.Key;
var type = pair.Value;
chunk.Next();
Assert.AreEqual(DBNull.Value, chunk.ExtractCell(0, type, 0), $"Expected DBNull.Value for SFDataType: {type}");
Assert.AreEqual(DBNull.Value, chunk.ExtractCell(0, type, 0, TimeZoneInfo.Utc), $"Expected DBNull.Value for SFDataType: {type}");
}
}

Expand All @@ -192,7 +192,7 @@ public void TestExtractCellThrowsExceptionForNoneType()
var chunk = new ArrowResultChunk(_recordBatchOne);
chunk.Next();

Assert.Throws<NotSupportedException>(() => chunk.ExtractCell(0, SFDataType.None, 0));
Assert.Throws<NotSupportedException>(() => chunk.ExtractCell(0, SFDataType.None, 0, TimeZoneInfo.Utc));
}

[Test]
Expand Down Expand Up @@ -417,7 +417,7 @@ void TestExtractCell(IEnumerable testValues, SFDataType sfType, long scale, long
chunk.Next();

var expectedValue = (divider == 0) ? testValue : Convert.ToDecimal(testValue) / divider;
Assert.AreEqual(expectedValue, chunk.ExtractCell(0, sfType, scale));
Assert.AreEqual(expectedValue, chunk.ExtractCell(0, sfType, scale, TimeZoneInfo.Utc));
}
}
public static RecordBatch PrepareRecordBatch(SFDataType sfType, long scale, object values)
Expand Down
2 changes: 1 addition & 1 deletion Snowflake.Data.Tests/UnitTests/StructuredTypesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public void TestTimeConversions(string value, string sfTypeString, object expect
var csharpType = expected.GetType();

// act
var result = timeConverter.Convert(value, sfType, csharpType);
var result = timeConverter.Convert(value, sfType, csharpType, TimeZoneInfo.Local);

// assert
Assert.AreEqual(expected, result);
Expand Down
9 changes: 6 additions & 3 deletions Snowflake.Data/Client/SnowflakeDbDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ public T GetObject<T>(int ordinal)
case string stringValue:
{
var json = JObject.Parse(stringValue);
return JsonToStructuredTypeConverter.ConvertObject<T>(fields, json);
var sessionTimezone = resultSet.sfStatement.SfSession.GetSessionTimezone();
return JsonToStructuredTypeConverter.ConvertObject<T>(fields, json, sessionTimezone);
}
case Dictionary<string, object> structArray:
return ArrowConverter.ConvertObject<T>(structArray);
Expand Down Expand Up @@ -303,7 +304,8 @@ public T[] GetArray<T>(int ordinal)
case string stringValue:
{
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);
}
case List<object> listArray:
return ArrowConverter.ConvertArray<T>(listArray);
Expand Down Expand Up @@ -336,7 +338,8 @@ public Dictionary<TKey, TValue> GetMap<TKey, TValue>(int ordinal)
case string stringValue:
{
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);
}
case Dictionary<object, object> mapArray:
return ArrowConverter.ConvertMap<TKey, TValue>(mapArray);
Expand Down
17 changes: 14 additions & 3 deletions Snowflake.Data/Core/ArrowResultChunk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using Apache.Arrow;
using Apache.Arrow.Types;
using Snowflake.Data.Client;

namespace Snowflake.Data.Core
{
Expand Down Expand Up @@ -140,7 +141,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 @@ -324,6 +325,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 @@ -332,7 +339,9 @@ 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);
var localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime.UtcDateTime, sessionTimezone);
return new DateTimeOffset(localDateTime, sessionTimezone.GetUtcOffset(localDateTime));
}
else
{
Expand All @@ -342,7 +351,9 @@ 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);
var localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime.UtcDateTime, sessionTimezone);
return new DateTimeOffset(localDateTime, sessionTimezone.GetUtcOffset(localDateTime));
}

case SFDataType.TIMESTAMP_NTZ:
Expand Down
Loading
Loading