Skip to content

Commit dfba44e

Browse files
SNOW-1271212 Fixed values uploaded to stage for bindings exceeding CLIENT_STAGE_ARRAY_BINDING_THRESHOLD (#897)
### Description When number of binded values during query execution exceeds the threshold of a session parameter CLIENT_STAGE_ARRAY_BINDING_THRESHOLD then values are written as a CSV file to a stage and it get's picked during query execution. Improper values (or values truncating nanos) has been uploaded prior to this fix for date and time related columns of type: DATE, TIME, TIMESTAMP_LTZ, TIMESTAMP_NTZ, TIMESTAMP_TZ. ### Checklist - [x] Code compiles correctly - [x] Code is formatted according to [Coding Conventions](../CodingConventions.md) - [x] Created tests which fail without the change (if possible) - [x] All tests passing (`dotnet test`) - [x] Extended the README / documentation, if necessary - [x] Provide JIRA issue id (if possible) or GitHub issue id in PR name
1 parent a302e0f commit dfba44e

File tree

9 files changed

+454
-47
lines changed

9 files changed

+454
-47
lines changed

Snowflake.Data.Tests/IntegrationTests/SFBindTestIT.cs

Lines changed: 243 additions & 23 deletions
Large diffs are not rendered by default.

Snowflake.Data.Tests/SFBaseTest.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Runtime.InteropServices;
1313
using NUnit.Framework;
1414
using Snowflake.Data.Client;
15+
using Snowflake.Data.Log;
1516
using Snowflake.Data.Tests.Util;
1617

1718
[assembly:LevelOfParallelism(10)]
@@ -56,6 +57,8 @@ public static void TearDownContext()
5657
#endif
5758
public class SFBaseTestAsync
5859
{
60+
private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger<SFBaseTestAsync>();
61+
5962
private const string ConnectionStringWithoutAuthFmt = "scheme={0};host={1};port={2};" +
6063
"account={3};role={4};db={5};schema={6};warehouse={7}";
6164
private const string ConnectionStringSnowflakeAuthFmt = ";user={0};password={1};";
@@ -106,10 +109,16 @@ private void RemoveTables()
106109
}
107110

108111
protected void CreateOrReplaceTable(IDbConnection conn, string tableName, IEnumerable<string> columns, string additionalQueryStr = null)
112+
{
113+
CreateOrReplaceTable(conn, tableName, "", columns, additionalQueryStr);
114+
}
115+
116+
protected void CreateOrReplaceTable(IDbConnection conn, string tableName, string tableType, IEnumerable<string> columns, string additionalQueryStr = null)
109117
{
110118
var columnsStr = string.Join(", ", columns);
111119
var cmd = conn.CreateCommand();
112-
cmd.CommandText = $"CREATE OR REPLACE TABLE {tableName}({columnsStr}) {additionalQueryStr}";
120+
cmd.CommandText = $"CREATE OR REPLACE {tableType} TABLE {tableName}({columnsStr}) {additionalQueryStr}";
121+
s_logger.Debug(cmd.CommandText);
113122
cmd.ExecuteNonQuery();
114123

115124
_tablesToRemove.Add(tableName);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved.
3+
*/
4+
5+
using System;
6+
using NUnit.Framework;
7+
using Snowflake.Data.Core;
8+
9+
namespace Snowflake.Data.Tests.UnitTests
10+
{
11+
[TestFixture]
12+
class SFBindUploaderTest
13+
{
14+
private readonly SFBindUploader _bindUploader = new SFBindUploader(null, "test");
15+
16+
[TestCase(SFDataType.DATE, "0", "1/1/1970")]
17+
[TestCase(SFDataType.DATE, "73785600000", "5/4/1972")]
18+
[TestCase(SFDataType.DATE, "1709164800000", "2/29/2024")]
19+
public void TestCsvDataConversionForDate(SFDataType dbType, string input, string expected)
20+
{
21+
// Arrange
22+
var dateExpected = DateTime.Parse(expected);
23+
var check = SFDataConverter.csharpValToSfVal(SFDataType.DATE, dateExpected);
24+
Assert.AreEqual(check, input);
25+
// Act
26+
DateTime dateActual = DateTime.Parse(_bindUploader.GetCSVData(dbType.ToString(), input));
27+
// Assert
28+
Assert.AreEqual(dateExpected, dateActual);
29+
}
30+
31+
[TestCase(SFDataType.TIME, "0", "00:00:00.000000")]
32+
[TestCase(SFDataType.TIME, "100000000", "00:00:00.100000")]
33+
[TestCase(SFDataType.TIME, "1000000000", "00:00:01.000000")]
34+
[TestCase(SFDataType.TIME, "60123456000", "00:01:00.123456")]
35+
[TestCase(SFDataType.TIME, "46801000000000", "13:00:01.000000")]
36+
public void TestCsvDataConversionForTime(SFDataType dbType, string input, string expected)
37+
{
38+
// Arrange
39+
DateTime timeExpected = DateTime.Parse(expected);
40+
var check = SFDataConverter.csharpValToSfVal(SFDataType.TIME, timeExpected);
41+
Assert.AreEqual(check, input);
42+
// Act
43+
DateTime timeActual = DateTime.Parse(_bindUploader.GetCSVData(dbType.ToString(), input));
44+
// Assert
45+
Assert.AreEqual(timeExpected, timeActual);
46+
}
47+
48+
[TestCase(SFDataType.TIMESTAMP_LTZ, "39600000000000", "1970-01-01T12:00:00.0000000+01:00")]
49+
[TestCase(SFDataType.TIMESTAMP_LTZ, "1341136800000000000", "2012-07-01T12:00:00.0000000+02:00")]
50+
[TestCase(SFDataType.TIMESTAMP_LTZ, "352245599987654000", "1981-02-28T23:59:59.9876540+02:00")]
51+
[TestCase(SFDataType.TIMESTAMP_LTZ, "1678868249207000000", "2023/03/15T13:17:29.207+05:00")]
52+
public void TestCsvDataConversionForTimestampLtz(SFDataType dbType, string input, string expected)
53+
{
54+
// Arrange
55+
var timestampExpected = DateTimeOffset.Parse(expected);
56+
var check = SFDataConverter.csharpValToSfVal(SFDataType.TIMESTAMP_LTZ, timestampExpected);
57+
Assert.AreEqual(check, input);
58+
// Act
59+
var timestampActual = DateTimeOffset.Parse(_bindUploader.GetCSVData(dbType.ToString(), input));
60+
// Assert
61+
Assert.AreEqual(timestampExpected.ToLocalTime(), timestampActual);
62+
}
63+
64+
[TestCase(SFDataType.TIMESTAMP_TZ, "1341136800000000000 1560", "2012-07-01 12:00:00.000000 +02:00")]
65+
[TestCase(SFDataType.TIMESTAMP_TZ, "352245599987654000 1560", "1981-02-28 23:59:59.987654 +02:00")]
66+
public void TestCsvDataConversionForTimestampTz(SFDataType dbType, string input, string expected)
67+
{
68+
// Arrange
69+
DateTimeOffset timestampExpected = DateTimeOffset.Parse(expected);
70+
var check = SFDataConverter.csharpValToSfVal(SFDataType.TIMESTAMP_TZ, timestampExpected);
71+
Assert.AreEqual(check, input);
72+
// Act
73+
DateTimeOffset timestampActual = DateTimeOffset.Parse(_bindUploader.GetCSVData(dbType.ToString(), input));
74+
// Assert
75+
Assert.AreEqual(timestampExpected, timestampActual);
76+
}
77+
78+
[TestCase(SFDataType.TIMESTAMP_NTZ, "1341144000000000000", "2012-07-01 12:00:00.000000")]
79+
[TestCase(SFDataType.TIMESTAMP_NTZ, "352252799987654000", "1981-02-28 23:59:59.987654")]
80+
public void TestCsvDataConversionForTimestampNtz(SFDataType dbType, string input, string expected)
81+
{
82+
// Arrange
83+
DateTime timestampExpected = DateTime.Parse(expected);
84+
var check = SFDataConverter.csharpValToSfVal(SFDataType.TIMESTAMP_NTZ, timestampExpected);
85+
Assert.AreEqual(check, input);
86+
// Act
87+
DateTime timestampActual = DateTime.Parse(_bindUploader.GetCSVData(dbType.ToString(), input));
88+
// Assert
89+
Assert.AreEqual(timestampExpected, timestampActual);
90+
}
91+
92+
[TestCase(SFDataType.TEXT, "", "\"\"")]
93+
[TestCase(SFDataType.TEXT, "\"", "\"\"\"\"")]
94+
[TestCase(SFDataType.TEXT, "\n", "\"\n\"")]
95+
[TestCase(SFDataType.TEXT, "\t", "\"\t\"")]
96+
[TestCase(SFDataType.TEXT, ",", "\",\"")]
97+
[TestCase(SFDataType.TEXT, "Sample text", "Sample text")]
98+
public void TestCsvDataConversionForText(SFDataType dbType, string input, string expected)
99+
{
100+
// Act
101+
var actual = _bindUploader.GetCSVData(dbType.ToString(), input);
102+
// Assert
103+
Assert.AreEqual(expected, actual);
104+
}
105+
106+
}
107+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Data;
2+
3+
namespace Snowflake.Data.Tests.Util
4+
{
5+
public static class DbCommandExtensions
6+
{
7+
internal static IDbDataParameter Add(this IDbCommand command, string name, DbType dbType, object value)
8+
{
9+
var parameter = command.CreateParameter();
10+
parameter.ParameterName = name;
11+
parameter.DbType = dbType;
12+
parameter.Value = value;
13+
command.Parameters.Add(parameter);
14+
return parameter;
15+
}
16+
17+
}
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Data;
2+
3+
namespace Snowflake.Data.Tests.Util
4+
{
5+
public static class DbConnectionExtensions
6+
{
7+
internal static IDbCommand CreateCommand(this IDbConnection connection, string commandText)
8+
{
9+
var command = connection.CreateCommand();
10+
command.Connection = connection;
11+
command.CommandText = commandText;
12+
return command;
13+
}
14+
15+
internal static int ExecuteNonQuery(this IDbConnection connection, string commandText)
16+
{
17+
var command = connection.CreateCommand();
18+
command.Connection = connection;
19+
command.CommandText = commandText;
20+
return command.ExecuteNonQuery();
21+
}
22+
}
23+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using NUnit.Framework;
3+
4+
namespace Snowflake.Data.Tests.Util
5+
{
6+
public enum SFTableType
7+
{
8+
Standard,
9+
Hybrid,
10+
Iceberg
11+
}
12+
13+
static class TableTypeExtensions
14+
{
15+
internal static string TableDDLCreationPrefix(this SFTableType val) => val == SFTableType.Standard ? "" : val.ToString().ToUpper();
16+
17+
internal static string TableDDLCreationFlags(this SFTableType val)
18+
{
19+
if (val != SFTableType.Iceberg)
20+
return "";
21+
var externalVolume = Environment.GetEnvironmentVariable("ICEBERG_EXTERNAL_VOLUME");
22+
var catalog = Environment.GetEnvironmentVariable("ICEBERG_CATALOG");
23+
var baseLocation = Environment.GetEnvironmentVariable("ICEBERG_BASE_LOCATION");
24+
Assert.IsNotNull(externalVolume, "env ICEBERG_EXTERNAL_VOLUME not set!");
25+
Assert.IsNotNull(catalog, "env ICEBERG_CATALOG not set!");
26+
Assert.IsNotNull(baseLocation, "env ICEBERG_BASE_LOCATION not set!");
27+
return $"EXTERNAL_VOLUME = '{externalVolume}' CATALOG = '{catalog}' BASE_LOCATION = '{baseLocation}'";
28+
}
29+
}
30+
}

Snowflake.Data/Client/SnowflakeDbCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,5 +460,7 @@ private void CheckIfCommandTextIsSet()
460460
throw new Exception(errorMessage);
461461
}
462462
}
463+
464+
internal string GetBindStage() => sfStatement?.GetBindStage();
463465
}
464466
}

Snowflake.Data/Core/SFBindUploader.cs

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,13 @@ internal async Task UploadStreamAsync(MemoryStream stream, string destFileName,
224224
statement.SetUploadStream(stream, destFileName, stagePath);
225225
await statement.ExecuteTransferAsync(putStmt, cancellationToken).ConfigureAwait(false);
226226
}
227-
private string GetCSVData(string sType, string sValue)
227+
228+
internal string GetCSVData(string sType, string sValue)
228229
{
229230
if (sValue == null)
230231
return sValue;
231232

232-
DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
233-
DateTimeOffset dateTimeOffset = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
233+
DateTime epoch = SFDataConverter.UnixEpoch;
234234
switch (sType)
235235
{
236236
case "TEXT":
@@ -246,33 +246,29 @@ private string GetCSVData(string sType, string sValue)
246246
return '"' + sValue.Replace("\"", "\"\"") + '"';
247247
return sValue;
248248
case "DATE":
249-
long dateLong = long.Parse(sValue);
250-
DateTime date = dateTime.AddMilliseconds(dateLong).ToUniversalTime();
249+
long msFromEpoch = long.Parse(sValue); // SFDateConverter.csharpValToSfVal provides in [ms] from Epoch
250+
DateTime date = epoch.AddMilliseconds(msFromEpoch);
251251
return date.ToShortDateString();
252252
case "TIME":
253-
long timeLong = long.Parse(sValue);
254-
DateTime time = dateTime.AddMilliseconds(timeLong).ToUniversalTime();
255-
return time.ToLongTimeString();
253+
long nsSinceMidnight = long.Parse(sValue); // SFDateConverter.csharpValToSfVal provides in [ns] from Midnight
254+
DateTime time = epoch.AddTicks(nsSinceMidnight/100);
255+
return time.ToString("HH:mm:ss.fffffff");
256256
case "TIMESTAMP_LTZ":
257-
long ltzLong = long.Parse(sValue);
258-
TimeSpan ltzts = new TimeSpan(ltzLong / 100);
259-
DateTime ltzdt = dateTime + ltzts;
260-
return ltzdt.ToString();
257+
long nsFromEpochLtz = long.Parse(sValue); // SFDateConverter.csharpValToSfVal provides in [ns] from Epoch
258+
DateTime ltz = epoch.AddTicks(nsFromEpochLtz/100);
259+
return ltz.ToLocalTime().ToString("O"); // ISO 8601 format
261260
case "TIMESTAMP_NTZ":
262-
long ntzLong = long.Parse(sValue);
263-
TimeSpan ts = new TimeSpan(ntzLong/100);
264-
DateTime dt = dateTime + ts;
265-
return dt.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
261+
long nsFromEpochNtz = long.Parse(sValue); // SFDateConverter.csharpValToSfVal provides in [ns] from Epoch
262+
DateTime ntz = epoch.AddTicks(nsFromEpochNtz/100);
263+
return ntz.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
266264
case "TIMESTAMP_TZ":
267265
string[] tstzString = sValue.Split(' ');
268-
long tzLong = long.Parse(tstzString[0]);
269-
int tzInt = (int.Parse(tstzString[1]) - 1440) / 60;
270-
TimeSpan tzts = new TimeSpan(tzLong/100);
271-
DateTime tzdt = dateTime + tzts;
272-
TimeSpan tz = new TimeSpan(tzInt, 0, 0);
273-
DateTimeOffset tzDateTimeOffset = new DateTimeOffset(tzdt, tz);
266+
long nsFromEpochTz = long.Parse(tstzString[0]); // SFDateConverter provides in [ns] from Epoch
267+
int timeZoneOffset = int.Parse(tstzString[1]) - 1440; // SFDateConverter provides in minutes increased by 1440m
268+
DateTime timestamp = epoch.AddTicks(nsFromEpochTz/100).AddMinutes(timeZoneOffset);
269+
TimeSpan offset = TimeSpan.FromMinutes(timeZoneOffset);
270+
DateTimeOffset tzDateTimeOffset = new DateTimeOffset(timestamp.Ticks, offset);
274271
return tzDateTimeOffset.ToString("yyyy-MM-dd HH:mm:ss.fffffff zzz");
275-
276272
}
277273
return sValue;
278274
}

Snowflake.Data/Core/SFStatement.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ internal SFStatement(SFSession session)
147147
_restRequester = session.restRequester;
148148
}
149149

150+
internal string GetBindStage() => _bindStage;
151+
150152
private void AssignQueryRequestId()
151153
{
152154
lock (_requestIdLock)

0 commit comments

Comments
 (0)