Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…nector-net into SNOW-798828-Target-NET-Standard-2.0
  • Loading branch information
sfc-gh-ext-simba-lf committed May 9, 2024
2 parents 3009dcb + 0c19e2d commit 1e36fb0
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 37 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ The following table lists all valid connection properties:
| FILE_TRANSFER_MEMORY_THRESHOLD | No | The maximum number of bytes to store in memory used in order to provide a file encryption. If encrypting/decrypting file size exceeds provided value a temporary file will be created and the work will be continued in the temporary file instead of memory. <br/> If no value provided 1MB will be used as a default value (that is 1048576 bytes). <br/> It is possible to configure any integer value bigger than zero representing maximal number of bytes to reside in memory. |
| CLIENT_CONFIG_FILE | No | The location of the client configuration json file. In this file you can configure easy logging feature. |
| ALLOWUNDERSCORESINHOST | No | Specifies whether to allow underscores in account names. This impacts PrivateLink customers whose account names contain underscores. In this situation, you must override the default value by setting allowUnderscoresInHost to true. |
| QUERY_TAG | No | Optional string that can be used to tag queries and other SQL statements executed within a connection. The tags are displayed in the output of the QUERY_HISTORY , QUERY_HISTORY_BY_* functions. |
| QUERY_TAG | No | Optional string that can be used to tag queries and other SQL statements executed within a connection. The tags are displayed in the output of the QUERY_HISTORY , QUERY_HISTORY_BY_* functions.<br/> To set QUERY_TAG on the statement level you can use SnowflakeDbCommand.QueryTag. |

<br />

Expand Down Expand Up @@ -234,6 +234,25 @@ The following examples show how you can include different types of special chara

Note that previously you needed to use a double equal sign (==) to escape the character. However, beginning with version 2.0.18, you can use a single equal size.


Snowflake supports using [double quote identifiers](https://docs.snowflake.com/en/sql-reference/identifiers-syntax#double-quoted-identifiers) for object property values (WAREHOUSE, DATABASE, SCHEMA AND ROLES). The value should be delimited with `\"` in the connection string. The value is case-sensitive and allow to use special characters as part of the value.

```cs
string connectionString = String.Format(
"account=testaccount; " +
"database=\"testDB\";"
);
```
- To include a `"` character as part of the value should be escaped using `\"\"`.

```cs
string connectionString = String.Format(
"account=testaccount; " +
"database=\"\"\"test\"\"user\"\"\";" // DATABASE => ""test"db""
);
```


### Other Authentication Methods

If you are using a different method for authentication, see the examples below:
Expand Down
20 changes: 20 additions & 0 deletions Snowflake.Data.Tests/IntegrationTests/SFDbCommandIT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1624,5 +1624,25 @@ public void TestGetResultsOfUnknownQueryIdWithConfiguredRetry()
conn.Close();
}
}

