Skip to content

Commit

Permalink
SNOW-1053935: Support for double quote character in delimited identif…
Browse files Browse the repository at this point in the history
…ier. (#926)

### Description
Support for double quote character in delimited identifier.

### Checklist
- [x] Code compiles correctly
- [x] Code is formatted according to [Coding
Conventions](../blob/master/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
  • Loading branch information
sfc-gh-jmartinezramirez authored Apr 26, 2024
1 parent 14cf8a5 commit 15f4c55
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 26 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
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
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 15f4c55

Please sign in to comment.