diff --git a/Snowflake.Data.Tests/Client/Address.cs b/Snowflake.Data.Tests/Client/Address.cs new file mode 100644 index 000000000..66ba0bc36 --- /dev/null +++ b/Snowflake.Data.Tests/Client/Address.cs @@ -0,0 +1,20 @@ +namespace Snowflake.Data.Tests.Client +{ + public class Address + { + public string city { get; set; } + public string state { get; set; } + public Zip zip { get; set; } + + public Address() + { + } + + public Address(string city, string state, Zip zip) + { + this.city = city; + this.state = state; + this.zip = zip; + } + } +} diff --git a/Snowflake.Data.Tests/Client/AllNullableUnstructuredTypesClass.cs b/Snowflake.Data.Tests/Client/AllNullableUnstructuredTypesClass.cs new file mode 100644 index 000000000..aa8329885 --- /dev/null +++ b/Snowflake.Data.Tests/Client/AllNullableUnstructuredTypesClass.cs @@ -0,0 +1,28 @@ +using System; + +namespace Snowflake.Data.Tests.Client +{ + public class AllNullableUnstructuredTypesClass + { + public string StringValue { get; set; } + public char? CharValue { get; set; } + public byte? ByteValue { get; set; } + public sbyte? SByteValue { get; set; } + public short? ShortValue { get; set; } + public ushort? UShortValue { get; set; } + public int? IntValue { get; set; } + public int? UIntValue { get; set; } + public long? LongValue { get; set; } + public long? ULongValue { get; set; } + public float? FloatValue { get; set; } + public double? DoubleValue { get; set; } + public decimal? DecimalValue { get; set; } + public bool? BooleanValue { get; set; } + public Guid? GuidValue { get; set; } + public DateTime? DateTimeValue { get; set; } + public DateTimeOffset? DateTimeOffsetValue { get; set; } + public TimeSpan? TimeSpanValue { get; set; } + public byte[] BinaryValue { get; set; } + public string SemiStructuredValue { get; set; } + } +} diff --git a/Snowflake.Data.Tests/Client/AllUnstructuredTypesClass.cs b/Snowflake.Data.Tests/Client/AllUnstructuredTypesClass.cs new file mode 100644 index 000000000..0257c21de --- /dev/null +++ b/Snowflake.Data.Tests/Client/AllUnstructuredTypesClass.cs @@ -0,0 +1,28 @@ +using System; + +namespace Snowflake.Data.Tests.Client +{ + public class AllUnstructuredTypesClass + { + public string StringValue { get; set; } + public char CharValue { get; set; } + public byte ByteValue { get; set; } + public sbyte SByteValue { get; set; } + public short ShortValue { get; set; } + public ushort UShortValue { get; set; } + public int IntValue { get; set; } + public int UIntValue { get; set; } + public long LongValue { get; set; } + public long ULongValue { get; set; } + public float FloatValue { get; set; } + public double DoubleValue { get; set; } + public decimal DecimalValue { get; set; } + public bool BooleanValue { get; set; } + public Guid GuidValue { get; set; } + public DateTime DateTimeValue { get; set; } + public DateTimeOffset DateTimeOffsetValue { get; set; } + public TimeSpan TimeSpanValue { get; set; } + public byte[] BinaryValue { get; set; } + public string SemiStructuredValue { get; set; } + } +} diff --git a/Snowflake.Data.Tests/Client/AnnotatedClassForConstructorConstruction.cs b/Snowflake.Data.Tests/Client/AnnotatedClassForConstructorConstruction.cs new file mode 100644 index 000000000..bdf30b651 --- /dev/null +++ b/Snowflake.Data.Tests/Client/AnnotatedClassForConstructorConstruction.cs @@ -0,0 +1,22 @@ +using Snowflake.Data.Client; + +namespace Snowflake.Data.Tests.Client +{ + [SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.CONSTRUCTOR)] + public class AnnotatedClassForConstructorConstruction + { + public string StringValue { get; set; } + public int? IgnoredValue { get; set; } + public int IntegerValue { get; set; } + + public AnnotatedClassForConstructorConstruction() + { + } + + public AnnotatedClassForConstructorConstruction(string stringValue, int integerValue) + { + StringValue = stringValue; + IntegerValue = integerValue; + } + } +} diff --git a/Snowflake.Data.Tests/Client/AnnotatedClassForPropertiesNamesConstruction.cs b/Snowflake.Data.Tests/Client/AnnotatedClassForPropertiesNamesConstruction.cs new file mode 100644 index 000000000..eedc40e25 --- /dev/null +++ b/Snowflake.Data.Tests/Client/AnnotatedClassForPropertiesNamesConstruction.cs @@ -0,0 +1,13 @@ +using Snowflake.Data.Client; + +namespace Snowflake.Data.Tests.Client +{ + [SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.PROPERTIES_NAMES)] + public class AnnotatedClassForPropertiesNamesConstruction + { + [SnowflakeColumn(Name = "x")] + public string StringValue { get; set; } + public int? IgnoredValue { get; set; } + public int IntegerValue { get; set; } + } +} diff --git a/Snowflake.Data.Tests/Client/AnnotatedClassForPropertiesOrderConstruction.cs b/Snowflake.Data.Tests/Client/AnnotatedClassForPropertiesOrderConstruction.cs new file mode 100644 index 000000000..dc08f68bf --- /dev/null +++ b/Snowflake.Data.Tests/Client/AnnotatedClassForPropertiesOrderConstruction.cs @@ -0,0 +1,13 @@ +using Snowflake.Data.Client; + +namespace Snowflake.Data.Tests.Client +{ + [SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.PROPERTIES_ORDER)] + public class AnnotatedClassForPropertiesOrderConstruction + { + public string StringValue { get; set; } + [SnowflakeColumn(IgnoreForPropertyOrder = true)] + public int? IgnoredValue { get; set; } + public int IntegerValue { get; set; } + } +} diff --git a/Snowflake.Data.Tests/Client/DateTimeOffsetWrapper.cs b/Snowflake.Data.Tests/Client/DateTimeOffsetWrapper.cs new file mode 100644 index 000000000..de79d4dd0 --- /dev/null +++ b/Snowflake.Data.Tests/Client/DateTimeOffsetWrapper.cs @@ -0,0 +1,9 @@ +using System; + +namespace Snowflake.Data.Tests.Client +{ + public class DateTimeOffsetWrapper + { + public DateTimeOffset Value { get; set; } + } +} diff --git a/Snowflake.Data.Tests/Client/DateTimeWrapper.cs b/Snowflake.Data.Tests/Client/DateTimeWrapper.cs new file mode 100644 index 000000000..5024248ff --- /dev/null +++ b/Snowflake.Data.Tests/Client/DateTimeWrapper.cs @@ -0,0 +1,9 @@ +using System; + +namespace Snowflake.Data.Tests.Client +{ + public class DateTimeWrapper + { + public DateTime Value { get; set; } + } +} diff --git a/Snowflake.Data.Tests/Client/Grades.cs b/Snowflake.Data.Tests/Client/Grades.cs new file mode 100644 index 000000000..e7cd9df8a --- /dev/null +++ b/Snowflake.Data.Tests/Client/Grades.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; + +namespace Snowflake.Data.Tests.Client +{ + public class Grades + { + public string[] Names { get; set; } + + public Grades() + { + } + + public Grades(string[] names) + { + Names = names; + } + } + + public class GradesWithList + { + public List Names { get; set; } + + public GradesWithList() + { + } + + public GradesWithList(List names) + { + Names = names; + } + } + + public class GradesWithMap + { + public Dictionary Names { get; set; } + + public GradesWithMap() + { + } + + public GradesWithMap(Dictionary names) + { + Names = names; + } + } +} diff --git a/Snowflake.Data.Tests/Client/Identity.cs b/Snowflake.Data.Tests/Client/Identity.cs new file mode 100644 index 000000000..49678a50d --- /dev/null +++ b/Snowflake.Data.Tests/Client/Identity.cs @@ -0,0 +1,34 @@ +namespace Snowflake.Data.Tests.Client +{ + public class Identity + { + public string Name { get; set; } + + public Identity() + { + } + + public Identity(string name) + { + Name = name; + } + + protected bool Equals(Identity other) + { + return Name == other.Name; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Identity)obj); + } + + public override int GetHashCode() + { + return (Name != null ? Name.GetHashCode() : 0); + } + } +} diff --git a/Snowflake.Data.Tests/Client/ObjectArrayMapWrapper.cs b/Snowflake.Data.Tests/Client/ObjectArrayMapWrapper.cs new file mode 100644 index 000000000..2f7929b43 --- /dev/null +++ b/Snowflake.Data.Tests/Client/ObjectArrayMapWrapper.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Snowflake.Data.Tests.Client +{ + public class ObjectArrayMapWrapper + { + public Identity ObjectValue { get; set; } + public List ListValue { get; set; } + public string[] ArrayValue { get; set; } + public IList IListValue { get; set; } + public Dictionary MapValue { get; set; } + public IDictionary IMapValue { get; set; } + } +} diff --git a/Snowflake.Data.Tests/Client/StringWrapper.cs b/Snowflake.Data.Tests/Client/StringWrapper.cs new file mode 100644 index 000000000..7f5947929 --- /dev/null +++ b/Snowflake.Data.Tests/Client/StringWrapper.cs @@ -0,0 +1,7 @@ +namespace Snowflake.Data.Tests.Client +{ + public class StringWrapper + { + public string Value { get; set; } + } +} diff --git a/Snowflake.Data.Tests/Client/Zip.cs b/Snowflake.Data.Tests/Client/Zip.cs new file mode 100644 index 000000000..83c5e9c21 --- /dev/null +++ b/Snowflake.Data.Tests/Client/Zip.cs @@ -0,0 +1,40 @@ +namespace Snowflake.Data.Tests.Client +{ + public class Zip + { + public string prefix { get; set; } + public string postfix { get; set; } + + public Zip() + { + } + + public Zip(string prefix, string postfix) + { + this.prefix = prefix; + this.postfix = postfix; + } + + protected bool Equals(Zip other) + { + return prefix == other.prefix && postfix == other.postfix; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Zip)obj); + } + + public override int GetHashCode() + { +#if NETFRAMEWORK + return prefix.GetHashCode() * 177 + postfix.GetHashCode(); +#else + return System.HashCode.Combine(prefix, postfix); +#endif + } + } +} diff --git a/Snowflake.Data.Tests/IntegrationTests/SFBindTestIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFBindTestIT.cs index 00a1857a2..08da1cecf 100755 --- a/Snowflake.Data.Tests/IntegrationTests/SFBindTestIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFBindTestIT.cs @@ -19,7 +19,7 @@ namespace Snowflake.Data.Tests.IntegrationTests { - [TestFixture] + [TestFixture] class SFBindTestIT : SFBaseTest { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); @@ -27,12 +27,12 @@ class SFBindTestIT : SFBaseTest [Test] public void TestArrayBind() { - + using (IDbConnection conn = new SnowflakeDbConnection()) { conn.ConnectionString = ConnectionString; conn.Open(); - + CreateOrReplaceTable(conn, TableName, new [] { "cola INTEGER", @@ -208,7 +208,7 @@ public void TestBindValue() { dbConnection.ConnectionString = ConnectionString; dbConnection.Open(); - + CreateOrReplaceTable(dbConnection, TableName, new[] { "intData NUMBER", @@ -222,7 +222,7 @@ public void TestBindValue() "dateTimeData DATETIME", "dateTimeWithTimeZone TIMESTAMP_TZ" }); - + foreach (DbType type in Enum.GetValues(typeof(DbType))) { bool isTypeSupported = true; @@ -299,7 +299,7 @@ public void TestBindValue() param.Value = Encoding.UTF8.GetBytes("BinaryData"); break; default: - // Not supported + // Not supported colName = "stringData"; isTypeSupported = false; break; @@ -361,7 +361,7 @@ public void TestBindValueWithSFDataType() dbConnection.Open(); foreach (SFDataType type in Enum.GetValues(typeof(SFDataType))) { - if (!type.Equals(SFDataType.None)) + if (!type.Equals(SFDataType.None) && !type.Equals(SFDataType.MAP)) { bool isTypeSupported = true; string[] columns; @@ -381,7 +381,7 @@ public void TestBindValueWithSFDataType() "unsupportedType VARCHAR" }; } - + CreateOrReplaceTable(dbConnection, TableName, columns); using (IDbCommand command = dbConnection.CreateCommand()) @@ -437,7 +437,7 @@ public void TestBindValueWithSFDataType() Assert.AreEqual(1, rowsInserted); } // DB rejects query if param type is VARIANT, OBJECT or ARRAY - else if (!type.Equals(SFDataType.VARIANT) && + else if (!type.Equals(SFDataType.VARIANT) && !type.Equals(SFDataType.OBJECT) && !type.Equals(SFDataType.ARRAY)) { @@ -492,7 +492,7 @@ public void TestParameterCollection() p2.ParameterName = "2"; p1.DbType = DbType.Int16; p2.Value = 2; - + var p3 = cmd.CreateParameter(); p2.ParameterName = "2"; @@ -507,7 +507,7 @@ public void TestParameterCollection() ((SnowflakeDbParameterCollection)cmd.Parameters).AddRange(parameters); Assert.Throws( () => { cmd.Parameters.CopyTo(parameters, 5); }); - + Assert.AreEqual(3, cmd.Parameters.Count); Assert.IsTrue(cmd.Parameters.Contains(p2)); Assert.IsTrue(cmd.Parameters.Contains("2")); @@ -518,7 +518,7 @@ public void TestParameterCollection() Assert.AreEqual(2, cmd.Parameters.Count); Assert.AreSame(p1, cmd.Parameters[0]); - cmd.Parameters.RemoveAt(0); + cmd.Parameters.RemoveAt(0); Assert.AreSame(p3, cmd.Parameters[0]); cmd.Parameters.Clear(); @@ -536,7 +536,7 @@ public void TestPutArrayBind() { conn.ConnectionString = ConnectionString; conn.Open(); - + CreateOrReplaceTable(conn, TableName, new [] { "cola INTEGER", @@ -544,7 +544,7 @@ public void TestPutArrayBind() "colc DATE", "cold TIME", "cole TIMESTAMP_NTZ", - "colf TIMESTAMP_TZ" + "colf TIMESTAMP_TZ" }); using (IDbCommand cmd = conn.CreateCommand()) @@ -579,7 +579,7 @@ public void TestPutArrayBind() p2.DbType = DbType.String; p2.Value = arrstring.ToArray(); cmd.Parameters.Add(p2); - + DateTime date1 = DateTime.ParseExact("2000-01-01 00:00:00.0000000", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); DateTime date2 = DateTime.ParseExact("2020-05-11 23:59:59.9999999", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); DateTime date3 = DateTime.ParseExact("2021-07-22 23:59:59.9999999", "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture); @@ -645,7 +645,7 @@ public void TestPutArrayBind() p6.DbType = DbType.DateTimeOffset; p6.Value = arrTz.ToArray(); cmd.Parameters.Add(p6); - + var count = cmd.ExecuteNonQuery(); Assert.AreEqual(total * 3, count); @@ -657,18 +657,18 @@ public void TestPutArrayBind() conn.Close(); } } - + [Test] public void TestPutArrayBindWorkDespiteOtTypeNameHandlingAuto() { JsonConvert.DefaultSettings = () => new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }; - + using (IDbConnection conn = new SnowflakeDbConnection(ConnectionString)) { conn.Open(); - + CreateOrReplaceTable(conn, TableName, new [] { "cola REAL", @@ -682,7 +682,7 @@ public void TestPutArrayBindWorkDespiteOtTypeNameHandlingAuto() cmd.CommandText = insertCommand; var total = 250; - + List arrdouble = new List(); List arrstring = new List(); List arrint = new List(); @@ -691,11 +691,11 @@ public void TestPutArrayBindWorkDespiteOtTypeNameHandlingAuto() arrdouble.Add(i * 10 + 1); arrdouble.Add(i * 10 + 2); arrdouble.Add(i * 10 + 3); - + arrstring.Add("stra"+i); arrstring.Add("strb"+i); arrstring.Add("strc"+i); - + arrint.Add(i * 10 + 1); arrint.Add(i * 10 + 2); arrint.Add(i * 10 + 3); @@ -705,13 +705,13 @@ public void TestPutArrayBindWorkDespiteOtTypeNameHandlingAuto() p1.DbType = DbType.Double; p1.Value = arrdouble.ToArray(); cmd.Parameters.Add(p1); - + var p2 = cmd.CreateParameter(); p2.ParameterName = "2"; p2.DbType = DbType.String; p2.Value = arrstring.ToArray(); cmd.Parameters.Add(p2); - + var p3 = cmd.CreateParameter(); p3.ParameterName = "3"; p3.DbType = DbType.Int32; @@ -737,7 +737,7 @@ public void TestPutArrayIntegerBind() { conn.ConnectionString = ConnectionString; conn.Open(); - + CreateOrReplaceTable(conn, TableName, new [] { "cola INTEGER" @@ -835,7 +835,7 @@ public void TestExplicitDbTypeAssignmentForArrayValue() conn.Close(); } } - + private const string FormatYmd = "yyyy/MM/dd"; private const string FormatHms = "HH\\:mm\\:ss"; private const string FormatHmsf = "HH\\:mm\\:ss\\.fff"; @@ -869,7 +869,7 @@ public void TestExplicitDbTypeAssignmentForArrayValue() [TestCase(ResultFormat.ARROW, SFTableType.Hybrid, SFDataType.TIMESTAMP_NTZ, 6, DbType.DateTime, FormatYmdHms, null)] [TestCase(ResultFormat.ARROW, SFTableType.Hybrid, SFDataType.TIMESTAMP_TZ, 6, DbType.DateTimeOffset, FormatYmdHmsZ, null)] [TestCase(ResultFormat.ARROW, SFTableType.Hybrid, SFDataType.TIMESTAMP_LTZ, 6, DbType.DateTimeOffset, FormatYmdHmsZ, null)] - // ICEBERG Tables; require env variables: ICEBERG_EXTERNAL_VOLUME, ICEBERG_CATALOG, ICEBERG_BASE_LOCATION. + // ICEBERG Tables; require env variables: ICEBERG_EXTERNAL_VOLUME, ICEBERG_CATALOG, ICEBERG_BASE_LOCATION. [TestCase(ResultFormat.JSON, SFTableType.Iceberg, SFDataType.DATE, null, DbType.Date, FormatYmd, null)] [TestCase(ResultFormat.JSON, SFTableType.Iceberg, SFDataType.TIME, null, DbType.Time, FormatHms, null)] [TestCase(ResultFormat.JSON, SFTableType.Iceberg, SFDataType.TIME, 6, DbType.Time, FormatHmsf, null)] @@ -897,7 +897,7 @@ public void TestDateTimeBinding(ResultFormat resultFormat, SFTableType tableType var smallBatchRowCount = 2; var bigBatchRowCount = bindingThreshold / 2; s_logger.Info(testCase); - + using (IDbConnection conn = new SnowflakeDbConnection(ConnectionString)) { conn.Open(); @@ -906,13 +906,13 @@ public void TestDateTimeBinding(ResultFormat resultFormat, SFTableType tableType if (!timeZone.IsNullOrEmpty()) // Driver ignores this setting and relies on local environment timezone conn.ExecuteNonQuery($"alter session set TIMEZONE = '{timeZone}'"); - CreateOrReplaceTable(conn, - TableName, - tableType.TableDDLCreationPrefix(), + CreateOrReplaceTable(conn, + TableName, + tableType.TableDDLCreationPrefix(), new[] { "id number(10,0) not null primary key", // necessary only for HYBRID tables - $"ts {columnWithPrecision}" - }, + $"ts {columnWithPrecision}" + }, tableType.TableDDLCreationFlags()); // Act+Assert @@ -938,7 +938,7 @@ public void TestDateTimeBinding(ResultFormat resultFormat, SFTableType tableType Assert.AreEqual(1+smallBatchRowCount+bigBatchRowCount, row); } } - + private void InsertSingleRecord(IDbConnection conn, string sqlInsert, DbType binding, int identifier, ExpectedTimestampWrapper ts) { using (var insert = conn.CreateCommand(sqlInsert)) @@ -958,7 +958,7 @@ private void InsertSingleRecord(IDbConnection conn, string sqlInsert, DbType bin // Act s_logger.Info(sqlInsert); var rowsAffected = insert.ExecuteNonQuery(); - + // Assert Assert.AreEqual(1, rowsAffected); Assert.IsNull(((SnowflakeDbCommand)insert).GetBindStage()); @@ -980,11 +980,11 @@ private void InsertMultipleRecords(IDbConnection conn, string sqlInsert, DbType { insert.Add("2", binding, Enumerable.Repeat(ts.GetDateTime(), rowsCount).ToArray()); } - + // Act s_logger.Debug(sqlInsert); var rowsAffected = insert.ExecuteNonQuery(); - + // Assert Assert.AreEqual(rowsCount, rowsAffected); if (shouldUseBinding) @@ -993,11 +993,11 @@ private void InsertMultipleRecords(IDbConnection conn, string sqlInsert, DbType Assert.IsNull(((SnowflakeDbCommand)insert).GetBindStage()); } } - - private static string ColumnTypeWithPrecision(SFDataType columnType, Int32? columnPrecision) + + private static string ColumnTypeWithPrecision(SFDataType columnType, Int32? columnPrecision) => columnPrecision != null ? $"{columnType}({columnPrecision})" : $"{columnType}"; } - + class ExpectedTimestampWrapper { private readonly SFDataType _columnType; @@ -1051,7 +1051,7 @@ internal void AssertEqual(object actual, string comparisonFormat, string faultMe internal DateTime GetDateTime() => _expectedDateTime ?? throw new Exception($"Column {_columnType} is not matching the expected value type {typeof(DateTime)}"); internal DateTimeOffset GetDateTimeOffset() => _expectedDateTimeOffset ?? throw new Exception($"Column {_columnType} is not matching the expected value type {typeof(DateTime)}"); - + internal static bool IsOffsetType(SFDataType type) => type == SFDataType.TIMESTAMP_LTZ || type == SFDataType.TIMESTAMP_TZ; } } diff --git a/Snowflake.Data.Tests/IntegrationTests/SFDbCommandIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFDbCommandIT.cs index cd30779aa..5aa01ee46 100755 --- a/Snowflake.Data.Tests/IntegrationTests/SFDbCommandIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFDbCommandIT.cs @@ -955,10 +955,11 @@ public void TestExecuteWithMaxRetryReached() stopwatch.Stop(); var totalDelaySeconds = 1 + 2 + 4 + 8 + 16 + 16 + 16 + 16; + const int MillisecondsDifferenceToAccept = 5; // retry 8 times with backoff 1, 2, 4, 8, 16, 16, 16, 16 seconds // but should not delay more than another 16 seconds Assert.Less(stopwatch.ElapsedMilliseconds, (totalDelaySeconds + 20) * 1000); - Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, totalDelaySeconds * 1000); + Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds + MillisecondsDifferenceToAccept, totalDelaySeconds * 1000); } } diff --git a/Snowflake.Data.Tests/IntegrationTests/SFDbFactoryIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFDbFactoryIT.cs index a9fb3b43f..ad59a879f 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFDbFactoryIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFDbFactoryIT.cs @@ -2,11 +2,12 @@ * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. */ +using NUnit.Framework; +using System.Data; +using System.Data.Common; + namespace Snowflake.Data.Tests.IntegrationTests { - using NUnit.Framework; - using System.Data; - using System.Data.Common; [TestFixture] class SFDbFactoryIT : SFBaseTest @@ -24,7 +25,7 @@ class SFDbFactoryIT : SFBaseTest // In .NET Standard, DbProviderFactories is gone. // Reference https://weblog.west-wind.com/posts/2017/Nov/27/Working-around-the-lack-of-dynamic-DbProviderFactory-loading-in-NET-Core // for more details - _factory = Client.SnowflakeDbFactory.Instance; + _factory = Snowflake.Data.Client.SnowflakeDbFactory.Instance; #endif _command = _factory.CreateCommand(); diff --git a/Snowflake.Data.Tests/IntegrationTests/SFReusableChunkTest.cs b/Snowflake.Data.Tests/IntegrationTests/SFReusableChunkTest.cs index 1dcc4ddc2..be2142936 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFReusableChunkTest.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFReusableChunkTest.cs @@ -1,15 +1,15 @@ using System; +using Snowflake.Data.Client; using Snowflake.Data.Tests.Util; +using NUnit.Framework; +using System.Data; +using System.IO; +using System.Threading.Tasks; +using Snowflake.Data.Core; namespace Snowflake.Data.Tests.IntegrationTests { - using NUnit.Framework; - using System.Data; - using System.IO; - using Core; - using Client; - using System.Threading.Tasks; - + [TestFixture, NonParallelizable] class SFReusableChunkTest : SFBaseTest { @@ -20,7 +20,7 @@ public void TestDelCharPr431() { conn.ConnectionString = ConnectionString; conn.Open(); - + SessionParameterAlterer.SetResultFormat(conn, ResultFormat.JSON); CreateOrReplaceTable(conn, TableName, new []{"col STRING"}); @@ -63,12 +63,12 @@ public void TestParseJson() { IChunkParserFactory previous = ChunkParserFactory.Instance; ChunkParserFactory.Instance = new TestChunkParserFactory(1); - + using (IDbConnection conn = new SnowflakeDbConnection()) { conn.ConnectionString = ConnectionString; conn.Open(); - + SessionParameterAlterer.SetResultFormat(conn, ResultFormat.JSON); CreateOrReplaceTable(conn, TableName, new []{"src VARIANT"}); @@ -78,8 +78,8 @@ public void TestParseJson() string insertCommand = $@" -- borrowed from https://docs.snowflake.com/en/user-guide/querying-semistructured.html#sample-data-used-in-examples insert into {TableName} ( -select parse_json('{{ - ""date"" : ""2017 - 04 - 28"", +select parse_json('{{ + ""date"" : ""2017 - 04 - 28"", ""dealership"" : ""Valley View Auto Sales"", ""salesperson"" : {{ ""id"": ""55"", @@ -125,12 +125,12 @@ public void TestChunkRetry() { IChunkParserFactory previous = ChunkParserFactory.Instance; ChunkParserFactory.Instance = new TestChunkParserFactory(6); // lower than default retry of 7 - + using (IDbConnection conn = new SnowflakeDbConnection()) { conn.ConnectionString = ConnectionString; conn.Open(); - + SessionParameterAlterer.SetResultFormat(conn, ResultFormat.JSON); CreateOrReplaceTable(conn, TableName, new []{"col STRING"}); @@ -172,15 +172,15 @@ public void TestChunkRetry() [Test] public void TestExceptionThrownWhenChunkDownloadRetryCountExceeded() - { + { IChunkParserFactory previous = ChunkParserFactory.Instance; ChunkParserFactory.Instance = new TestChunkParserFactory(8); // larger than default max retry of 7 - + using (IDbConnection conn = new SnowflakeDbConnection()) { conn.ConnectionString = ConnectionString; conn.Open(); - + CreateOrReplaceTable(conn, TableName, new []{"col STRING"}); IDbCommand cmd = conn.CreateCommand(); @@ -188,7 +188,7 @@ public void TestExceptionThrownWhenChunkDownloadRetryCountExceeded() int largeTableRowCount = 100000; string insertCommand = $"insert into {TableName}(select hex_decode_string(hex_encode('snow') || '7F' || hex_encode('FLAKE')) from table(generator(rowcount => {largeTableRowCount})))"; - cmd.CommandText = insertCommand; + cmd.CommandText = insertCommand; IDataReader insertReader = cmd.ExecuteReader(); Assert.AreEqual(largeTableRowCount, insertReader.RecordsAffected); @@ -223,13 +223,13 @@ class TestChunkParserFactory : IChunkParserFactory { private int _exceptionsThrown; private readonly int _expectedExceptionsNumber; - + public TestChunkParserFactory(int exceptionsToThrow) { _expectedExceptionsNumber = exceptionsToThrow; _exceptionsThrown = 0; } - + public IChunkParser GetParser(ResultFormat resultFormat, Stream stream) { if (++_exceptionsThrown <= _expectedExceptionsNumber) @@ -242,9 +242,9 @@ public IChunkParser GetParser(ResultFormat resultFormat, Stream stream) class ThrowingReusableChunkParser : IChunkParser { public Task ParseChunk(IResultChunk chunk) - { + { throw new Exception("json parsing error."); } } } -} \ No newline at end of file +} diff --git a/Snowflake.Data.Tests/IntegrationTests/StructuredArraysIT.cs b/Snowflake.Data.Tests/IntegrationTests/StructuredArraysIT.cs new file mode 100644 index 000000000..afc31ea54 --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/StructuredArraysIT.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core; +using Snowflake.Data.Tests.Client; +using Snowflake.Data.Tests.Util; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + [TestFixture] + public class StructuredArraysIT: StructuredTypesIT + { + [Test] + public void TestSelectArray() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arraySFString = "ARRAY_CONSTRUCT('a','b','c')::ARRAY(TEXT)"; + command.CommandText = $"SELECT {arraySFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(3, array.Length); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, array); + } + } + } + + [Test] + public void TestSelectArrayOfObjects() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arrayOfObjects = + "ARRAY_CONSTRUCT(OBJECT_CONSTRUCT('name', 'Alex'), OBJECT_CONSTRUCT('name', 'Brian'))::ARRAY(OBJECT(name VARCHAR))"; + command.CommandText = $"SELECT {arrayOfObjects}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(2, array.Length); + CollectionAssert.AreEqual(new[] { new Identity("Alex"), new Identity("Brian") }, array); + } + } + } + + [Test] + public void TestSelectArrayOfArrays() + { + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + // arrange + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arrayOfArrays = "ARRAY_CONSTRUCT(ARRAY_CONSTRUCT('a', 'b'), ARRAY_CONSTRUCT('c', 'd'))::ARRAY(ARRAY(TEXT))"; + command.CommandText = $"SELECT {arrayOfArrays}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(2, array.Length); + CollectionAssert.AreEqual(new[] { new[] { "a", "b" }, new[] { "c", "d" } }, array); + } + } + } + + [Test] + public void TestSelectArrayOfMap() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arrayOfMap = "ARRAY_CONSTRUCT(OBJECT_CONSTRUCT('a', 'b'))::ARRAY(MAP(VARCHAR,VARCHAR))"; + command.CommandText = $"SELECT {arrayOfMap}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray>(0); + + // assert + Assert.AreEqual(1, array.Length); + var map = array[0]; + Assert.NotNull(map); + Assert.AreEqual(1, map.Count); + Assert.AreEqual("b",map["a"]); + } + } + } + + [Test] + [TestCase(@"ARRAY_CONSTRUCT(OBJECT_CONSTRUCT('a', 'b'))::ARRAY(OBJECT)", "{\"a\": \"b\"}")] + [TestCase(@"ARRAY_CONSTRUCT(ARRAY_CONSTRUCT('a', 'b'))::ARRAY(ARRAY)", "[\"a\", \"b\"]")] + [TestCase(@"ARRAY_CONSTRUCT(TO_VARIANT(OBJECT_CONSTRUCT('a', 'b')))::ARRAY(VARIANT)", "{\"a\": \"b\"}")] + public void TestSelectSemiStructuredTypesInArray(string valueSfString, string expectedValue) + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + command.CommandText = $"SELECT {valueSfString}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.NotNull(array); + CollectionAssert.AreEqual(new [] { RemoveWhiteSpaces(expectedValue) }, array.Select(RemoveWhiteSpaces).ToArray()); + } + } + } + + [Test] + public void TestSelectArrayOfIntegers() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arrayOfIntegers = "ARRAY_CONSTRUCT(3, 5, 8)::ARRAY(INTEGER)"; + command.CommandText = $"SELECT {arrayOfIntegers}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(3, array.Length); + CollectionAssert.AreEqual(new[] { 3, 5, 8 }, array); + } + } + } + + [Test] + public void TestSelectArrayOfLong() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arrayOfLongs = "ARRAY_CONSTRUCT(3, 5, 8)::ARRAY(BIGINT)"; + command.CommandText = $"SELECT {arrayOfLongs}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(3, array.Length); + CollectionAssert.AreEqual(new[] { 3L, 5L, 8L }, array); + } + } + } + + [Test] + public void TestSelectArrayOfFloats() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arrayOfFloats = "ARRAY_CONSTRUCT(3.1, 5.2, 8.11)::ARRAY(FLOAT)"; + command.CommandText = $"SELECT {arrayOfFloats}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(3, array.Length); + CollectionAssert.AreEqual(new[] { 3.1f, 5.2f, 8.11f }, array); + } + } + } + + [Test] + public void TestSelectArrayOfDoubles() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arrayOfDoubles = "ARRAY_CONSTRUCT(3.1, 5.2, 8.11)::ARRAY(DOUBLE)"; + command.CommandText = $"SELECT {arrayOfDoubles}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(3, array.Length); + CollectionAssert.AreEqual(new[] { 3.1d, 5.2d, 8.11d }, array); + } + } + } + + [Test] + public void TestSelectStringArrayWithNulls() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arraySFString = "ARRAY_CONSTRUCT('a',NULL,'b')::ARRAY(TEXT)"; + command.CommandText = $"SELECT {arraySFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(3, array.Length); + CollectionAssert.AreEqual(new[] { "a", null, "b" }, array); + } + } + } + + [Test] + public void TestSelectIntArrayWithNulls() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arrayOfNumberSFString = "ARRAY_CONSTRUCT(3,NULL,5)::ARRAY(INTEGER)"; + command.CommandText = $"SELECT {arrayOfNumberSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var array = reader.GetArray(0); + + // assert + Assert.AreEqual(3, array.Length); + CollectionAssert.AreEqual(new int?[] { 3, null, 5 }, array); + } + } + } + + [Test] + public void TestSelectNullArray() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var nullArraySFString = "NULL::ARRAY(TEXT)"; + command.CommandText = $"SELECT {nullArraySFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var nullArray = reader.GetArray(0); + + // assert + Assert.IsNull(nullArray); + } + } + } + + [Test] + public void TestThrowExceptionForInvalidArray() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arraySFString = "ARRAY_CONSTRUCT('x', 'y')::ARRAY"; + command.CommandText = $"SELECT {arraySFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var thrown = Assert.Throws(() => reader.GetArray(0)); + + // assert + SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_DETAILED_ERROR); + Assert.That(thrown.Message, Does.Contain("Failed to read structured type when getting an array")); + Assert.That(thrown.Message, Does.Contain("Method GetArray can be used only for structured array")); + } + } + } + + [Test] + public void TestThrowExceptionForInvalidArrayElement() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arraySFString = "ARRAY_CONSTRUCT('a76dacad-0e35-497b-bf9b-7cd49262b68b', 'z76dacad-0e35-497b-bf9b-7cd49262b68b')::ARRAY(TEXT)"; + command.CommandText = $"SELECT {arraySFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var thrown = Assert.Throws(() => reader.GetArray(0)); + + // assert + SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_ERROR); + Assert.That(thrown.Message, Does.Contain("Failed to read structured type when reading path $[1]")); + } + } + } + + [Test] + public void TestThrowExceptionForNextedInvalidElement() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var arraySFString = @"ARRAY_CONSTRUCT( + OBJECT_CONSTRUCT('x', 'a', 'y', 'b') + )::ARRAY(OBJECT(x VARCHAR, y VARCHAR))"; + command.CommandText = $"SELECT {arraySFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var thrown = Assert.Throws(() => reader.GetArray(0)); + + // assert + SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_DETAILED_ERROR); + Assert.That(thrown.Message, Does.Contain("Failed to read structured type when reading path $[0][1]")); + Assert.That(thrown.Message, Does.Contain("Could not read text type into System.Int32")); + } + } + } + + } +} diff --git a/Snowflake.Data.Tests/IntegrationTests/StructuredMapsIT.cs b/Snowflake.Data.Tests/IntegrationTests/StructuredMapsIT.cs new file mode 100644 index 000000000..86598f91f --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/StructuredMapsIT.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core; +using Snowflake.Data.Tests.Client; +using Snowflake.Data.Tests.Util; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + [TestFixture] + public class StructuredMapsIT: StructuredTypesIT + { + [Test] + public void TestSelectMap() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var addressAsSFString = "OBJECT_CONSTRUCT('city','San Mateo', 'state', 'CA', 'zip', '01-234')::MAP(VARCHAR, VARCHAR)"; + command.CommandText = $"SELECT {addressAsSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var map = reader.GetMap(0); + + // assert + Assert.NotNull(map); + Assert.AreEqual(3, map.Count); + Assert.AreEqual("San Mateo", map["city"]); + Assert.AreEqual("CA", map["state"]); + Assert.AreEqual("01-234", map["zip"]); + } + } + } + + [Test] + public void TestSelectMapWithIntegerKeys() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var mapSfString = "OBJECT_CONSTRUCT('5','San Mateo', '8', 'CA', '13', '01-234')::MAP(INTEGER, VARCHAR)"; + command.CommandText = $"SELECT {mapSfString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var map = reader.GetMap(0); + + // assert + Assert.NotNull(map); + Assert.AreEqual(3, map.Count); + Assert.AreEqual("San Mateo", map[5]); + Assert.AreEqual("CA", map[8]); + Assert.AreEqual("01-234", map[13]); + } + } + } + + [Test] + public void TestSelectMapWithLongKeys() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var mapSfString = "OBJECT_CONSTRUCT('5','San Mateo', '8', 'CA', '13', '01-234')::MAP(INTEGER, VARCHAR)"; + command.CommandText = $"SELECT {mapSfString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var map = reader.GetMap(0); + + // assert + Assert.NotNull(map); + Assert.AreEqual(3, map.Count); + Assert.AreEqual("San Mateo", map[5L]); + Assert.AreEqual("CA", map[8L]); + Assert.AreEqual("01-234", map[13L]); + } + } + } + + [Test] + public void TestSelectMapOfObjects() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var mapWitObjectValueSFString = @"OBJECT_CONSTRUCT( + 'Warsaw', OBJECT_CONSTRUCT('prefix', '01', 'postfix', '234'), + 'San Mateo', OBJECT_CONSTRUCT('prefix', '02', 'postfix', '567') + )::MAP(VARCHAR, OBJECT(prefix VARCHAR, postfix VARCHAR))"; + command.CommandText = $"SELECT {mapWitObjectValueSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var map = reader.GetMap(0); + + // assert + Assert.NotNull(map); + Assert.AreEqual(2, map.Count); + Assert.AreEqual(new Zip("01", "234"), map["Warsaw"]); + Assert.AreEqual(new Zip("02", "567"), map["San Mateo"]); + } + } + } + + [Test] + public void TestSelectMapOfArrays() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var mapWithArrayValueSFString = "OBJECT_CONSTRUCT('a', ARRAY_CONSTRUCT('b', 'c'))::MAP(VARCHAR, ARRAY(TEXT))"; + command.CommandText = $"SELECT {mapWithArrayValueSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var map = reader.GetMap(0); + + // assert + Assert.AreEqual(1, map.Count); + CollectionAssert.AreEqual(new string[] {"a"}, map.Keys); + CollectionAssert.AreEqual(new string[] {"b", "c"}, map["a"]); + } + } + } + + [Test] + public void TestSelectMapOfLists() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var mapWithArrayValueSFString = "OBJECT_CONSTRUCT('a', ARRAY_CONSTRUCT('b', 'c'))::MAP(VARCHAR, ARRAY(TEXT))"; + command.CommandText = $"SELECT {mapWithArrayValueSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var map = reader.GetMap>(0); + + // assert + Assert.AreEqual(1, map.Count); + CollectionAssert.AreEqual(new string[] {"a"}, map.Keys); + CollectionAssert.AreEqual(new string[] {"b", "c"}, map["a"]); + } + } + } + + [Test] + public void TestSelectMapOfMaps() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var mapAsSFString = "OBJECT_CONSTRUCT('a', OBJECT_CONSTRUCT('b', 'c'))::MAP(TEXT, MAP(TEXT, TEXT))"; + command.CommandText = $"SELECT {mapAsSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var map = reader.GetMap>(0); + + // assert + Assert.AreEqual(1, map.Count); + var nestedMap = map["a"]; + Assert.AreEqual(1, nestedMap.Count); + Assert.AreEqual("c", nestedMap["b"]); + } + } + } + + [Test] + [TestCase(@"OBJECT_CONSTRUCT('x', OBJECT_CONSTRUCT('a', 'b'))::MAP(VARCHAR,OBJECT)", "{\"a\": \"b\"}")] + [TestCase(@"OBJECT_CONSTRUCT('x', ARRAY_CONSTRUCT('a', 'b'))::MAP(VARCHAR,ARRAY)", "[\"a\", \"b\"]")] + [TestCase(@"OBJECT_CONSTRUCT('x', TO_VARIANT(OBJECT_CONSTRUCT('a', 'b')))::MAP(VARCHAR,VARIANT)", "{\"a\": \"b\"}")] + public void TestSelectSemiStructuredTypesInMap(string valueSfString, string expectedValue) + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + command.CommandText = $"SELECT {valueSfString}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var map = reader.GetMap(0); + + // assert + Assert.NotNull(map); + Assert.AreEqual(1, map.Count); + CollectionAssert.AreEqual(RemoveWhiteSpaces(expectedValue), RemoveWhiteSpaces(map["x"])); + } + } + } + + [Test] + public void TestSelectNullMap() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var nullMapSFString = "NULL::MAP(TEXT,TEXT)"; + command.CommandText = $"SELECT {nullMapSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var nullMap = reader.GetMap(0); + + // assert + Assert.IsNull(nullMap); + } + } + } + + [Test] + public void TestThrowExceptionForInvalidMap() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var invalidMapSFString = "OBJECT_CONSTRUCT('x', 'y')::OBJECT"; + command.CommandText = $"SELECT {invalidMapSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var thrown = Assert.Throws(() => reader.GetMap(0)); + + // assert + SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_DETAILED_ERROR); + Assert.That(thrown.Message, Does.Contain("Failed to read structured type when getting a map")); + Assert.That(thrown.Message, Does.Contain("Method GetMap can be used only for structured map")); + } + } + } + + [Test] + public void TestThrowExceptionForInvalidMapElement() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var invalidMapSFString = @"OBJECT_CONSTRUCT( + 'x', 'a76dacad-0e35-497b-bf9b-7cd49262b68b', + 'y', 'z76dacad-0e35-497b-bf9b-7cd49262b68b' + )::MAP(TEXT,TEXT)"; + command.CommandText = $"SELECT {invalidMapSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var thrown = Assert.Throws(() => reader.GetMap(0)); + + // assert + SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_ERROR); + Assert.That(thrown.Message, Does.Contain("Failed to read structured type when reading path $[1]")); + } + } + } + } +} diff --git a/Snowflake.Data.Tests/IntegrationTests/StructuredObjectsIT.cs b/Snowflake.Data.Tests/IntegrationTests/StructuredObjectsIT.cs new file mode 100644 index 000000000..3a6bf3f66 --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/StructuredObjectsIT.cs @@ -0,0 +1,439 @@ +using System.Collections.Generic; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core; +using Snowflake.Data.Tests.Client; +using Snowflake.Data.Tests.Util; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + [TestFixture] + public class StructuredObjectIT: StructuredTypesIT + { + [Test] + public void TestSelectStructuredTypeObject() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var addressAsSFString = "OBJECT_CONSTRUCT('city','San Mateo', 'state', 'CA')::OBJECT(city VARCHAR, state VARCHAR)"; + command.CommandText = $"SELECT {addressAsSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var address = reader.GetObject
(0); + + // assert + Assert.AreEqual("San Mateo", address.city); + Assert.AreEqual("CA", address.state); + Assert.IsNull(address.zip); + } + } + } + + [Test] + public void TestSelectNestedStructuredTypeObject() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var addressAsSFString = + "OBJECT_CONSTRUCT('city','San Mateo', 'state', 'CA', 'zip', OBJECT_CONSTRUCT('prefix', '00', 'postfix', '11'))::OBJECT(city VARCHAR, state VARCHAR, zip OBJECT(prefix VARCHAR, postfix VARCHAR))"; + command.CommandText = $"SELECT {addressAsSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var address = reader.GetObject
(0); + + // assert + Assert.AreEqual("San Mateo", address.city); + Assert.AreEqual("CA", address.state); + Assert.NotNull(address.zip); + Assert.AreEqual("00", address.zip.prefix); + Assert.AreEqual("11", address.zip.postfix); + } + } + } + + [Test] + public void TestSelectObjectWithMap() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectWithMap = "OBJECT_CONSTRUCT('names', OBJECT_CONSTRUCT('Excellent', '6', 'Poor', '1'))::OBJECT(names MAP(VARCHAR,VARCHAR))"; + command.CommandText = $"SELECT {objectWithMap}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var grades = reader.GetObject(0); + + // assert + Assert.NotNull(grades); + Assert.AreEqual(2, grades.Names.Count); + Assert.AreEqual("6", grades.Names["Excellent"]); + Assert.AreEqual("1", grades.Names["Poor"]); + } + } + } + + [Test] + public void TestSelectObjectWithArrays() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectWithArray = "OBJECT_CONSTRUCT('names', ARRAY_CONSTRUCT('Excellent', 'Poor'))::OBJECT(names ARRAY(TEXT))"; + command.CommandText = $"SELECT {objectWithArray}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var grades = reader.GetObject(0); + + // assert + Assert.NotNull(grades); + CollectionAssert.AreEqual(new[] { "Excellent", "Poor" }, grades.Names); + } + } + } + + [Test] + public void TestSelectObjectWithList() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectWithArray = "OBJECT_CONSTRUCT('names', ARRAY_CONSTRUCT('Excellent', 'Poor'))::OBJECT(names ARRAY(TEXT))"; + command.CommandText = $"SELECT {objectWithArray}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var grades = reader.GetObject(0); + + // assert + Assert.NotNull(grades); + CollectionAssert.AreEqual(new List { "Excellent", "Poor" }, grades.Names); + } + } + } + + [Test] + [TestCase(@"OBJECT_CONSTRUCT('Value', OBJECT_CONSTRUCT('a', 'b'))::OBJECT(Value OBJECT)", "{\"a\": \"b\"}")] + [TestCase(@"OBJECT_CONSTRUCT('Value', ARRAY_CONSTRUCT('a', 'b'))::OBJECT(Value ARRAY)", "[\"a\", \"b\"]")] + [TestCase(@"OBJECT_CONSTRUCT('Value', TO_VARIANT(OBJECT_CONSTRUCT('a', 'b')))::OBJECT(Value VARIANT)", "{\"a\": \"b\"}")] + public void TestSelectSemiStructuredTypesInObject(string valueSfString, string expectedValue) + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + command.CommandText = $"SELECT {valueSfString}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var wrapperObject = reader.GetObject(0); + + // assert + Assert.NotNull(wrapperObject); + Assert.AreEqual(RemoveWhiteSpaces(expectedValue), RemoveWhiteSpaces(wrapperObject.Value)); + } + } + } + + [Test] + public void TestSelectStructuredTypesAsNulls() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectSFString = @"OBJECT_CONSTRUCT_KEEP_NULL( + 'ObjectValue', NULL, + 'ListValue', NULL, + 'ArrayValue', NULL, + 'IListValue', NULL, + 'MapValue', NULL, + 'IMapValue', NULL + )::OBJECT( + ObjectValue OBJECT(Name TEXT), + ListValue ARRAY(TEXT), + ArrayValue ARRAY(TEXT), + IListValue ARRAY(TEXT), + MapValue MAP(INTEGER, INTEGER), + IMapValue MAP(INTEGER, INTEGER) + )"; + command.CommandText = $"SELECT {objectSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var objectWithStructuredTypes = reader.GetObject(0); + + // assert + Assert.NotNull(objectWithStructuredTypes); + Assert.IsNull(objectWithStructuredTypes.ObjectValue); + Assert.IsNull(objectWithStructuredTypes.ListValue); + Assert.IsNull(objectWithStructuredTypes.ArrayValue); + Assert.IsNull(objectWithStructuredTypes.IListValue); + Assert.IsNull(objectWithStructuredTypes.MapValue); + Assert.IsNull(objectWithStructuredTypes.IMapValue); + } + } + } + + [Test] + public void TestSelectNestedStructuredTypesNotNull() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectSFString = @"OBJECT_CONSTRUCT_KEEP_NULL( + 'ObjectValue', OBJECT_CONSTRUCT('Name', 'John'), + 'ListValue', ARRAY_CONSTRUCT('a', 'b'), + 'ArrayValue', ARRAY_CONSTRUCT('c'), + 'IListValue', ARRAY_CONSTRUCT('d', 'e'), + 'MapValue', OBJECT_CONSTRUCT('3', '5'), + 'IMapValue', OBJECT_CONSTRUCT('8', '13') + )::OBJECT( + ObjectValue OBJECT(Name TEXT), + ListValue ARRAY(TEXT), + ArrayValue ARRAY(TEXT), + IListValue ARRAY(TEXT), + MapValue MAP(INTEGER, INTEGER), + IMapValue MAP(INTEGER, INTEGER) + )"; + command.CommandText = $"SELECT {objectSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var objectWithStructuredTypes = reader.GetObject(0); + + // assert + Assert.NotNull(objectWithStructuredTypes); + Assert.AreEqual(new Identity("John"), objectWithStructuredTypes.ObjectValue); + CollectionAssert.AreEqual(new [] {"a", "b"}, objectWithStructuredTypes.ListValue); + CollectionAssert.AreEqual(new [] {"c"}, objectWithStructuredTypes.ArrayValue); + CollectionAssert.AreEqual(new [] {"d", "e"}, objectWithStructuredTypes.IListValue); + Assert.AreEqual(typeof(List), objectWithStructuredTypes.IListValue.GetType()); + Assert.AreEqual(1, objectWithStructuredTypes.MapValue.Count); + Assert.AreEqual(5, objectWithStructuredTypes.MapValue[3]); + Assert.AreEqual(1, objectWithStructuredTypes.IMapValue.Count); + Assert.AreEqual(13, objectWithStructuredTypes.IMapValue[8]); + Assert.AreEqual(typeof(Dictionary), objectWithStructuredTypes.IMapValue.GetType()); + } + } + } + + [Test] + public void TestRenamePropertyForPropertiesNamesConstruction() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectSFString = @"OBJECT_CONSTRUCT( + 'IntegerValue', '8', + 'x', 'abc' + )::OBJECT( + IntegerValue INTEGER, + x TEXT + )"; + command.CommandText = $"SELECT {objectSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var objectForAnnotatedClass = reader.GetObject(0); + + // assert + Assert.NotNull(objectForAnnotatedClass); + Assert.AreEqual("abc", objectForAnnotatedClass.StringValue); + Assert.IsNull(objectForAnnotatedClass.IgnoredValue); + Assert.AreEqual(8, objectForAnnotatedClass.IntegerValue); + } + } + } + + [Test] + public void TestIgnorePropertyForPropertiesOrderConstruction() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectSFString = @"OBJECT_CONSTRUCT( + 'x', 'abc', + 'IntegerValue', '8' + )::OBJECT( + x TEXT, + IntegerValue INTEGER + )"; + command.CommandText = $"SELECT {objectSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var objectForAnnotatedClass = reader.GetObject(0); + + // assert + Assert.NotNull(objectForAnnotatedClass); + Assert.AreEqual("abc", objectForAnnotatedClass.StringValue); + Assert.IsNull(objectForAnnotatedClass.IgnoredValue); + Assert.AreEqual(8, objectForAnnotatedClass.IntegerValue); + } + } + } + + [Test] + public void TestConstructorConstructionMethod() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectSFString = @"OBJECT_CONSTRUCT( + 'x', 'abc', + 'IntegerValue', '8' + )::OBJECT( + x TEXT, + IntegerValue INTEGER + )"; + command.CommandText = $"SELECT {objectSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var objectForAnnotatedClass = reader.GetObject(0); + + // assert + Assert.NotNull(objectForAnnotatedClass); + Assert.AreEqual("abc", objectForAnnotatedClass.StringValue); + Assert.IsNull(objectForAnnotatedClass.IgnoredValue); + Assert.AreEqual(8, objectForAnnotatedClass.IntegerValue); + } + } + } + + [Test] + public void TestSelectNullObject() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var nullObjectSFString = "NULL::OBJECT(Name TEXT)"; + command.CommandText = $"SELECT {nullObjectSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var nullObject = reader.GetObject(0); + + // assert + Assert.IsNull(nullObject); + } + } + } + + [Test] + public void TestThrowExceptionForInvalidObject() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectSFString = "OBJECT_CONSTRUCT('x', 'y')::OBJECT"; + command.CommandText = $"SELECT {objectSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var thrown = Assert.Throws(() => reader.GetObject(0)); + + // assert + SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_DETAILED_ERROR); + Assert.That(thrown.Message, Does.Contain("Failed to read structured type when getting an object")); + Assert.That(thrown.Message, Does.Contain("Method GetObject can be used only for structured object")); + } + } + } + + [Test] + public void TestThrowExceptionForInvalidPropertyType() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var objectSFString = "OBJECT_CONSTRUCT('x', 'a', 'y', 'b')::OBJECT(x VARCHAR, y VARCHAR)"; + command.CommandText = $"SELECT {objectSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var thrown = Assert.Throws(() => reader.GetObject(0)); + + // assert + SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_DETAILED_ERROR); + Assert.That(thrown.Message, Does.Contain("Failed to read structured type when reading path $[1].")); + Assert.That(thrown.Message, Does.Contain("Could not read text type into System.Int32")); + } + } + } + } +} diff --git a/Snowflake.Data.Tests/IntegrationTests/StructuredTypesIT.cs b/Snowflake.Data.Tests/IntegrationTests/StructuredTypesIT.cs new file mode 100644 index 000000000..a10841005 --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/StructuredTypesIT.cs @@ -0,0 +1,29 @@ +using System.Linq; +using Snowflake.Data.Client; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + public abstract class StructuredTypesIT : SFBaseTest + { + protected void EnableStructuredTypes(SnowflakeDbConnection connection) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "alter session set ENABLE_STRUCTURED_TYPES_IN_CLIENT_RESPONSE = true"; + command.ExecuteNonQuery(); + command.CommandText = "alter session set IGNORE_CLIENT_VESRION_IN_STRUCTURED_TYPES_RESPONSE = true"; + command.ExecuteNonQuery(); + command.CommandText = "ALTER SESSION SET DOTNET_QUERY_RESULT_FORMAT = JSON"; + command.ExecuteNonQuery(); + } + } + + protected string RemoveWhiteSpaces(string text) + { + var charArrayWithoutWhiteSpaces = text.ToCharArray() + .Where(c => !char.IsWhiteSpace(c)) + .ToArray(); + return new string(charArrayWithoutWhiteSpaces); + } + } +} diff --git a/Snowflake.Data.Tests/IntegrationTests/StructuredTypesWithEmbeddedUnstructuredIT.cs b/Snowflake.Data.Tests/IntegrationTests/StructuredTypesWithEmbeddedUnstructuredIT.cs new file mode 100644 index 000000000..6f88126d9 --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/StructuredTypesWithEmbeddedUnstructuredIT.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core; +using Snowflake.Data.Tests.Client; +using Snowflake.Data.Tests.Util; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + [TestFixture] + public class StructuredTypesWithEmbeddedUnstructuredIT: StructuredTypesIT + { + [Test] + public void TestSelectAllUnstructuredTypesObject() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var timeZone = GetTimeZone(connection); + var expectedOffset = timeZone.GetUtcOffset(DateTime.Parse("2024-07-11 14:20:05")); + var expectedOffsetString = ToOffsetString(expectedOffset); + var allTypesObjectAsSFString = @"OBJECT_CONSTRUCT( + 'StringValue', 'abcąęśźń', + 'CharValue', 'x', + 'ByteValue', 15, + 'SByteValue', -14, + 'ShortValue', 1200, + 'UShortValue', 65000, + 'IntValue', 150150, + 'UIntValue', 151151, + 'LongValue', 9111222333444555666, + 'ULongValue', 9111222333444555666, + 'FloatValue', 1.23, + 'DoubleValue', 1.23, + 'DecimalValue', 1.23, + 'BooleanValue', true, + 'GuidValue', '57af59a1-f010-450a-8c37-8fdc78e6ee93', + 'DateTimeValue', '2024-07-11 14:20:05'::TIMESTAMP_NTZ, + 'DateTimeOffsetValue', '2024-07-11 14:20:05'::TIMESTAMP_LTZ, + 'TimeSpanValue', '14:20:05'::TIME, + 'BinaryValue', TO_BINARY('this is binary data', 'UTF-8'), + 'SemiStructuredValue', OBJECT_CONSTRUCT('a', 'b') + )::OBJECT( + StringValue VARCHAR, + CharValue CHAR, + ByteValue SMALLINT, + SByteValue SMALLINT, + ShortValue SMALLINT, + UShortValue INTEGER, + IntValue INTEGER, + UIntValue INTEGER, + LongValue BIGINT, + ULongValue BIGINT, + FloatValue FLOAT, + DoubleValue DOUBLE, + DecimalValue REAL, + BooleanValue BOOLEAN, + GuidValue TEXT, + DateTimeValue TIMESTAMP_NTZ, + DateTimeOffsetValue TIMESTAMP_LTZ, + TimeSpanValue TIME, + BinaryValue BINARY, + SemiStructuredValue OBJECT + ), '2024-07-11 14:20:05'::TIMESTAMP_LTZ"; + var bytesForBinary = Encoding.UTF8.GetBytes("this is binary data"); + command.CommandText = $"SELECT {allTypesObjectAsSFString}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var allUnstructuredTypesObject = reader.GetObject(0); + + // assert + Assert.NotNull(allUnstructuredTypesObject); + Assert.AreEqual("abcąęśźń", allUnstructuredTypesObject.StringValue); + Assert.AreEqual('x', allUnstructuredTypesObject.CharValue); + Assert.AreEqual(15, allUnstructuredTypesObject.ByteValue); + Assert.AreEqual(-14, allUnstructuredTypesObject.SByteValue); + Assert.AreEqual(1200, allUnstructuredTypesObject.ShortValue); + Assert.AreEqual(65000, allUnstructuredTypesObject.UShortValue); + Assert.AreEqual(150150, allUnstructuredTypesObject.IntValue); + Assert.AreEqual(151151, allUnstructuredTypesObject.UIntValue); + Assert.AreEqual(9111222333444555666, allUnstructuredTypesObject.LongValue); + Assert.AreEqual(9111222333444555666, allUnstructuredTypesObject.ULongValue); + Assert.AreEqual(1.23f, allUnstructuredTypesObject.FloatValue); + Assert.AreEqual(1.23d, allUnstructuredTypesObject.DoubleValue); + Assert.AreEqual(1.23, allUnstructuredTypesObject.DecimalValue); + Assert.AreEqual(true, allUnstructuredTypesObject.BooleanValue); + Assert.AreEqual(Guid.Parse("57af59a1-f010-450a-8c37-8fdc78e6ee93"), allUnstructuredTypesObject.GuidValue); + Assert.AreEqual(DateTime.Parse("2024-07-11 14:20:05"), allUnstructuredTypesObject.DateTimeValue); + Assert.AreEqual(DateTimeOffset.Parse($"2024-07-11 14:20:05 {expectedOffsetString}"), allUnstructuredTypesObject.DateTimeOffsetValue); + Assert.AreEqual(TimeSpan.Parse("14:20:05"), allUnstructuredTypesObject.TimeSpanValue); + CollectionAssert.AreEqual(bytesForBinary, allUnstructuredTypesObject.BinaryValue); + Assert.AreEqual(RemoveWhiteSpaces("{\"a\": \"b\"}"), RemoveWhiteSpaces(allUnstructuredTypesObject.SemiStructuredValue)); + } + } + } + + [Test] + public void TestSelectAllUnstructuredTypesObjectIntoNullableFields() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var timeZone = GetTimeZone(connection); + var expectedOffset = timeZone.GetUtcOffset(DateTime.Parse("2024-07-11 14:20:05")); + var expectedOffsetString = ToOffsetString(expectedOffset); + var allTypesObjectAsSFString = @"OBJECT_CONSTRUCT( + 'StringValue', 'abc', + 'CharValue', 'x', + 'ByteValue', 15, + 'SByteValue', -14, + 'ShortValue', 1200, + 'UShortValue', 65000, + 'IntValue', 150150, + 'UIntValue', 151151, + 'LongValue', 9111222333444555666, + 'ULongValue', 9111222333444555666, + 'FloatValue', 1.23, + 'DoubleValue', 1.23, + 'DecimalValue', 1.23, + 'BooleanValue', true, + 'GuidValue', '57af59a1-f010-450a-8c37-8fdc78e6ee93', + 'DateTimeValue', '2024-07-11 14:20:05'::TIMESTAMP_NTZ, + 'DateTimeOffsetValue', '2024-07-11 14:20:05'::TIMESTAMP_LTZ, + 'TimeSpanValue', '14:20:05'::TIME, + 'BinaryValue', TO_BINARY('this is binary data', 'UTF-8'), + 'SemiStructuredValue', OBJECT_CONSTRUCT('a', 'b') + )::OBJECT( + StringValue VARCHAR, + CharValue CHAR, + ByteValue SMALLINT, + SByteValue SMALLINT, + ShortValue SMALLINT, + UShortValue INTEGER, + IntValue INTEGER, + UIntValue INTEGER, + LongValue BIGINT, + ULongValue BIGINT, + FloatValue FLOAT, + DoubleValue DOUBLE, + DecimalValue REAL, + BooleanValue BOOLEAN, + GuidValue TEXT, + DateTimeValue TIMESTAMP_NTZ, + DateTimeOffsetValue TIMESTAMP_LTZ, + TimeSpanValue TIME, + BinaryValue BINARY, + SemiStructuredValue OBJECT + )"; + var bytesForBinary = Encoding.UTF8.GetBytes("this is binary data"); + command.CommandText = $"SELECT {allTypesObjectAsSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var allUnstructuredTypesObject = reader.GetObject(0); + + // assert + Assert.NotNull(allUnstructuredTypesObject); + Assert.AreEqual("abc", allUnstructuredTypesObject.StringValue); + Assert.AreEqual('x', allUnstructuredTypesObject.CharValue); + Assert.AreEqual(15, allUnstructuredTypesObject.ByteValue); + Assert.AreEqual(-14, allUnstructuredTypesObject.SByteValue); + Assert.AreEqual(1200, allUnstructuredTypesObject.ShortValue); + Assert.AreEqual(65000, allUnstructuredTypesObject.UShortValue); + Assert.AreEqual(150150, allUnstructuredTypesObject.IntValue); + Assert.AreEqual(151151, allUnstructuredTypesObject.UIntValue); + Assert.AreEqual(9111222333444555666, allUnstructuredTypesObject.LongValue); + Assert.AreEqual(9111222333444555666, allUnstructuredTypesObject.ULongValue); // there is a problem with 18111222333444555666 value + Assert.AreEqual(1.23f, allUnstructuredTypesObject.FloatValue); + Assert.AreEqual(1.23d, allUnstructuredTypesObject.DoubleValue); + Assert.AreEqual(1.23, allUnstructuredTypesObject.DecimalValue); + Assert.AreEqual(true, allUnstructuredTypesObject.BooleanValue); + Assert.AreEqual(Guid.Parse("57af59a1-f010-450a-8c37-8fdc78e6ee93"), allUnstructuredTypesObject.GuidValue); + Assert.AreEqual(DateTime.Parse("2024-07-11 14:20:05"), allUnstructuredTypesObject.DateTimeValue); + Assert.AreEqual(DateTimeOffset.Parse($"2024-07-11 14:20:05 {expectedOffsetString}"), allUnstructuredTypesObject.DateTimeOffsetValue); + Assert.AreEqual(TimeSpan.Parse("14:20:05"), allUnstructuredTypesObject.TimeSpanValue); + CollectionAssert.AreEqual(bytesForBinary, allUnstructuredTypesObject.BinaryValue); + Assert.AreEqual(RemoveWhiteSpaces("{\"a\": \"b\"}"), RemoveWhiteSpaces(allUnstructuredTypesObject.SemiStructuredValue)); + } + } + } + + [Test] + public void TestSelectNullIntoUnstructuredTypesObject() + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + var allTypesObjectAsSFString = @"OBJECT_CONSTRUCT_KEEP_NULL( + 'StringValue', NULL, + 'CharValue', NULL, + 'ByteValue', NULL, + 'SByteValue', NULL, + 'ShortValue', NULL, + 'UShortValue', NULL, + 'IntValue', NULL, + 'UIntValue', NULL, + 'LongValue', NULL, + 'ULongValue', NULL, + 'FloatValue', NULL, + 'DoubleValue', NULL, + 'DecimalValue', NULL, + 'BooleanValue', NULL, + 'GuidValue', NULL, + 'DateTimeValue', NULL, + 'DateTimeOffsetValue', NULL, + 'TimeSpanValue', NULL, + 'BinaryValue', NULL, + 'SemiStructuredValue', NULL + )::OBJECT( + StringValue VARCHAR, + CharValue CHAR, + ByteValue SMALLINT, + SByteValue SMALLINT, + ShortValue SMALLINT, + UShortValue INTEGER, + IntValue INTEGER, + UIntValue INTEGER, + LongValue BIGINT, + ULongValue BIGINT, + FloatValue FLOAT, + DoubleValue DOUBLE, + DecimalValue REAL, + BooleanValue BOOLEAN, + GuidValue TEXT, + DateTimeValue TIMESTAMP_NTZ, + DateTimeOffsetValue TIMESTAMP_LTZ, + TimeSpanValue TIME, + BinaryValue BINARY, + SemiStructuredValue OBJECT + )"; + command.CommandText = $"SELECT {allTypesObjectAsSFString}"; + var reader = (SnowflakeDbDataReader)command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act + var allUnstructuredTypesObject = reader.GetObject(0); + + // assert + Assert.NotNull(allUnstructuredTypesObject); + Assert.IsNull(allUnstructuredTypesObject.StringValue); + Assert.IsNull(allUnstructuredTypesObject.CharValue); + Assert.IsNull(allUnstructuredTypesObject.ByteValue); + Assert.IsNull(allUnstructuredTypesObject.SByteValue); + Assert.IsNull(allUnstructuredTypesObject.ShortValue); + Assert.IsNull(allUnstructuredTypesObject.UShortValue); + Assert.IsNull(allUnstructuredTypesObject.IntValue); + Assert.IsNull(allUnstructuredTypesObject.UIntValue); + Assert.IsNull(allUnstructuredTypesObject.LongValue); + Assert.IsNull(allUnstructuredTypesObject.ULongValue); + Assert.IsNull(allUnstructuredTypesObject.FloatValue); + Assert.IsNull(allUnstructuredTypesObject.DoubleValue); + Assert.IsNull(allUnstructuredTypesObject.DecimalValue); + Assert.IsNull(allUnstructuredTypesObject.BooleanValue); + Assert.IsNull(allUnstructuredTypesObject.GuidValue); + Assert.IsNull(allUnstructuredTypesObject.DateTimeValue); + Assert.IsNull(allUnstructuredTypesObject.DateTimeOffsetValue); + Assert.IsNull(allUnstructuredTypesObject.TimeSpanValue); + Assert.IsNull(allUnstructuredTypesObject.BinaryValue); + Assert.IsNull(allUnstructuredTypesObject.SemiStructuredValue); + } + } + } + + [Test] + [TestCaseSource(nameof(DateTimeConversionCases))] + public void TestSelectDateTime(string dbValue, string dbType, DateTime? expectedRaw, DateTime expected) + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + SetTimePrecision(connection, 9); + var rawValueString = $"'{dbValue}'::{dbType}"; + var objectValueString = $"OBJECT_CONSTRUCT('Value', {rawValueString})::OBJECT(Value {dbType})"; + command.CommandText = $"SELECT {rawValueString}, {objectValueString}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act/assert + if (expectedRaw != null) + { + var rawValue = reader.GetDateTime(0); + Assert.AreEqual(expectedRaw, rawValue); + Assert.AreEqual(expectedRaw?.Kind, rawValue.Kind); + } + var wrappedValue = reader.GetObject(1); + Assert.AreEqual(expected, wrappedValue.Value); + Assert.AreEqual(expected.Kind, wrappedValue.Value.Kind); + } + } + } + + internal static IEnumerable DateTimeConversionCases() + { + yield return new object[] { "2024-07-11 14:20:05", SFDataType.TIMESTAMP_NTZ.ToString(), DateTime.Parse("2024-07-11 14:20:05").ToUniversalTime(), DateTime.Parse("2024-07-11 14:20:05").ToUniversalTime() }; + yield return new object[] { "2024-07-11 14:20:05 +5:00", SFDataType.TIMESTAMP_TZ.ToString(), null, DateTime.Parse("2024-07-11 09:20:05").ToUniversalTime() }; + yield return new object[] {"2024-07-11 14:20:05 -7:00", SFDataType.TIMESTAMP_LTZ.ToString(), null, DateTime.Parse("2024-07-11 21:20:05").ToUniversalTime() }; + yield return new object[] { "2024-07-11", SFDataType.DATE.ToString(), DateTime.Parse("2024-07-11").ToUniversalTime(), DateTime.Parse("2024-07-11").ToUniversalTime() }; + yield return new object[] { "2024-07-11 14:20:05.123456789", SFDataType.TIMESTAMP_NTZ.ToString(), DateTime.Parse("2024-07-11 14:20:05.1234567").ToUniversalTime(), DateTime.Parse("2024-07-11 14:20:05.1234568").ToUniversalTime()}; + yield return new object[] { "2024-07-11 14:20:05.123456789 +5:00", SFDataType.TIMESTAMP_TZ.ToString(), null, DateTime.Parse("2024-07-11 09:20:05.1234568").ToUniversalTime() }; + yield return new object[] {"2024-07-11 14:20:05.123456789 -7:00", SFDataType.TIMESTAMP_LTZ.ToString(), null, DateTime.Parse("2024-07-11 21:20:05.1234568").ToUniversalTime() }; + } + + [Test] + [TestCaseSource(nameof(DateTimeOffsetConversionCases))] + public void TestSelectDateTimeOffset(string dbValue, string dbType, DateTime? expectedRaw, DateTimeOffset expected) + { + // arrange + using (var connection = new SnowflakeDbConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + EnableStructuredTypes(connection); + SetTimePrecision(connection, 9); + var rawValueString = $"'{dbValue}'::{dbType}"; + var objectValueString = $"OBJECT_CONSTRUCT('Value', {rawValueString})::OBJECT(Value {dbType})"; + command.CommandText = $"SELECT {rawValueString}, {objectValueString}"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + + // act/assert + if (expectedRaw != null) + { + var rawValue = reader.GetDateTime(0); + Assert.AreEqual(expectedRaw, rawValue); + Assert.AreEqual(expectedRaw?.Kind, rawValue.Kind); + } + var wrappedValue = reader.GetObject(1); + Assert.AreEqual(expected, wrappedValue.Value); + } + } + } + + internal static IEnumerable DateTimeOffsetConversionCases() + { + yield return new object[] {"2024-07-11 14:20:05", SFDataType.TIMESTAMP_NTZ.ToString(), DateTime.Parse("2024-07-11 14:20:05").ToUniversalTime(), DateTimeOffset.Parse("2024-07-11 14:20:05Z")}; + yield return new object[] {"2024-07-11 14:20:05 +5:00", SFDataType.TIMESTAMP_TZ.ToString(), null, DateTimeOffset.Parse("2024-07-11 14:20:05 +5:00")}; + yield return new object[] {"2024-07-11 14:20:05 -7:00", SFDataType.TIMESTAMP_LTZ.ToString(), null, DateTimeOffset.Parse("2024-07-11 14:20:05 -7:00")}; + yield return new object[] {"2024-07-11", SFDataType.DATE.ToString(), DateTime.Parse("2024-07-11").ToUniversalTime(), DateTimeOffset.Parse("2024-07-11Z")}; + yield return new object[] {"2024-07-11 14:20:05.123456789", SFDataType.TIMESTAMP_NTZ.ToString(), DateTime.Parse("2024-07-11 14:20:05.1234567").ToUniversalTime(), DateTimeOffset.Parse("2024-07-11 14:20:05.1234568Z")}; + yield return new object[] {"2024-07-11 14:20:05.123456789 +5:00", SFDataType.TIMESTAMP_TZ.ToString(), null, DateTimeOffset.Parse("2024-07-11 14:20:05.1234568 +5:00")}; + yield return new object[] {"2024-07-11 14:20:05.123456789 -7:00", SFDataType.TIMESTAMP_LTZ.ToString(), null, DateTimeOffset.Parse("2024-07-11 14:20:05.1234568 -7:00")}; + } + + private TimeZoneInfo GetTimeZone(SnowflakeDbConnection connection) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "show parameters like 'timezone'"; + var reader = (SnowflakeDbDataReader) command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + var timeZoneString = reader.GetString(1); + return TimeZoneInfoConverter.FindSystemTimeZoneById(timeZoneString); + } + } + + private string ToOffsetString(TimeSpan timeSpan) + { + var offsetString = timeSpan.ToString(); + var secondsIndex = offsetString.LastIndexOf(":"); + var offsetWithoutSeconds = offsetString.Substring(0, secondsIndex); + return offsetWithoutSeconds.StartsWith("+") || offsetWithoutSeconds.StartsWith("-") + ? offsetWithoutSeconds + : "+" + offsetWithoutSeconds; + } + + private void SetTimePrecision(SnowflakeDbConnection connection, int precision) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = $"ALTER SESSION SET TIMESTAMP_NTZ_OUTPUT_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF{precision}'"; + command.ExecuteNonQuery(); + command.CommandText = $"ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF{precision} TZHTZM'"; + command.ExecuteNonQuery(); + } + } + } +} diff --git a/Snowflake.Data.Tests/SFBaseTest.cs b/Snowflake.Data.Tests/SFBaseTest.cs index a6929eed5..1e8e13018 100755 --- a/Snowflake.Data.Tests/SFBaseTest.cs +++ b/Snowflake.Data.Tests/SFBaseTest.cs @@ -444,4 +444,28 @@ public void AfterTest(ITest test) public ActionTargets Targets => ActionTargets.Test | ActionTargets.Suite; } + + public class IgnoreOnEnvIsSetAttribute : Attribute, ITestAction + { + private readonly string _key; + + public IgnoreOnEnvIsSetAttribute(string key) + { + _key = key; + } + + public void BeforeTest(ITest test) + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(_key))) + { + Assert.Ignore("Test is ignored when environment variable {0} is set ", _key); + } + } + + public void AfterTest(ITest test) + { + } + + public ActionTargets Targets => ActionTargets.Test | ActionTargets.Suite; + } } diff --git a/Snowflake.Data.Tests/UnitTests/StructurePathTest.cs b/Snowflake.Data.Tests/UnitTests/StructurePathTest.cs new file mode 100644 index 000000000..b705575d8 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/StructurePathTest.cs @@ -0,0 +1,97 @@ +using NUnit.Framework; +using Snowflake.Data.Core.Converter; + +namespace Snowflake.Data.Tests.UnitTests +{ + [TestFixture] + public class StructurePathTest + { + [Test] + public void TestRootPath() + { + // act + var value = new StructurePath().ToString(); + + // assert + Assert.AreEqual("$", value); + } + + [Test] + public void TestAddPropertyIndex() + { + // arrange + var path = new StructurePath(); + + // act + var value = path.WithPropertyIndex(2); + + // assert + Assert.AreEqual("$[2]", value.ToString()); + } + + [Test] + public void TestAddPropertyIndexToComplexPath() + { + // arrange + var path = new StructurePath().WithPropertyIndex(2); + + // act + var value = path.WithPropertyIndex(1); + + // assert + Assert.AreEqual("$[2][1]", value.ToString()); + } + + [Test] + public void TestAddArrayIndex() + { + // arrange + var path = new StructurePath(); + + // act + var value = path.WithArrayIndex(2); + + // assert + Assert.AreEqual("$[2]", value.ToString()); + } + + [Test] + public void TestAddArrayIndexToComplexPath() + { + // arrange + var path = new StructurePath().WithArrayIndex(2); + + // act + var value = path.WithArrayIndex(1); + + // assert + Assert.AreEqual("$[2][1]", value.ToString()); + } + + [Test] + public void TestAddMapIndex() + { + // arrange + var path = new StructurePath(); + + // act + var value = path.WithMapIndex(2); + + // assert + Assert.AreEqual("$[2]", value.ToString()); + } + + [Test] + public void TestAddMapIndexToComplexPath() + { + // arrange + var path = new StructurePath().WithMapIndex(2); + + // act + var value = path.WithMapIndex(1); + + // assert + Assert.AreEqual("$[2][1]", value.ToString()); + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/StructuredTypesTest.cs b/Snowflake.Data.Tests/UnitTests/StructuredTypesTest.cs new file mode 100644 index 000000000..a10b4660c --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/StructuredTypesTest.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Snowflake.Data.Core; +using Snowflake.Data.Core.Converter; + +namespace Snowflake.Data.Tests.UnitTests +{ + [TestFixture] + public class StructuredTypesTest + { + [Test] + [TestCaseSource(nameof(TimeConversionCases))] + public void TestTimeConversions(string value, string sfTypeString, object expected) + { + // arrange + var timeConverter = new TimeConverter(); + var sfType = (SFDataType) Enum.Parse(typeof(SFDataType), sfTypeString); + var csharpType = expected.GetType(); + + // act + var result = timeConverter.Convert(value, sfType, csharpType); + + // assert + Assert.AreEqual(expected, result); + } + + internal static IEnumerable TimeConversionCases() + { + yield return new object[] {"2024-07-11 14:20:05", SFDataType.TIMESTAMP_NTZ.ToString(), DateTime.Parse("2024-07-11 14:20:05").ToUniversalTime()}; + yield return new object[] {"2024-07-11 14:20:05", SFDataType.TIMESTAMP_NTZ.ToString(), DateTimeOffset.Parse("2024-07-11 14:20:05Z")}; + yield return new object[] {"2024-07-11 14:20:05 +5:00", SFDataType.TIMESTAMP_TZ.ToString(), DateTimeOffset.Parse("2024-07-11 14:20:05 +5:00")}; + yield return new object[] {"2024-07-11 14:20:05 +5:00", SFDataType.TIMESTAMP_TZ.ToString(), DateTime.Parse("2024-07-11 09:20:05").ToUniversalTime()}; + yield return new object[] {"2024-07-11 14:20:05 -7:00", SFDataType.TIMESTAMP_LTZ.ToString(), DateTimeOffset.Parse("2024-07-11 14:20:05 -7:00")}; + yield return new object[] {"2024-07-11 14:20:05 -7:00", SFDataType.TIMESTAMP_LTZ.ToString(), DateTime.Parse("2024-07-11 21:20:05").ToUniversalTime()}; + yield return new object[] {"14:20:05", SFDataType.TIME.ToString(), TimeSpan.Parse("14:20:05")}; + yield return new object[] {"2024-07-11", SFDataType.DATE.ToString(), DateTime.Parse("2024-07-11")}; + yield return new object[] {"2024-07-11 14:20:05.123456", SFDataType.TIMESTAMP_NTZ.ToString(), DateTime.Parse("2024-07-11 14:20:05.123456").ToUniversalTime()}; + yield return new object[] {"2024-07-11 14:20:05.123456", SFDataType.TIMESTAMP_NTZ.ToString(), DateTimeOffset.Parse("2024-07-11 14:20:05.123456Z")}; + yield return new object[] {"2024-07-11 14:20:05.123456 +5:00", SFDataType.TIMESTAMP_TZ.ToString(), DateTimeOffset.Parse("2024-07-11 14:20:05.123456 +5:00")}; + yield return new object[] {"2024-07-11 14:20:05.123456 +5:00", SFDataType.TIMESTAMP_TZ.ToString(), DateTime.Parse("2024-07-11 09:20:05.123456").ToUniversalTime()}; + yield return new object[] {"2024-07-11 14:20:05.123456 -7:00", SFDataType.TIMESTAMP_LTZ.ToString(), DateTimeOffset.Parse("2024-07-11 14:20:05.123456 -7:00")}; + yield return new object[] {"2024-07-11 14:20:05.123456 -7:00", SFDataType.TIMESTAMP_LTZ.ToString(), DateTime.Parse("2024-07-11 21:20:05.123456").ToUniversalTime()}; + yield return new object[] {"14:20:05.123456", SFDataType.TIME.ToString(), TimeSpan.Parse("14:20:05.123456")}; + } + } +} diff --git a/Snowflake.Data.Tests/Util/TimeZoneInfoConverter.cs b/Snowflake.Data.Tests/Util/TimeZoneInfoConverter.cs new file mode 100644 index 000000000..2046a77b3 --- /dev/null +++ b/Snowflake.Data.Tests/Util/TimeZoneInfoConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Snowflake.Data.Tests.Util +{ + public static class TimeZoneInfoConverter + { + private static Dictionary s_mapping = TimeZoneMapping().ToDictionary(x => x.Key, x => x.Value); + + public static TimeZoneInfo FindSystemTimeZoneById(string timeZoneString) + { + try + { + return TimeZoneInfo.FindSystemTimeZoneById(timeZoneString); + } + catch (TimeZoneNotFoundException) + { + if (s_mapping.TryGetValue(timeZoneString, out var alternativeTimeZoneString)) + { + return TimeZoneInfo.FindSystemTimeZoneById(alternativeTimeZoneString); + } + throw new Exception($"Could not recognize time zone: {timeZoneString}"); + } + } + + private static IEnumerable> TimeZoneMapping() + { + yield return new KeyValuePair("America/Los_Angeles", "Pacific Standard Time"); + yield return new KeyValuePair("Europe/Warsaw", "Central European Standard Time"); + yield return new KeyValuePair("Asia/Tokyo", "Tokyo Standard Time"); + } + } + + +} diff --git a/Snowflake.Data/Client/SnowflakeColumn.cs b/Snowflake.Data/Client/SnowflakeColumn.cs new file mode 100644 index 000000000..3019d6112 --- /dev/null +++ b/Snowflake.Data/Client/SnowflakeColumn.cs @@ -0,0 +1,10 @@ +using System; + +namespace Snowflake.Data.Client +{ + public class SnowflakeColumn : Attribute + { + public string Name { get; set; } = null; + public bool IgnoreForPropertyOrder { get; set; } = false; + } +} diff --git a/Snowflake.Data/Client/SnowflakeDbDataReader.cs b/Snowflake.Data/Client/SnowflakeDbDataReader.cs index 20ca1f7ba..2e8d071a2 100755 --- a/Snowflake.Data/Client/SnowflakeDbDataReader.cs +++ b/Snowflake.Data/Client/SnowflakeDbDataReader.cs @@ -5,13 +5,14 @@ using System; using System.Data.Common; using System.Collections; +using System.Collections.Generic; using Snowflake.Data.Core; using System.Data; using System.Threading; using System.Threading.Tasks; using Snowflake.Data.Log; -using System.Text; -using System.IO; +using Newtonsoft.Json.Linq; +using Snowflake.Data.Core.Converter; namespace Snowflake.Data.Client { @@ -30,7 +31,7 @@ public class SnowflakeDbDataReader : DbDataReader private int RecordsAffectedInternal; internal ResultFormat ResultFormat => resultSet.ResultFormat; - + internal SnowflakeDbDataReader(SnowflakeDbCommand command, SFBaseResultSet resultSet) { this.dbCommand = command; @@ -99,7 +100,7 @@ public string GetQueryId() { return resultSet.queryId; } - + private DataTable PopulateSchemaTable(SFBaseResultSet resultSet) { var table = new DataTable("SchemaTable"); @@ -136,7 +137,7 @@ private DataTable PopulateSchemaTable(SFBaseResultSet resultSet) return table; } - + public override bool GetBoolean(int ordinal) { return resultSet.GetBoolean(ordinal); @@ -255,6 +256,75 @@ public override int GetValues(object[] values) return count; } + public T GetObject(int ordinal) + where T : class, new() + { + try + { + var rowType = resultSet.sfResultSetMetaData.rowTypes[ordinal]; + var fields = rowType.fields; + if (fields == null || fields.Count == 0 || !JsonToStructuredTypeConverter.IsObjectType(rowType.type)) + { + throw new StructuredTypesReadingException($"Method GetObject<{typeof(T)}> can be used only for structured object"); + } + var stringValue = GetString(ordinal); + var json = stringValue == null ? null : JObject.Parse(stringValue); + return JsonToStructuredTypeConverter.ConvertObject(fields, json); + } + catch (Exception e) + { + if (e is SnowflakeDbException) + throw; + throw StructuredTypesReadingHandler.ToSnowflakeDbException(e, "when getting an object"); + } + } + + public T[] GetArray(int ordinal) + { + try + { + var rowType = resultSet.sfResultSetMetaData.rowTypes[ordinal]; + var fields = rowType.fields; + if (fields == null || fields.Count == 0 || !JsonToStructuredTypeConverter.IsArrayType(rowType.type)) + { + throw new StructuredTypesReadingException($"Method GetArray<{typeof(T)}> can be used only for structured array"); + } + + var stringValue = GetString(ordinal); + var json = stringValue == null ? null : JArray.Parse(stringValue); + return JsonToStructuredTypeConverter.ConvertArray(fields, json); + } + catch (Exception e) + { + if (e is SnowflakeDbException) + throw; + throw StructuredTypesReadingHandler.ToSnowflakeDbException(e, "when getting an array"); + } + } + + public Dictionary GetMap(int ordinal) + { + try + { + var rowType = resultSet.sfResultSetMetaData.rowTypes[ordinal]; + var fields = rowType.fields; + if (fields == null || fields.Count == 0 || !JsonToStructuredTypeConverter.IsMapType(rowType.type)) + { + throw new StructuredTypesReadingException($"Method GetMap<{typeof(TKey)}, {typeof(TValue)}> can be used only for structured map"); + } + + var stringValue = GetString(ordinal); + var json = stringValue == null ? null : JObject.Parse(stringValue); + return JsonToStructuredTypeConverter.ConvertMap(fields, json); + } + catch (Exception e) + { + if (e is SnowflakeDbException) + throw; + throw StructuredTypesReadingHandler.ToSnowflakeDbException(e, "when getting a map"); + } + } + public override bool IsDBNull(int ordinal) { return resultSet.IsDBNull(ordinal); @@ -300,6 +370,5 @@ public override void Close() resultSet.close(); isClosed = true; } - } } diff --git a/Snowflake.Data/Client/SnowflakeObject.cs b/Snowflake.Data/Client/SnowflakeObject.cs new file mode 100644 index 000000000..4d4f059e8 --- /dev/null +++ b/Snowflake.Data/Client/SnowflakeObject.cs @@ -0,0 +1,9 @@ +using System; + +namespace Snowflake.Data.Client +{ + public class SnowflakeObject: Attribute + { + public SnowflakeObjectConstructionMethod ConstructionMethod { get; set; } = SnowflakeObjectConstructionMethod.PROPERTIES_ORDER; + } +} diff --git a/Snowflake.Data/Client/SnowflakeObjectConstructionMethod.cs b/Snowflake.Data/Client/SnowflakeObjectConstructionMethod.cs new file mode 100644 index 000000000..792b26e42 --- /dev/null +++ b/Snowflake.Data/Client/SnowflakeObjectConstructionMethod.cs @@ -0,0 +1,9 @@ +namespace Snowflake.Data.Client +{ + public enum SnowflakeObjectConstructionMethod + { + PROPERTIES_ORDER, + PROPERTIES_NAMES, + CONSTRUCTOR + } +} diff --git a/Snowflake.Data/Core/Converter/Builder/IObjectBuilder.cs b/Snowflake.Data/Core/Converter/Builder/IObjectBuilder.cs new file mode 100644 index 000000000..851f2e066 --- /dev/null +++ b/Snowflake.Data/Core/Converter/Builder/IObjectBuilder.cs @@ -0,0 +1,13 @@ +using System; + +namespace Snowflake.Data.Core.Converter.Builder +{ + internal interface IObjectBuilder + { + void BuildPart(object value); + + Type MoveNext(string sfPropertyName); + + object Build(); + } +} diff --git a/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByConstructor.cs b/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByConstructor.cs new file mode 100644 index 000000000..f9f1403b6 --- /dev/null +++ b/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByConstructor.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Snowflake.Data.Core.Converter.Builder +{ + internal class ObjectBuilderByConstructor: IObjectBuilder + { + private Type _type; + private List _result; + private Type[] _parameters; + private int _index; + + public ObjectBuilderByConstructor(Type type, int fieldsCount) + { + _type = type; + var matchingConstructors = type.GetConstructors() + .Where(c => c.GetParameters().Length == fieldsCount) + .ToList(); + if (matchingConstructors.Count == 0) + { + throw new StructuredTypesReadingException($"Proper constructor not found for type: {type}"); + } + var constructor = matchingConstructors.First(); + _parameters = constructor.GetParameters().Select(p => p.ParameterType).ToArray(); + _index = -1; + _result = new List(); + } + + public Type MoveNext(string sfPropertyName) + { + _index++; + return _parameters[_index]; + } + + public void BuildPart(object value) + { + _result.Add(value); + } + + public object Build() + { + object[] parameters = _result.ToArray(); + return Activator.CreateInstance(_type, parameters); + } + } +} diff --git a/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByPropertyNames.cs b/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByPropertyNames.cs new file mode 100644 index 000000000..553b429c0 --- /dev/null +++ b/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByPropertyNames.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Snowflake.Data.Client; + +namespace Snowflake.Data.Core.Converter.Builder +{ + internal class ObjectBuilderByPropertyNames: IObjectBuilder + { + private readonly Type _type; + private readonly List> _result; + private PropertyInfo _currentProperty; + private readonly Dictionary _sfToClientPropertyNames; + + public ObjectBuilderByPropertyNames(Type type) + { + _type = type; + _result = new List>(); + _sfToClientPropertyNames = new Dictionary(); + foreach (var property in type.GetProperties()) + { + var sfPropertyName = GetSnowflakeName(property); + _sfToClientPropertyNames.Add(sfPropertyName, property.Name); + } + } + + private string GetSnowflakeName(PropertyInfo propertyInfo) + { + var sfAnnotation = propertyInfo.GetCustomAttributes().FirstOrDefault(); + return string.IsNullOrEmpty(sfAnnotation?.Name) ? propertyInfo.Name : sfAnnotation.Name; + } + + public void BuildPart(object value) + { + _result.Add(Tuple.Create(_currentProperty, value)); + } + + public Type MoveNext(string sfPropertyName) + { + if (!_sfToClientPropertyNames.TryGetValue(sfPropertyName, out var clientPropertyName)) + { + throw new StructuredTypesReadingException($"Could not find property: {sfPropertyName}"); + } + _currentProperty = _type.GetProperty(clientPropertyName); + if (_currentProperty == null) + { + throw new StructuredTypesReadingException($"Could not find property: {sfPropertyName}"); + } + return _currentProperty.PropertyType; + } + + public object Build() + { + var result = Activator.CreateInstance(_type); + _result.ForEach(p => p.Item1.SetValue(result, p.Item2, null)); + return result; + } + } +} diff --git a/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByPropertyOrder.cs b/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByPropertyOrder.cs new file mode 100644 index 000000000..313c47e51 --- /dev/null +++ b/Snowflake.Data/Core/Converter/Builder/ObjectBuilderByPropertyOrder.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Snowflake.Data.Client; + +namespace Snowflake.Data.Core.Converter.Builder +{ + internal class ObjectBuilderByPropertyOrder: IObjectBuilder + { + private readonly Type _type; + private readonly PropertyInfo[] _properties; + private int _index; + private readonly List> _result; + private PropertyInfo _currentProperty; + + public ObjectBuilderByPropertyOrder(Type type) + { + _type = type; + _properties = type.GetProperties().Where(property => !IsIgnoredForPropertiesOrder(property)).ToArray(); + _index = -1; + _result = new List>(); + } + + private bool IsIgnoredForPropertiesOrder(PropertyInfo property) + { + var sfAnnotation = property.GetCustomAttributes().FirstOrDefault(); + return sfAnnotation != null && sfAnnotation.IgnoreForPropertyOrder; + } + + public void BuildPart(object value) + { + _result.Add(Tuple.Create(_currentProperty, value)); + } + + public Type MoveNext(string sfPropertyName) + { + _index++; + _currentProperty = _properties[_index]; + return _currentProperty.PropertyType; + } + + public object Build() + { + var result = Activator.CreateInstance(_type); + _result.ForEach(p => p.Item1.SetValue(result, p.Item2, null)); + return result; + } + } +} diff --git a/Snowflake.Data/Core/Converter/Builder/ObjectBuilderFactory.cs b/Snowflake.Data/Core/Converter/Builder/ObjectBuilderFactory.cs new file mode 100644 index 000000000..24ee23478 --- /dev/null +++ b/Snowflake.Data/Core/Converter/Builder/ObjectBuilderFactory.cs @@ -0,0 +1,25 @@ +using System; +using Snowflake.Data.Client; + +namespace Snowflake.Data.Core.Converter.Builder +{ + internal static class ObjectBuilderFactory + { + public static IObjectBuilder Create(Type type, int fieldsCount, SnowflakeObjectConstructionMethod constructionMethod) + { + if (constructionMethod == SnowflakeObjectConstructionMethod.PROPERTIES_NAMES) + { + return new ObjectBuilderByPropertyNames(type); + } + if (constructionMethod == SnowflakeObjectConstructionMethod.PROPERTIES_ORDER) + { + return new ObjectBuilderByPropertyOrder(type); + } + if (constructionMethod == SnowflakeObjectConstructionMethod.CONSTRUCTOR) + { + return new ObjectBuilderByConstructor(type, fieldsCount); + } + throw new StructuredTypesReadingException("Unknown construction method"); + } + } +} diff --git a/Snowflake.Data/Core/Converter/JsonToStructuredTypeConverter.cs b/Snowflake.Data/Core/Converter/JsonToStructuredTypeConverter.cs new file mode 100644 index 000000000..5d9a51a4b --- /dev/null +++ b/Snowflake.Data/Core/Converter/JsonToStructuredTypeConverter.cs @@ -0,0 +1,425 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json.Linq; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Converter.Builder; + +namespace Snowflake.Data.Core.Converter +{ + internal static class JsonToStructuredTypeConverter + { + private static readonly TimeConverter s_timeConverter = new TimeConverter(); + + public static T ConvertObject(List fields, JObject value) + { + var type = typeof(T); + return (T) ConvertToObject(type, fields, new StructurePath(), value); + } + + public static T[] ConvertArray(List fields, JArray value) + { + var type = typeof(T[]); + var elementType = typeof(T); + return (T[]) ConvertToArray(type, elementType, fields, new StructurePath(), value); + } + + public static Dictionary ConvertMap(List fields, JObject value) + { + var type = typeof(Dictionary); + var keyType = typeof(TKey); + var valueType = typeof(TValue); + return (Dictionary) ConvertToMap(type, keyType, valueType, fields, new StructurePath(), value); + } + + + private static object ConvertToObject(Type type, List fields, StructurePath structurePath, JToken json) + { + if (json == null || json.Type == JTokenType.Null || json.Type == JTokenType.Undefined) + { + return null; + } + + if (json.Type != JTokenType.Object) + { + throw new StructuredTypesReadingException($"JSON value is not a JSON object. Occured for path {structurePath}"); + } + + var jsonObject = (JObject)json; + var constructionMethod = GetConstructionMethod(type); + var objectBuilder = ObjectBuilderFactory.Create(type, fields?.Count ?? 0, constructionMethod); + using (var metadataIterator = fields.GetEnumerator()) + { + using (var jsonEnumerator = jsonObject.GetEnumerator()) + { + var propertyIndex = -1; + do + { + var nextMetadataAvailable = metadataIterator.MoveNext(); + var nextJsonAvailable = jsonEnumerator.MoveNext(); + if (nextMetadataAvailable ^ nextJsonAvailable) // exclusive or + { + throw new StructuredTypesReadingException($"Internal error: object fields count not matching metadata fields count. Occured for path {structurePath}"); + } + + if (!nextMetadataAvailable) + break; + propertyIndex++; + var propertyPath = structurePath.WithPropertyIndex(propertyIndex); + var jsonPropertyWithValue = jsonEnumerator.Current; + var fieldMetadata = metadataIterator.Current; + var key = jsonPropertyWithValue.Key; + var fieldValue = jsonPropertyWithValue.Value; + try + { + var fieldType = objectBuilder.MoveNext(key); + var value = ConvertToStructuredOrUnstructuredValue(fieldType, fieldMetadata, propertyPath, fieldValue); + objectBuilder.BuildPart(value); + } + catch (Exception e) + { + if (e is SnowflakeDbException) + throw; + throw StructuredTypesReadingHandler.ToSnowflakeDbException(e, $"when handling property with path {propertyPath}"); + } + } while (true); + } + } + return objectBuilder.Build(); + } + + private static SnowflakeObjectConstructionMethod GetConstructionMethod(Type type) + { + return type.GetCustomAttributes(false) + .Where(attribute => attribute.GetType() == typeof(SnowflakeObject)) + .Select(attribute => ((SnowflakeObject)attribute).ConstructionMethod) + .FirstOrDefault(); + } + + private static object ConvertToUnstructuredType(FieldMetadata fieldMetadata, Type fieldType, JToken json) + { + if (json == null || json.Type == JTokenType.Null || json.Type == JTokenType.Undefined) + { + return null; + } + if (IsTextMetadata(fieldMetadata)) + { + var value = json.Value(); + if (fieldType == typeof(char) || fieldType == typeof(char?)) + { + return char.Parse(value); + } + if (fieldType == typeof(Guid) || fieldType == typeof(Guid?)) + { + return Guid.Parse(value); + } + if (fieldType == typeof(string)) + { + return value; + } + throw new StructuredTypesReadingException($"Could not read {fieldMetadata.type} type into {fieldType}"); + } + if (IsFixedMetadata(fieldMetadata)) + { + var value = json.Value(); + if (fieldType == typeof(byte) || fieldType == typeof(byte?)) + { + return byte.Parse(value); + } + if (fieldType == typeof(sbyte) || fieldType == typeof(sbyte?)) + { + return sbyte.Parse(value); + } + if (fieldType == typeof(short) || fieldType == typeof(short?)) + { + return short.Parse(value); + } + if (fieldType == typeof(ushort) || fieldType == typeof(ushort?)) + { + return ushort.Parse(value); + } + if (fieldType == typeof(Int32) || fieldType == typeof(Int32?)) + { + var bytes = Encoding.UTF8.GetBytes(value); + return FastParser.FastParseInt32(bytes, 0, bytes.Length); + } + if (fieldType == typeof(Int64) || fieldType == typeof(Int64?)) + { + var bytes = Encoding.UTF8.GetBytes(value); + return FastParser.FastParseInt64(bytes, 0, bytes.Length); + } + throw new StructuredTypesReadingException($"Could not read {fieldMetadata.type} type into {fieldType}"); + } + if (IsRealMetadata(fieldMetadata)) + { + var value = json.Value(); + var bytes = Encoding.UTF8.GetBytes(value); + var decimalValue = FastParser.FastParseDecimal(bytes, 0, bytes.Length); + if (fieldType == typeof(float) || fieldType == typeof(float?)) + { + return (float) decimalValue; + } + if (fieldType == typeof(double) || fieldType == typeof(double?)) + { + return (double) decimalValue; + } + return decimalValue; + } + if (IsBooleanMetadata(fieldMetadata)) + { + return json.Value(); + } + if (IsTimestampNtzMetadata(fieldMetadata)) + { + var value = json.Value(); + return s_timeConverter.Convert(value, SFDataType.TIMESTAMP_NTZ, fieldType); + } + if (IsTimestampLtzMetadata(fieldMetadata)) + { + var value = json.Value(); + return s_timeConverter.Convert(value, SFDataType.TIMESTAMP_LTZ, fieldType); + } + if (IsTimestampTzMetadata(fieldMetadata)) + { + var value = json.Value(); + return s_timeConverter.Convert(value, SFDataType.TIMESTAMP_TZ, fieldType); + } + if (IsTimeMetadata(fieldMetadata)) + { + var value = json.Value(); + return s_timeConverter.Convert(value, SFDataType.TIME, fieldType); + } + if (IsDateMetadata(fieldMetadata)) + { + var value = json.Value(); + return s_timeConverter.Convert(value, SFDataType.DATE, fieldType); + } + if (IsBinaryMetadata(fieldMetadata)) + { + var value = json.Value(); + if (fieldType == typeof(byte[])) + { + return SFDataConverter.HexToBytes(value); + } + throw new StructuredTypesReadingException($"Could not read BINARY type into {fieldType}"); + } + if (IsObjectMetadata(fieldMetadata)) // semi structured object + { + return json.ToString(); + } + if (IsArrayMetadata(fieldMetadata)) // semi structured array + { + return json.ToString(); + } + if (IsVariantMetadata(fieldMetadata)) + { + return json.ToString(); + } + throw new StructuredTypesReadingException($"Could not read {fieldMetadata.type} type into {fieldType}"); + } + + private static object ConvertToArray(Type type, Type elementType, List fields, StructurePath structurePath, JToken json) + { + if (json == null || json.Type == JTokenType.Null || json.Type == JTokenType.Undefined) + { + return null; + } + if (json.Type != JTokenType.Array) + { + throw new StructuredTypesReadingException($"JSON value is not a JSON array. Occured for path {structurePath}"); + } + var jsonArray = (JArray)json; + var arrayType = GetArrayType(type, elementType); + var result = (IList) Activator.CreateInstance(arrayType, jsonArray.Count); + var elementMetadata = fields[0]; + for (var i = 0; i < jsonArray.Count; i++) + { + var arrayElementPath = structurePath.WithArrayIndex(i); + result[i] = ConvertToStructuredOrUnstructuredValue(elementType, elementMetadata, arrayElementPath, jsonArray[i]); + } + if (type != arrayType) + { + var listType = type.IsAbstract ? typeof(List<>).MakeGenericType(elementType) : type; + var list = (IList) Activator.CreateInstance(listType); + for (int i = 0; i < result.Count; i++) + { + list.Add(result[i]); + } + return list; + } + + return result; + } + + private static object ConvertToMap(Type type, Type keyType, Type valueType, List fields, StructurePath structurePath, JToken json) + { + if (keyType != typeof(string) + && keyType != typeof(int) && keyType != typeof(int?) + && keyType != typeof(long) && keyType != typeof(long?)) + { + throw new StructuredTypesReadingException($"Unsupported key type of dictionary {keyType} for extracting a map. Occured for path {structurePath}"); + } + if (json == null || json.Type == JTokenType.Null || json.Type == JTokenType.Undefined) + { + return null; + } + if (json.Type != JTokenType.Object) + { + throw new StructuredTypesReadingException($"Extracting a map failed. JSON value is not a JSON object. Occured for path {structurePath}"); + } + if (fields == null || fields.Count != 2) + { + throw new StructuredTypesReadingException($"Extracting a map failed. Map metadata should have 2 metadata fields. Occured for path {structurePath}"); + } + var keyMetadata = fields[0]; + var fieldMetadata = fields[1]; + var jsonObject = (JObject)json; + var dictionaryType = type.IsAbstract ? typeof(Dictionary<,>).MakeGenericType(keyType, valueType) : type; + var result = (IDictionary) Activator.CreateInstance(dictionaryType); + using (var jsonEnumerator = jsonObject.GetEnumerator()) + { + var elementIndex = -1; + while (jsonEnumerator.MoveNext()) + { + elementIndex++; + var mapElementPath = structurePath.WithMapIndex(elementIndex); + var jsonPropertyWithValue = jsonEnumerator.Current; + var fieldValue = jsonPropertyWithValue.Value; + var key = IsTextMetadata(keyMetadata) || IsFixedMetadata(keyMetadata) + ? ConvertToUnstructuredType(keyMetadata, keyType, jsonPropertyWithValue.Key) + : throw new StructuredTypesReadingException($"Unsupported key type for map {keyMetadata.type}. Occured for path {mapElementPath}"); + var value = ConvertToStructuredOrUnstructuredValue(valueType, fieldMetadata, mapElementPath, fieldValue); + result.Add(key, value); + } + } + return result; + } + + private static object ConvertToStructuredOrUnstructuredValue(Type valueType, FieldMetadata fieldMetadata, StructurePath structurePath, JToken fieldValue) + { + try + { + if (IsObjectMetadata(fieldMetadata) && IsStructuredMetadata(fieldMetadata)) + { + return ConvertToObject(valueType, fieldMetadata.fields, structurePath, fieldValue); + } + + if (IsArrayMetadata(fieldMetadata) && IsStructuredMetadata(fieldMetadata)) + { + var nestedType = GetNestedType(valueType); + return ConvertToArray(valueType, nestedType, fieldMetadata.fields, structurePath, fieldValue); + } + + if (IsMapMetadata(fieldMetadata) && IsStructuredMetadata(fieldMetadata)) + { + var keyValueTypes = GetMapKeyValueTypes(valueType); + return ConvertToMap(valueType, keyValueTypes[0], keyValueTypes[1], fieldMetadata.fields, structurePath, fieldValue); + } + + return ConvertToUnstructuredType(fieldMetadata, valueType, fieldValue); + } + catch (Exception e) + { + if (e is SnowflakeDbException) + throw; + throw StructuredTypesReadingHandler.ToSnowflakeDbException(e, $"when reading path {structurePath}"); + } + } + + private static Type GetNestedType(Type type) + { + if (type.IsArray) + { + return type.GetElementType(); + } + if (IsListType(type) || IsIListType(type)) + { + return type.GenericTypeArguments[0]; + } + throw new StructuredTypesReadingException("Only arrays and lists are supported when extracting a structured array"); + } + + private static Type[] GetMapKeyValueTypes(Type type) + { + var genericParamWithTwoArguments = type.IsGenericType && type.GenericTypeArguments.Length == 2; + if (!genericParamWithTwoArguments) + { + throw new StructuredTypesReadingException("Could not get key and value types"); + } + return type.GenericTypeArguments; + } + + private static Type GetArrayType(Type type, Type elementType) + { + if (type.IsArray) + { + return type; + } + if (IsListType(type) || IsIListType(type)) + { + return elementType.MakeArrayType(); + } + throw new StructuredTypesReadingException("Only arrays and lists are supported when extracting a structured array"); + } + + private static bool IsListType(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); + + private static bool IsIListType(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IList<>); + + private static bool IsStructuredMetadata(FieldMetadata fieldMetadata) => + fieldMetadata.fields != null && fieldMetadata.fields.Count > 0; + + private static bool IsObjectMetadata(FieldMetadata fieldMetadata) => + SFDataType.OBJECT.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + internal static bool IsObjectType(string type) => + SFDataType.OBJECT.ToString().Equals(type, StringComparison.OrdinalIgnoreCase); + + private static bool IsTextMetadata(FieldMetadata fieldMetadata) => + SFDataType.TEXT.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsFixedMetadata(FieldMetadata fieldMetadata) => + SFDataType.FIXED.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsTimestampNtzMetadata(FieldMetadata fieldMetadata) => + SFDataType.TIMESTAMP_NTZ.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsTimestampLtzMetadata(FieldMetadata fieldMetadata) => + SFDataType.TIMESTAMP_LTZ.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsTimestampTzMetadata(FieldMetadata fieldMetadata) => + SFDataType.TIMESTAMP_TZ.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsTimeMetadata(FieldMetadata fieldMetadata) => + SFDataType.TIME.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsDateMetadata(FieldMetadata fieldMetadata) => + SFDataType.DATE.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsRealMetadata(FieldMetadata fieldMetadata) => + SFDataType.REAL.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsBooleanMetadata(FieldMetadata fieldMetadata) => + SFDataType.BOOLEAN.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsBinaryMetadata(FieldMetadata fieldMetadata) => + SFDataType.BINARY.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsArrayMetadata(FieldMetadata fieldMetadata) => + SFDataType.ARRAY.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + internal static bool IsArrayType(string type) => + SFDataType.ARRAY.ToString().Equals(type, StringComparison.OrdinalIgnoreCase); + + private static bool IsVariantMetadata(FieldMetadata fieldMetadata) => + SFDataType.VARIANT.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + private static bool IsMapMetadata(FieldMetadata fieldMetadata) => + SFDataType.MAP.ToString().Equals(fieldMetadata.type, StringComparison.OrdinalIgnoreCase); + + internal static bool IsMapType(string type) => + SFDataType.MAP.ToString().Equals(type, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/Snowflake.Data/Core/Converter/StructurePath.cs b/Snowflake.Data/Core/Converter/StructurePath.cs new file mode 100644 index 000000000..9b51d4573 --- /dev/null +++ b/Snowflake.Data/Core/Converter/StructurePath.cs @@ -0,0 +1,36 @@ +namespace Snowflake.Data.Core.Converter +{ + internal class StructurePath + { + private readonly string _path; + + public StructurePath() : this("$") + { + } + + private StructurePath(string path) + { + _path = path; + } + + public StructurePath WithPropertyIndex(int propertyIndex) + { + return new StructurePath($"{_path}[{propertyIndex}]"); + } + + public StructurePath WithArrayIndex(int arrayIndex) + { + return new StructurePath($"{_path}[{arrayIndex}]"); + } + + public StructurePath WithMapIndex(int mapIndex) + { + return new StructurePath($"{_path}[{mapIndex}]"); + } + + public override string ToString() + { + return _path; + } + } +} diff --git a/Snowflake.Data/Core/Converter/StructuredTypesReadingException.cs b/Snowflake.Data/Core/Converter/StructuredTypesReadingException.cs new file mode 100644 index 000000000..fa1e3754e --- /dev/null +++ b/Snowflake.Data/Core/Converter/StructuredTypesReadingException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Snowflake.Data.Core.Converter +{ + internal class StructuredTypesReadingException : Exception + { + public StructuredTypesReadingException(string message) : base(message) + { + } + } +} diff --git a/Snowflake.Data/Core/Converter/StructuredTypesReadingHandler.cs b/Snowflake.Data/Core/Converter/StructuredTypesReadingHandler.cs new file mode 100644 index 000000000..ce46332c9 --- /dev/null +++ b/Snowflake.Data/Core/Converter/StructuredTypesReadingHandler.cs @@ -0,0 +1,22 @@ +using System; +using Snowflake.Data.Client; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Core.Converter +{ + public class StructuredTypesReadingHandler + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + public static SnowflakeDbException ToSnowflakeDbException(Exception exception, string context) + { + if (exception is StructuredTypesReadingException) + { + s_logger.Debug("Exception caught when reading structured types", exception); + return new SnowflakeDbException(SFError.STRUCTURED_TYPE_READ_DETAILED_ERROR, context, exception.Message); + } + s_logger.Debug("Exception caught when reading structured types"); + return new SnowflakeDbException(SFError.STRUCTURED_TYPE_READ_ERROR, context); + } + } +} diff --git a/Snowflake.Data/Core/Converter/TimeConverter.cs b/Snowflake.Data/Core/Converter/TimeConverter.cs new file mode 100644 index 000000000..3f1252762 --- /dev/null +++ b/Snowflake.Data/Core/Converter/TimeConverter.cs @@ -0,0 +1,79 @@ +using System; + +namespace Snowflake.Data.Core.Converter +{ + internal class TimeConverter + { + public object Convert(string value, SFDataType timestampType, Type fieldType) + { + if (fieldType == typeof(string)) + { + return value; + } + if (timestampType == SFDataType.TIMESTAMP_NTZ) + { + var dateTimeUtc = DateTime.Parse(value).ToUniversalTime(); + if (fieldType == typeof(DateTime) || fieldType == typeof(DateTime?)) + { + return dateTimeUtc; + } + + if (fieldType == typeof(DateTimeOffset) || fieldType == typeof(DateTimeOffset?)) + { + return (DateTimeOffset) dateTimeUtc; + } + + throw new StructuredTypesReadingException($"Cannot read TIMESTAMP_NTZ into {fieldType} type"); + } + + if (timestampType == SFDataType.TIMESTAMP_TZ) + { + var dateTimeOffset = DateTimeOffset.Parse(value); + if (fieldType == typeof(DateTimeOffset) || fieldType == typeof(DateTimeOffset?)) + { + return dateTimeOffset; + } + if (fieldType == typeof(DateTime) || fieldType == typeof(DateTime?)) + { + return dateTimeOffset.ToUniversalTime().DateTime.ToUniversalTime(); + } + + throw new StructuredTypesReadingException($"Cannot read TIMESTAMP_TZ into {fieldType} type"); + } + if (timestampType == SFDataType.TIMESTAMP_LTZ) + { + var dateTimeOffset = DateTimeOffset.Parse(value); + if (fieldType == typeof(DateTimeOffset) || fieldType == typeof(DateTimeOffset?)) + { + return dateTimeOffset; + } + if (fieldType == typeof(DateTime) || fieldType == typeof(DateTime?)) + { + return dateTimeOffset.UtcDateTime; + } + throw new StructuredTypesReadingException($"Cannot read TIMESTAMP_LTZ into {fieldType} type"); + } + if (timestampType == SFDataType.TIME) + { + if (fieldType == typeof(TimeSpan) || fieldType == typeof(TimeSpan?)) + { + return TimeSpan.Parse(value); + } + throw new StructuredTypesReadingException($"Cannot read TIME into {fieldType} type"); + } + if (timestampType == SFDataType.DATE) + { + if (fieldType == typeof(DateTimeOffset) || fieldType == typeof(DateTimeOffset?)) + { + return DateTimeOffset.Parse(value).ToUniversalTime(); + } + if (fieldType == typeof(DateTime) || fieldType == typeof(DateTime?)) + { + return DateTime.Parse(value).ToUniversalTime(); + } + throw new StructuredTypesReadingException($"Cannot not read DATE into {fieldType} type"); + } + throw new StructuredTypesReadingException($"Unsupported conversion from {timestampType.ToString()} to {fieldType} type"); + } + } +} diff --git a/Snowflake.Data/Core/ErrorMessages.resx b/Snowflake.Data/Core/ErrorMessages.resx index c8e65e465..3532f3394 100755 --- a/Snowflake.Data/Core/ErrorMessages.resx +++ b/Snowflake.Data/Core/ErrorMessages.resx @@ -192,4 +192,10 @@ Executing command on a non-opened connection. + + Failed to read structured type {0}. + + + Failed to read structured type {0}. \n : Error : {1} + diff --git a/Snowflake.Data/Core/RestResponse.cs b/Snowflake.Data/Core/RestResponse.cs index 75f1698ea..64275fa42 100755 --- a/Snowflake.Data/Core/RestResponse.cs +++ b/Snowflake.Data/Core/RestResponse.cs @@ -15,10 +15,10 @@ abstract class BaseRestResponse { [JsonProperty(PropertyName = "message")] internal String message { get; set; } - + [JsonProperty(PropertyName = "code", NullValueHandling = NullValueHandling.Ignore)] internal int code { get; set; } - + [JsonProperty(PropertyName = "success")] internal bool success { get; set; } @@ -222,7 +222,7 @@ internal class QueryExecResponseData : IQueryExecResponseData // multiple statements response data [JsonProperty(PropertyName = "resultIds", NullValueHandling = NullValueHandling.Ignore)] internal string resultIds { get; set; } - + [JsonProperty(PropertyName = "queryResultFormat", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(StringEnumConverter))] internal ResultFormat queryResultFormat { get; set; } @@ -295,8 +295,38 @@ internal class ExecResponseRowType [JsonProperty(PropertyName = "nullable")] internal bool nullable { get; set; } + + [JsonProperty(PropertyName = "fields")] + internal List fields { get; set; } + } + + internal class FieldMetadata + { + [JsonProperty(PropertyName = "name")] + internal string name { get; set; } + + [JsonProperty(PropertyName = "byteLength", NullValueHandling = NullValueHandling.Ignore)] + private Int64 byteLength { get; set; } + + [JsonProperty(PropertyName = "typeName")] + internal string typeName { get; set; } + + [JsonProperty(PropertyName = "type")] + internal string type { get; set; } + + [JsonProperty(PropertyName = "scale", NullValueHandling = NullValueHandling.Ignore)] + internal Int64 scale { get; set; } + + [JsonProperty(PropertyName = "precision", NullValueHandling = NullValueHandling.Ignore)] + internal Int64 precision { get; set; } + + [JsonProperty(PropertyName = "nullable")] + internal bool nullable { get; set; } + + [JsonProperty(PropertyName = "fields")] + internal List fields { get; set; } } - + internal class ExecResponseChunk { [JsonProperty(PropertyName = "url")] @@ -483,4 +513,4 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s throw new NotImplementedException(); } } -} +} diff --git a/Snowflake.Data/Core/SFDataConverter.cs b/Snowflake.Data/Core/SFDataConverter.cs index 6822f03f4..2300a26bb 100755 --- a/Snowflake.Data/Core/SFDataConverter.cs +++ b/Snowflake.Data/Core/SFDataConverter.cs @@ -14,7 +14,7 @@ namespace Snowflake.Data.Core public enum SFDataType { None, FIXED, REAL, TEXT, DATE, VARIANT, TIMESTAMP_LTZ, TIMESTAMP_NTZ, - TIMESTAMP_TZ, OBJECT, BINARY, TIME, BOOLEAN, ARRAY + TIMESTAMP_TZ, OBJECT, BINARY, TIME, BOOLEAN, ARRAY, MAP } static class SFDataConverter @@ -53,7 +53,7 @@ internal static object ConvertToCSharpVal(UTF8Buffer srcVal, SFDataType srcType, try { // The most common conversions are checked first for maximum performance - if (destType == typeof(Int64)) + if (destType == typeof(Int64)) { return FastParser.FastParseInt64(srcVal.Buffer, srcVal.offset, srcVal.length); } @@ -117,7 +117,7 @@ internal static object ConvertToCSharpVal(UTF8Buffer srcVal, SFDataType srcType, } else if (destType == typeof(char[])) { - byte[] data = srcType == SFDataType.BINARY ? + byte[] data = srcType == SFDataType.BINARY ? HexToBytes(srcVal.ToString()) : srcVal.GetBytes(); return Encoding.UTF8.GetString(data).ToCharArray(); } @@ -138,7 +138,7 @@ private static object ConvertToTimeSpan(UTF8Buffer srcVal, SFDataType srcType) { case SFDataType.TIME: // Convert fractional seconds since midnight to TimeSpan - // A single tick represents one hundred nanoseconds or one ten-millionth of a second. + // A single tick represents one hundred nanoseconds or one ten-millionth of a second. // There are 10,000 ticks in a millisecond return TimeSpan.FromTicks(GetTicksFromSecondAndNanosecond(srcVal)); default: @@ -183,32 +183,32 @@ private static DateTimeOffset ConvertToDateTimeOffset(UTF8Buffer srcVal, SFDataT TimeSpan offSetTimespan = new TimeSpan((offset - 1440) / 60, 0, 0); return new DateTimeOffset(UnixEpoch.Ticks + GetTicksFromSecondAndNanosecond(timeVal), TimeSpan.Zero).ToOffset(offSetTimespan); } - case SFDataType.TIMESTAMP_LTZ: + case SFDataType.TIMESTAMP_LTZ: return new DateTimeOffset(UnixEpoch.Ticks + - GetTicksFromSecondAndNanosecond(srcVal), TimeSpan.Zero).ToLocalTime(); + GetTicksFromSecondAndNanosecond(srcVal), TimeSpan.Zero).ToLocalTime(); default: - throw new SnowflakeDbException(SFError.INVALID_DATA_CONVERSION, srcVal, + throw new SnowflakeDbException(SFError.INVALID_DATA_CONVERSION, srcVal, srcType, typeof(DateTimeOffset).ToString()); } } - static int[] powersOf10 = { - 1, - 10, - 100, - 1000, - 10000, - 100000, - 1000000, - 10000000, - 100000000 + static int[] powersOf10 = { + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000 }; /// /// Convert the time value with the format seconds.nanoseconds to a number of /// Ticks. A single tick represents one hundred nanoseconds. - /// For example, "23:59:59.123456789" is represented by 86399.123456789 and is + /// For example, "23:59:59.123456789" is represented by 86399.123456789 and is /// 863991234567 ticks (precision is maximum 7). /// /// The source data returned by the server. For example '86399.123456789' @@ -314,7 +314,7 @@ private static string BytesToHex(byte[] bytes) return hexBuilder.ToString(); } - private static byte[] HexToBytes(string hex) + internal static byte[] HexToBytes(string hex) { int NumberChars = hex.Length; byte[] bytes = new byte[NumberChars / 2]; diff --git a/Snowflake.Data/Core/SFError.cs b/Snowflake.Data/Core/SFError.cs index e4e03618f..44de969a1 100755 --- a/Snowflake.Data/Core/SFError.cs +++ b/Snowflake.Data/Core/SFError.cs @@ -86,7 +86,13 @@ public enum SFError EXECUTE_COMMAND_ON_CLOSED_CONNECTION, [SFErrorAttr(errorCode = 270060)] - INCONSISTENT_RESULT_ERROR + INCONSISTENT_RESULT_ERROR, + + [SFErrorAttr(errorCode = 270061)] + STRUCTURED_TYPE_READ_ERROR, + + [SFErrorAttr(errorCode = 270062)] + STRUCTURED_TYPE_READ_DETAILED_ERROR } class SFErrorAttr : Attribute diff --git a/Snowflake.Data/Core/SFResultSetMetaData.cs b/Snowflake.Data/Core/SFResultSetMetaData.cs index 4a8c5651c..bae97d221 100755 --- a/Snowflake.Data/Core/SFResultSetMetaData.cs +++ b/Snowflake.Data/Core/SFResultSetMetaData.cs @@ -31,7 +31,7 @@ class SFResultSetMetaData internal readonly SFStatementType statementType; internal readonly List> columnTypes; - + /// /// This map is used to cache column name to column index. Index is 0-based. /// @@ -96,12 +96,12 @@ internal int GetColumnIndexByName(string targetColumnName) columnNameToIndexCache[targetColumnName] = indexCounter; return indexCounter; } - indexCounter++; + indexCounter++; } } return -1; } - + internal SFDataType GetColumnTypeByIndex(int targetIndex) { return columnTypes[targetIndex].Item1; @@ -117,6 +117,12 @@ internal long GetScaleByIndex(int targetIndex) return rowTypes[targetIndex].scale; } + internal bool IsStructuredType(int targetIndex) + { + var fields = rowTypes[targetIndex].fields; + return fields != null && fields.Count > 0; + } + private SFDataType GetSFDataType(string type) { SFDataType rslt; @@ -124,7 +130,7 @@ private SFDataType GetSFDataType(string type) return rslt; throw new SnowflakeDbException(SFError.INTERNAL_ERROR, - $"Unknow column type: {type}"); + $"Unknow column type: {type}"); } private Type GetNativeTypeForColumn(SFDataType sfType, ExecResponseRowType col) @@ -138,7 +144,8 @@ private Type GetNativeTypeForColumn(SFDataType sfType, ExecResponseRowType col) case SFDataType.TEXT: case SFDataType.VARIANT: case SFDataType.OBJECT: - case SFDataType.ARRAY: + case SFDataType.ARRAY: + case SFDataType.MAP: return typeof(string); case SFDataType.DATE: case SFDataType.TIME: @@ -156,10 +163,10 @@ private Type GetNativeTypeForColumn(SFDataType sfType, ExecResponseRowType col) $"Unknow column type: {sfType}"); } } - + internal Type GetCSharpTypeByIndex(int targetIndex) { - return columnTypes[targetIndex].Item2; + return columnTypes[targetIndex].Item2; } internal string GetColumnNameByIndex(int targetIndex) @@ -203,7 +210,7 @@ private SFStatementType FindStatementTypeById(long id) internal enum SFStatementType { [SFStatementTypeAttr(typeId = 0x0000)] - UNKNOWN, + UNKNOWN, [SFStatementTypeAttr(typeId = 0x1000)] SELECT, @@ -212,7 +219,7 @@ internal enum SFStatementType EXPLAIN, /// - /// Data Manipulation Language + /// Data Manipulation Language /// [SFStatementTypeAttr(typeId = 0x3000)] DML, @@ -257,7 +264,7 @@ internal enum SFStatementType /// Transaction Command Language /// [SFStatementTypeAttr(typeId = 0x5000)] - TCL, + TCL, /// /// Data Definition Language