From 677a8bb84061df0b8a56e112ecb8d9844ba08b06 Mon Sep 17 00:00:00 2001 From: Naveen Date: Fri, 7 Aug 2026 01:06:22 +0530 Subject: [PATCH] Fix SPARSE before MASKED WITH column ordering (#216) CREATE TABLE column definitions that combine SPARSE and MASKED WITH in the order documented by the CREATE TABLE reference (SPARSE first, then MASKED WITH) failed to parse with "Incorrect syntax near 'MASKED'", while the reversed order parsed fine. In regularColumnBody the grammar only accepted the leading maskedClause before the SPARSE/FILESTREAM storage options, so MASKED could never follow SPARSE. Add a second, guarded maskedClause after the storage block (before the HIDDEN clause where present) so both documented and reversed orders parse. The `!vParent.IsMasked` guard prevents MASKED WITH from being specified twice on the same column. Applied to all grammars that support MASKED WITH: TSql130 through TSql180 and TSqlFabricDW. Tests: - Positive round-trip: TestScripts/SparseMaskedColumnOrderTests130.sql with baseline Baselines130/SparseMaskedColumnOrderTests130.sql, wired through Only130SyntaxTests.cs (verified across the TSql130-TSql180 parsers). - Negative: SparseMaskedColumnNegativeTest in ParserErrorsTests.cs asserts MASKED WITH specified twice is a parse error. --- SqlScriptDom/Parser/TSql/TSql130.g | 4 ++++ SqlScriptDom/Parser/TSql/TSql140.g | 4 ++++ SqlScriptDom/Parser/TSql/TSql150.g | 4 ++++ SqlScriptDom/Parser/TSql/TSql160.g | 4 ++++ SqlScriptDom/Parser/TSql/TSql170.g | 4 ++++ SqlScriptDom/Parser/TSql/TSql180.g | 4 ++++ SqlScriptDom/Parser/TSql/TSqlFabricDW.g | 4 ++++ .../SparseMaskedColumnOrderTests130.sql | 11 +++++++++++ Test/SqlDom/Only130SyntaxTests.cs | 1 + Test/SqlDom/ParserErrorsTests.cs | 15 +++++++++++++++ .../SparseMaskedColumnOrderTests130.sql | 12 ++++++++++++ 11 files changed, 67 insertions(+) create mode 100644 Test/SqlDom/Baselines130/SparseMaskedColumnOrderTests130.sql create mode 100644 Test/SqlDom/TestScripts/SparseMaskedColumnOrderTests130.sql diff --git a/SqlScriptDom/Parser/TSql/TSql130.g b/SqlScriptDom/Parser/TSql/TSql130.g index bc917aa6..a5df40d5 100644 --- a/SqlScriptDom/Parser/TSql/TSql130.g +++ b/SqlScriptDom/Parser/TSql/TSql130.g @@ -26976,6 +26976,10 @@ regularColumnBody [IndexAffectingStatement statementType, ColumnDefinition vPare } )? )? + ( + {NextTokenMatches(CodeGenerationSupporter.Masked) && !vParent.IsMasked}? + maskedClause[vParent] + )? { VerifyColumnDataType(vParent); } diff --git a/SqlScriptDom/Parser/TSql/TSql140.g b/SqlScriptDom/Parser/TSql/TSql140.g index 323a91ab..c619261e 100644 --- a/SqlScriptDom/Parser/TSql/TSql140.g +++ b/SqlScriptDom/Parser/TSql/TSql140.g @@ -27714,6 +27714,10 @@ regularColumnBody [IndexAffectingStatement statementType, ColumnDefinition vPare } )? )? + ( + {NextTokenMatches(CodeGenerationSupporter.Masked) && !vParent.IsMasked}? + maskedClause[vParent] + )? ( {NextTokenMatches(CodeGenerationSupporter.Hidden)}? tHidden:Identifier diff --git a/SqlScriptDom/Parser/TSql/TSql150.g b/SqlScriptDom/Parser/TSql/TSql150.g index 6fc72dfe..f6bf9024 100644 --- a/SqlScriptDom/Parser/TSql/TSql150.g +++ b/SqlScriptDom/Parser/TSql/TSql150.g @@ -28554,6 +28554,10 @@ regularColumnBody [IndexAffectingStatement statementType, ColumnDefinition vPare } )? )? + ( + {NextTokenMatches(CodeGenerationSupporter.Masked) && !vParent.IsMasked}? + maskedClause[vParent] + )? ( {NextTokenMatches(CodeGenerationSupporter.Hidden)}? tHidden:Identifier diff --git a/SqlScriptDom/Parser/TSql/TSql160.g b/SqlScriptDom/Parser/TSql/TSql160.g index 9bedb727..b8b3dec9 100644 --- a/SqlScriptDom/Parser/TSql/TSql160.g +++ b/SqlScriptDom/Parser/TSql/TSql160.g @@ -29034,6 +29034,10 @@ regularColumnBody [IndexAffectingStatement statementType, ColumnDefinition vPare } )? )? + ( + {NextTokenMatches(CodeGenerationSupporter.Masked) && !vParent.IsMasked}? + maskedClause[vParent] + )? ( {NextTokenMatches(CodeGenerationSupporter.Hidden)}? tHidden:Identifier diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g index ceb78600..87a8483d 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -29730,6 +29730,10 @@ regularColumnBody [IndexAffectingStatement statementType, ColumnDefinition vPare } )? )? + ( + {NextTokenMatches(CodeGenerationSupporter.Masked) && !vParent.IsMasked}? + maskedClause[vParent] + )? ( {NextTokenMatches(CodeGenerationSupporter.Hidden)}? tHidden:Identifier diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g index 934911db..0e6ab819 100644 --- a/SqlScriptDom/Parser/TSql/TSql180.g +++ b/SqlScriptDom/Parser/TSql/TSql180.g @@ -29968,6 +29968,10 @@ regularColumnBody [IndexAffectingStatement statementType, ColumnDefinition vPare } )? )? + ( + {NextTokenMatches(CodeGenerationSupporter.Masked) && !vParent.IsMasked}? + maskedClause[vParent] + )? ( {NextTokenMatches(CodeGenerationSupporter.Hidden)}? tHidden:Identifier diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g index 4ad40b85..7fafaec2 100644 --- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g +++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g @@ -29104,6 +29104,10 @@ regularColumnBody [IndexAffectingStatement statementType, ColumnDefinition vPare } )? )? + ( + {NextTokenMatches(CodeGenerationSupporter.Masked) && !vParent.IsMasked}? + maskedClause[vParent] + )? ( {NextTokenMatches(CodeGenerationSupporter.Hidden)}? tHidden:Identifier diff --git a/Test/SqlDom/Baselines130/SparseMaskedColumnOrderTests130.sql b/Test/SqlDom/Baselines130/SparseMaskedColumnOrderTests130.sql new file mode 100644 index 00000000..36ff7ead --- /dev/null +++ b/Test/SqlDom/Baselines130/SparseMaskedColumnOrderTests130.sql @@ -0,0 +1,11 @@ +CREATE TABLE t ( + c VARCHAR (100) SPARSE MASKED WITH (FUNCTION = 'default()') NULL +); + +CREATE TABLE t ( + c VARCHAR (100) SPARSE MASKED WITH (FUNCTION = 'default()') NULL +); + +CREATE TABLE t ( + c VARBINARY (MAX) SPARSE FILESTREAM MASKED WITH (FUNCTION = 'default()') NULL +); diff --git a/Test/SqlDom/Only130SyntaxTests.cs b/Test/SqlDom/Only130SyntaxTests.cs index 0665459f..945d90ef 100644 --- a/Test/SqlDom/Only130SyntaxTests.cs +++ b/Test/SqlDom/Only130SyntaxTests.cs @@ -27,6 +27,7 @@ public partial class SqlDomTests new ParserTest130("ColumnStoreInlineIndex130.sql", 10, 10, 10, 10, 10), new ParserTest130("CreateIndexStatementTests130.sql", 6, 6, 6, 6, 6), new ParserTest130("CreateTableTests130.sql", 62, 62, 62, 62, 62), + new ParserTest130("SparseMaskedColumnOrderTests130.sql"), new ParserTest130("CreateAlterSecurityPolicyStatementTests130.sql", 2, 33, 33, 33, 33), new ParserTest130("JsonForClauseTests130.sql", 14, 14, 14, 14, 14), new ParserTest130("DropStatementsTests130.sql", 10, 10, 9, 9, 9), diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs index faeb5a68..45b7daf3 100644 --- a/Test/SqlDom/ParserErrorsTests.cs +++ b/Test/SqlDom/ParserErrorsTests.cs @@ -206,6 +206,21 @@ public void CreateTableLedgerGeneratedAlwaysNegativeTest() new ParserErrorInfo(106, "SQL46010", "HIDEN")); } + /// + /// Negative test for GitHub issue #216: MASKED WITH must not be specified twice on the + /// same column, even though SPARSE and MASKED WITH are now accepted in either order. + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void SparseMaskedColumnNegativeTest() + { + string doubleMasked = + "CREATE TABLE t (c VARCHAR(100) MASKED WITH (FUNCTION = 'default()') SPARSE MASKED WITH (FUNCTION = 'email()') NULL)"; + ParserTestUtils.ErrorTest140(doubleMasked, + new ParserErrorInfo(doubleMasked.IndexOf("SPARSE MASKED") + 7, "SQL46010", "MASKED")); + } + /// /// Negative tests for HIDDEN columns /// diff --git a/Test/SqlDom/TestScripts/SparseMaskedColumnOrderTests130.sql b/Test/SqlDom/TestScripts/SparseMaskedColumnOrderTests130.sql new file mode 100644 index 00000000..8a29c18c --- /dev/null +++ b/Test/SqlDom/TestScripts/SparseMaskedColumnOrderTests130.sql @@ -0,0 +1,12 @@ +-- Regression tests for GitHub issue #216: SPARSE combined with MASKED WITH must parse in +-- either clause order. The order documented by the CREATE TABLE reference is SPARSE first, +-- then MASKED WITH. + +-- Documented order: SPARSE before MASKED WITH (this is the case that used to fail). +CREATE TABLE t (c VARCHAR(100) SPARSE MASKED WITH (FUNCTION = 'default()') NULL); + +-- Reversed order: MASKED WITH before SPARSE (this already worked). +CREATE TABLE t (c VARCHAR(100) MASKED WITH (FUNCTION = 'default()') SPARSE NULL); + +-- SPARSE FILESTREAM storage followed by MASKED WITH. +CREATE TABLE t (c VARBINARY(MAX) FILESTREAM SPARSE MASKED WITH (FUNCTION = 'default()') NULL);