[Test]
public void TestSetQueryTagOverridesConnectionString()
{
using (var conn = new SnowflakeDbConnection())
{
string expectedQueryTag = "Test QUERY_TAG 12345";
string connectQueryTag = "Test 123";
conn.ConnectionString = ConnectionString + $";query_tag={connectQueryTag}";

conn.Open();
var command = conn.CreateCommand();
((SnowflakeDbCommand)command).QueryTag = expectedQueryTag;
// This query itself will be part of the history and will have the query tag
command.CommandText = "SELECT QUERY_TAG FROM table(information_schema.query_history_by_session())";
var queryTag = command.ExecuteScalar();

Assert.AreEqual(expectedQueryTag, queryTag);
}
}
}
}
48 changes: 32 additions & 16 deletions Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ public void TestValidateCorrectAccountNames(string accountName, string expectedA
{
// arrange
var connectionString = $"ACCOUNT={accountName};USER=test;PASSWORD=test;";

// act
var properties = SFSessionProperties.ParseConnectionString(connectionString, null);

// assert
Assert.AreEqual(expectedAccountName, properties[SFSessionProperty.ACCOUNT]);
Assert.AreEqual(expectedHost, properties[SFSessionProperty.HOST]);
}

[Test]
[TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;FILE_TRANSFER_MEMORY_THRESHOLD=0;", "Error: Invalid parameter value 0 for FILE_TRANSFER_MEMORY_THRESHOLD")]
[TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;FILE_TRANSFER_MEMORY_THRESHOLD=xyz;", "Error: Invalid parameter value xyz for FILE_TRANSFER_MEMORY_THRESHOLD")]
Expand All @@ -63,7 +63,7 @@ public void TestThatItFailsForWrongConnectionParameter(string connectionString,
var exception = Assert.Throws<SnowflakeDbException>(
() => SFSessionProperties.ParseConnectionString(connectionString, null)
);

// assert
Assert.AreEqual(SFError.INVALID_CONNECTION_PARAMETER_VALUE.GetAttribute<SFErrorAttr>().errorCode, exception.ErrorCode);
Assert.IsTrue(exception.Message.Contains(expectedErrorMessagePart));
Expand All @@ -78,12 +78,10 @@ public void TestThatItFailsIfNoAccountSpecified(string connectionString)
var exception = Assert.Throws<SnowflakeDbException>(
() => SFSessionProperties.ParseConnectionString(connectionString, null)
);

// assert
Assert.AreEqual(SFError.MISSING_CONNECTION_PROPERTY.GetAttribute<SFErrorAttr>().errorCode, exception.ErrorCode);
}



[Test]
[TestCase("DB", SFSessionProperty.DB, "\"testdb\"")]
Expand All @@ -94,28 +92,46 @@ public void TestValidateSupportEscapedQuotesValuesForObjectProperties(string pro
{
// arrange
var connectionString = $"ACCOUNT=test;{propertyName}={value};USER=test;PASSWORD=test;";

// act
var properties = SFSessionProperties.ParseConnectionString(connectionString, null);

// assert
Assert.AreEqual(value, properties[sessionProperty]);
}


[Test]
[TestCase("DB", SFSessionProperty.DB, "testdb", "testdb")]
[TestCase("DB", SFSessionProperty.DB, "\"testdb\"", "\"testdb\"")]
[TestCase("DB", SFSessionProperty.DB, "\"\"\"testDB\"\"\"", "\"\"testDB\"\"")]
[TestCase("DB", SFSessionProperty.DB, "\"\"\"test\"\"DB\"\"\"", "\"\"test\"DB\"\"")]
[TestCase("SCHEMA", SFSessionProperty.SCHEMA, "\"quoted\"\"Schema\"", "\"quoted\"Schema\"")]
public void TestValidateSupportEscapedQuotesInsideValuesForObjectProperties(string propertyName, SFSessionProperty sessionProperty, string value, string expectedValue)
{
// arrange
var connectionString = $"ACCOUNT=test;{propertyName}={value};USER=test;PASSWORD=test;";

// act
var properties = SFSessionProperties.ParseConnectionString(connectionString, null);

// assert
Assert.AreEqual(expectedValue, properties[sessionProperty]);
}

[Test]
public void TestProcessEmptyUserAndPasswordInConnectionString()
{
// arrange
var connectionString = $"ACCOUNT=test;USER=;PASSWORD=;";

// act
var properties = SFSessionProperties.ParseConnectionString(connectionString, null);

// assert
Assert.AreEqual(string.Empty, properties[SFSessionProperty.USER]);
Assert.AreEqual(string.Empty, properties[SFSessionProperty.PASSWORD]);
}

public static IEnumerable<TestCase> ConnectionStringTestCases()
{
string defAccount = "testaccount";
Expand Down Expand Up @@ -168,7 +184,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
}
};

var testCaseWithBrowserResponseTimeout = new TestCase()
{
ConnectionString = $"ACCOUNT={defAccount};BROWSER_RESPONSE_TIMEOUT=180;authenticator=externalbrowser",
Expand Down Expand Up @@ -501,7 +517,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.QUERY_TAG, testQueryTag }
}
};

return new TestCase[]
{
simpleTestCase,
Expand All @@ -518,7 +534,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
testCaseQueryTag
};
}

