diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CopyOption.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CopyOption.cs
index 94a4b11..7678371 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CopyOption.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CopyOption.cs
@@ -4,7 +4,6 @@
//
//------------------------------------------------------------------------------
using System;
-using System.Globalization;
namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator
{
@@ -26,14 +25,13 @@ public override void ExplicitVisit(CopyOption node)
break;
}
GenerateSymbol(TSqlTokenType.LeftParenthesis);
- string identityValue = ExtractStringValue(((CopyCredentialOption)node.Value).Identity);
- GenerateNameEqualsValue("Identity", string.Format(CultureInfo.InvariantCulture, "'{0}'", identityValue));
+ CopyCredentialOption credential = (CopyCredentialOption)node.Value;
+ GenerateNameEqualsValue("Identity", GetCopyOptionStringLiteral(credential.Identity));
- if (((CopyCredentialOption)node.Value).Secret != null)
+ if (credential.Secret != null)
{
GenerateSymbol(TSqlTokenType.Comma);
- string secretValue = ExtractStringValue(((CopyCredentialOption)node.Value).Secret);
- GenerateNameEqualsValue("Secret", string.Format(CultureInfo.InvariantCulture, "'{0}'", secretValue));
+ GenerateNameEqualsValue("Secret", GetCopyOptionStringLiteral(credential.Secret));
}
GenerateSymbol(TSqlTokenType.RightParenthesis);
NewLine();
@@ -48,29 +46,46 @@ public override void ExplicitVisit(CopyOption node)
}
else
{
- string optionValue = ExtractStringValue(node.Value);
-
- if (node.Kind == CopyOptionKind.FirstRow || node.Kind == CopyOptionKind.MaxErrors || node.Kind == CopyOptionKind.File_Format)
- GenerateNameEqualsValue(node.Kind.ToString(), string.Format(CultureInfo.InvariantCulture, "{0}", optionValue));
+ if (node.Kind == CopyOptionKind.FirstRow || node.Kind == CopyOptionKind.MaxErrors)
+ GenerateNameEqualsValue(node.Kind.ToString(), ExtractStringValue(node.Value));
+ else if (node.Kind == CopyOptionKind.File_Format)
+ {
+ Identifier identifier = ((SingleValueTypeCopyOption)node.Value).SingleValue.Identifier;
+ GenerateNameEqualsValue(node.Kind.ToString(), new Identifier
+ {
+ Value = ScriptGeneratorSupporter.GetCasedString(identifier.Value, _options.KeywordCasing),
+ QuoteType = identifier.QuoteType
+ });
+ }
else
- GenerateNameEqualsValue(node.Kind.ToString(), string.Format(CultureInfo.InvariantCulture, "'{0}'", optionValue));
+ GenerateNameEqualsValue(node.Kind.ToString(), GetCopyOptionStringLiteral(
+ (StringLiteral)((SingleValueTypeCopyOption)node.Value).SingleValue.ValueExpression));
}
NewLine();
}
}
///
- /// Extracts value from copy credential option.
+ /// Creates a string literal with the existing COPY option casing.
///
- /// Copy option.
- /// Extracted string.
- private string ExtractStringValue(StringLiteral credOption)
+ /// Copy option literal.
+ /// A cased copy of the literal.
+ private StringLiteral GetCopyOptionStringLiteral(StringLiteral literal)
{
- if (credOption == null)
+ if (literal == null)
{
throw new ArgumentNullException("Invalid Credential values");
}
- return credOption.Value;
+
+ // Legacy PascalCase applied to the leading quote, leaving the value lowercase.
+ KeywordCasing casing = _options.KeywordCasing == KeywordCasing.PascalCase
+ ? KeywordCasing.Lowercase : _options.KeywordCasing;
+ return new StringLiteral
+ {
+ Value = ScriptGeneratorSupporter.GetCasedString(literal.Value, casing),
+ IsNational = literal.IsNational,
+ Collation = literal.Collation
+ };
}
///
diff --git a/Test/SqlDom/ScriptGenerator/ScriptGeneratorTests.cs b/Test/SqlDom/ScriptGenerator/ScriptGeneratorTests.cs
index 72f0b68..d66656b 100644
--- a/Test/SqlDom/ScriptGenerator/ScriptGeneratorTests.cs
+++ b/Test/SqlDom/ScriptGenerator/ScriptGeneratorTests.cs
@@ -17,6 +17,132 @@ namespace SqlStudio.Tests.UTSqlScriptDom
[TestClass]
public class SqlScriptGeneratorTests
{
+ [DataTestMethod]
+ [DataRow("ERRORFILE = 'reader''s/files'", "ErrorFile = 'READER''S/FILES'")]
+ [DataRow("ERRORFILE = '''reader''''s'''", "ErrorFile = '''READER''''S'''")]
+ [DataRow("ERRORFILE = N'r\u00e9ader''s/files'", "ErrorFile = N'R\u00c9ADER''S/FILES'")]
+ [DataRow("FIELDTERMINATOR = 'a''b'", "FieldTerminator = 'A''B'")]
+ [DataRow("ROWTERMINATOR = 'a''''b'", "RowTerminator = 'A''''B'")]
+ [DataRow("FIELDQUOTE = ''''", "FieldQuote = ''''")]
+ [DataRow("FIELDQUOTE = ''", "FieldQuote = ''")]
+ [DataRow("FIELDTERMINATOR = '0x0a'", "FieldTerminator = '0X0A'")]
+ [DataRow("FILE_TYPE = 'csv'", "File_Type = 'CSV'")]
+ [DataRow("COMPRESSION = 'gzip'", "Compression = 'GZIP'")]
+ [DataRow("DATEFORMAT = 'ymd'", "DateFormat = 'YMD'")]
+ [DataRow("ENCODING = 'utf8'", "Encoding = 'UTF8'")]
+ [DataRow("IDENTITY_INSERT = 'on'", "Identity_Insert = 'ON'")]
+ [DataRow("FIRSTROW = 2", "FirstRow = 2")]
+ [DataRow("MAXERRORS = 0", "MaxErrors = 0")]
+ [DataRow("FILE_FORMAT = fileformat", "File_Format = FILEFORMAT")]
+ [DataRow("FILE_FORMAT = [reader's format]", "File_Format = [READER'S FORMAT]")]
+ [DataRow("FILE_FORMAT = [reader]]s]", "File_Format = [READER]]S]")]
+ [DataRow("FILE_FORMAT = \"reader\"\"s format\"", "File_Format = \"READER\"\"S FORMAT\"")]
+ [DataRow("FILE_FORMAT = [select]", "File_Format = [SELECT]")]
+ [DataRow("FILE_FORMAT = [format;(1)]", "File_Format = [FORMAT;(1)]")]
+ [DataRow("CREDENTIAL = (IDENTITY = 'Shared Access Signature', SECRET = 'reader''s''''key')",
+ "Credential = (Identity = 'SHARED ACCESS SIGNATURE',Secret = 'READER''S''''KEY')")]
+ [DataRow("ERRORFILE_CREDENTIAL = (IDENTITY = 'Shared Access Signature', SECRET = '''key''')",
+ "ERRORFILE_CREDENTIAL = (Identity = 'SHARED ACCESS SIGNATURE',Secret = '''KEY''')")]
+ [DataRow("CREDENTIAL = (IDENTITY = N'Shared Access Signature', SECRET = N'key''value')",
+ "Credential = (Identity = N'SHARED ACCESS SIGNATURE',Secret = N'KEY''VALUE')")]
+ [DataRow("CREDENTIAL = (IDENTITY = 'Managed Identity')",
+ "Credential = (Identity = 'MANAGED IDENTITY')")]
+ [DataRow("ERRORFILE_CREDENTIAL = (IDENTITY = 'Managed Identity')",
+ "ERRORFILE_CREDENTIAL = (Identity = 'MANAGED IDENTITY')")]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void CopyOptionRoundTrip(string option, string expectedOption)
+ {
+ SqlVersion[] versions =
+ {
+ SqlVersion.Sql130, SqlVersion.Sql140, SqlVersion.Sql150, SqlVersion.Sql160,
+ SqlVersion.Sql170, SqlVersion.Sql180, SqlVersion.SqlFabricDW
+ };
+
+ foreach (SqlVersion version in versions)
+ {
+ VerifyCopyOptionRoundTrip(TSqlParser.CreateParser(version, true),
+ ParserTestUtils.CreateScriptGen(version), option, expectedOption);
+ }
+
+ VerifyCopyOptionRoundTrip(new TSql160Parser(true), new SqlServerlessScriptGenerator(),
+ option, expectedOption);
+ }
+
+ [DataTestMethod]
+ [DataRow(KeywordCasing.Uppercase, "ErrorFile = 'READER''S/FILES'", "File_Format = [MY FORMAT]")]
+ [DataRow(KeywordCasing.Lowercase, "ErrorFile = 'reader''s/files'", "File_Format = [my format]")]
+ [DataRow(KeywordCasing.PascalCase, "ErrorFile = 'reader''s/files'", "File_Format = [My format]")]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void CopyOptionCasing(KeywordCasing casing, string expectedLiteral, string expectedIdentifier)
+ {
+ var generator = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { KeywordCasing = casing });
+ VerifyCopyOptionRoundTrip(new TSql160Parser(true), generator,
+ "ERRORFILE = 'Reader''s/Files'", expectedLiteral);
+ VerifyCopyOptionRoundTrip(new TSql160Parser(true), generator,
+ "FILE_FORMAT = [My Format]", expectedIdentifier);
+ }
+
+ private static void VerifyCopyOptionRoundTrip(TSqlParser parser, SqlScriptGenerator generator,
+ string option, string expectedOption)
+ {
+ string sql = "COPY INTO dbo.t FROM 'https://example.invalid/data.csv' WITH (" + option + ");";
+ var script = (TSqlScript)parser.Parse(new StringReader(sql), out var errors);
+ Assert.AreEqual(0, errors.Count, sql);
+ var original = (CopyStatement)script.Batches[0].Statements[0];
+ var originalCredential = original.Options[0].Value as CopyCredentialOption;
+ var originalSingleValue = (original.Options[0].Value as SingleValueTypeCopyOption)?.SingleValue;
+ string originalIdentity = originalCredential?.Identity.Value;
+ string originalSecret = originalCredential?.Secret?.Value;
+ string originalValue = originalSingleValue?.Value;
+ QuoteType? originalQuoteType = originalSingleValue?.Identifier?.QuoteType;
+
+ generator.GenerateScript(script, out string generated);
+ StringAssert.Contains(generated, expectedOption, parser.GetType().Name);
+ Assert.AreEqual(originalIdentity, originalCredential?.Identity.Value);
+ Assert.AreEqual(originalSecret, originalCredential?.Secret?.Value);
+ Assert.AreEqual(originalValue, originalSingleValue?.Value);
+ Assert.AreEqual(originalQuoteType, originalSingleValue?.Identifier?.QuoteType);
+
+ var reparsed = (TSqlScript)parser.Parse(new StringReader(generated), out var reparseErrors);
+ Assert.AreEqual(0, reparseErrors.Count, generated);
+ Assert.AreEqual(1, reparsed.Batches.Count, generated);
+ Assert.AreEqual(1, reparsed.Batches[0].Statements.Count, generated);
+ Assert.IsInstanceOfType(reparsed.Batches[0].Statements[0], typeof(CopyStatement));
+ var copy = (CopyStatement)reparsed.Batches[0].Statements[0];
+ Assert.AreEqual(1, copy.Options.Count);
+ Assert.AreEqual(original.Options[0].Kind, copy.Options[0].Kind);
+
+ var expected = (TSqlScript)parser.Parse(new StringReader(
+ "COPY INTO dbo.t FROM 'https://example.invalid/data.csv' WITH (" + expectedOption + ");"),
+ out var expectedErrors);
+ Assert.AreEqual(0, expectedErrors.Count);
+ var expectedCopy = (CopyStatement)expected.Batches[0].Statements[0];
+ if (copy.Options[0].Value is CopyCredentialOption credential)
+ {
+ var expectedCredential = (CopyCredentialOption)expectedCopy.Options[0].Value;
+ Assert.AreEqual(expectedCredential.Identity.Value, credential.Identity.Value);
+ Assert.AreEqual(expectedCredential.Identity.IsNational, credential.Identity.IsNational);
+ Assert.AreEqual(expectedCredential.Secret?.Value, credential.Secret?.Value);
+ Assert.AreEqual(expectedCredential.Secret?.IsNational, credential.Secret?.IsNational);
+ }
+ else
+ {
+ var actualValue = ((SingleValueTypeCopyOption)copy.Options[0].Value).SingleValue;
+ var expectedValue = ((SingleValueTypeCopyOption)expectedCopy.Options[0].Value).SingleValue;
+ Assert.AreEqual(expectedValue.Value, actualValue.Value);
+ Assert.AreEqual(expectedValue.Identifier?.QuoteType, actualValue.Identifier?.QuoteType);
+ Assert.AreEqual((expectedValue.ValueExpression as StringLiteral)?.IsNational,
+ (actualValue.ValueExpression as StringLiteral)?.IsNational);
+ }
+
+ generator.GenerateScript(reparsed, out string regenerated);
+ Assert.AreEqual(generated, regenerated);
+ generator.GenerateScript(script, out string generatedAgain);
+ Assert.AreEqual(generated, generatedAgain);
+ }
+
[TestMethod]
[Priority(0)]
[SqlStudioTestCategory(Category.UnitTest)]