internal class TestCase
{
public string ConnectionString { get; set; }
Expand Down
23 changes: 14 additions & 9 deletions Snowflake.Data/Client/SnowflakeDbCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public override int CommandTimeout
get; set;
}

public string QueryTag
{
get; set;
}

public override CommandType CommandType
{
get
Expand Down Expand Up @@ -132,7 +137,7 @@ protected override DbConnection DbConnection
connection = sfc;
if (sfc.SfSession != null)
{
sfStatement = new SFStatement(sfc.SfSession);
sfStatement = new SFStatement(sfc.SfSession, QueryTag);
}
}
}
Expand All @@ -143,7 +148,7 @@ protected override DbParameterCollection DbParameterCollection
{
return this.parameterCollection;
}
}
}

protected override DbTransaction DbTransaction
{
Expand Down Expand Up @@ -373,15 +378,15 @@ private static Dictionary<string, BindingDTO> convertToBindList(List<SnowflakeDb

if (parameter.Value.GetType().IsArray &&
// byte array and char array will not be treated as array binding
parameter.Value.GetType().GetElementType() != typeof(char) &&
parameter.Value.GetType().GetElementType() != typeof(char) &&
parameter.Value.GetType().GetElementType() != typeof(byte))
{
List<object> vals = new List<object>();
foreach(object val in (Array)parameter.Value)
{
// if the user is using interface, SFDataType will be None and there will
// if the user is using interface, SFDataType will be None and there will
// a conversion from DbType to SFDataType
// if the user is using concrete class, they should specify SFDataType.
// if the user is using concrete class, they should specify SFDataType.
if (parameter.SFDataType == SFDataType.None)
{
Tuple<string, string> typeAndVal = SFDataConverter
Expand All @@ -392,7 +397,7 @@ private static Dictionary<string, BindingDTO> convertToBindList(List<SnowflakeDb
}
else
{
bindingType = parameter.SFDataType.ToString();
bindingType = parameter.SFDataType.ToString();
vals.Add(SFDataConverter.csharpValToSfVal(parameter.SFDataType, val));
}
}
Expand Down Expand Up @@ -420,21 +425,21 @@ private static Dictionary<string, BindingDTO> convertToBindList(List<SnowflakeDb
}
}

private void SetStatement()
private void SetStatement()
{
if (connection == null)
{
throw new SnowflakeDbException(SFError.EXECUTE_COMMAND_ON_CLOSED_CONNECTION);
}

var session = (connection as SnowflakeDbConnection).SfSession;

// SetStatement is called when executing a command. If SfSession is null
// the connection has never been opened. Exception might be a bit vague.
if (session == null)
throw new SnowflakeDbException(SFError.EXECUTE_COMMAND_ON_CLOSED_CONNECTION);

this.sfStatement = new SFStatement(session);
this.sfStatement = new SFStatement(session, QueryTag);
}

private SFBaseResultSet ExecuteInternal(bool describeOnly = false, bool asyncExec = false)
Expand Down
9 changes: 8 additions & 1 deletion Snowflake.Data/Core/SFStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,14 @@ internal SFStatement(SFSession session)
_restRequester = session.restRequester;
_queryTag = session._queryTag;
}


internal SFStatement(SFSession session, string queryTag)
{
SfSession = session;
_restRequester = session.restRequester;
_queryTag = queryTag ?? session._queryTag;
}

internal string GetBindStage() => _bindStage;

private void AssignQueryRequestId()
Expand Down
32 changes: 22 additions & 10 deletions Snowflake.Data/Core/Session/SFSessionProperty.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2012-2021 Snowflake Computing Inc. All rights reserved.
*/

Expand Down Expand Up @@ -167,7 +167,7 @@ internal static SFSessionProperties ParseConnectionString(string connectionStrin
logger.Info("Start parsing connection string.");
var builder = new DbConnectionStringBuilder();
try
{
{
builder.ConnectionString = connectionString;
}
catch (ArgumentException e)
Expand Down Expand Up @@ -197,7 +197,7 @@ internal static SFSessionProperties ParseConnectionString(string connectionStrin
logger.Warn($"Property {keys[i]} not found ignored.", e);
}
}

UpdatePropertiesForSpecialCases(properties, connectionString);

var useProxy = false;
Expand Down Expand Up @@ -241,7 +241,7 @@ internal static SFSessionProperties ParseConnectionString(string connectionStrin
ValidateAccountDomain(properties);

var allowUnderscoresInHost = ParseAllowUnderscoresInHost(properties);

// compose host value if not specified
if (!properties.ContainsKey(SFSessionProperty.HOST) ||
(0 == properties[SFSessionProperty.HOST].Length))
Expand All @@ -260,7 +260,7 @@ internal static SFSessionProperties ParseConnectionString(string connectionStrin
}

// Trim the account name to remove the region and cloud platform if any were provided
// because the login request data does not expect region and cloud information to be
// because the login request data does not expect region and cloud information to be
// passed on for account_name
properties[SFSessionProperty.ACCOUNT] = properties[SFSessionProperty.ACCOUNT].Split('.')[0];

Expand All @@ -287,15 +287,15 @@ private static void UpdatePropertiesForSpecialCases(SFSessionProperties properti
{
var sessionProperty = (SFSessionProperty)Enum.Parse(
typeof(SFSessionProperty), propertyName);
properties[sessionProperty]= tokens[1];
properties[sessionProperty]= ProcessObjectEscapedCharacters(tokens[1]);
}

break;
}
case "USER":
case "PASSWORD":
{

var sessionProperty = (SFSessionProperty)Enum.Parse(
typeof(SFSessionProperty), propertyName);
if (!properties.ContainsKey(sessionProperty))
Expand All @@ -310,6 +310,18 @@ private static void UpdatePropertiesForSpecialCases(SFSessionProperties properti
}
}

private static string ProcessObjectEscapedCharacters(string objectValue)
{
var match = Regex.Match(objectValue, "^\"(.*)\"$");
if(match.Success)
{
var replaceEscapedQuotes = match.Groups[1].Value.Replace("\"\"", "\"");
return $"\"{replaceEscapedQuotes}\"";
}

return objectValue;
}

private static void ValidateAccountDomain(SFSessionProperties properties)
{
var account = properties[SFSessionProperty.ACCOUNT];
Expand Down Expand Up @@ -372,7 +384,7 @@ private static void ValidateFileTransferMaxBytesInMemoryProperty(SFSessionProper
logger.Error($"Value for parameter {propertyName} could not be parsed");
throw new SnowflakeDbException(e, SFError.INVALID_CONNECTION_PARAMETER_VALUE, maxBytesInMemoryString, propertyName);
}

if (maxBytesInMemory <= 0)
{
logger.Error($"Value for parameter {propertyName} should be greater than 0");
Expand All @@ -381,7 +393,7 @@ private static void ValidateFileTransferMaxBytesInMemoryProperty(SFSessionProper
SFError.INVALID_CONNECTION_PARAMETER_VALUE, maxBytesInMemoryString, propertyName);
}
}

private static bool IsRequired(SFSessionProperty sessionProperty, SFSessionProperties properties)
{
if (sessionProperty.Equals(SFSessionProperty.PASSWORD))
Expand Down

0 comments on commit 1e36fb0

Please sign in to comment.