From 46922ef2152dc56e8df989269b2133c46c944539 Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Thu, 23 Apr 2026 09:29:30 -0700 Subject: [PATCH] Add support for TRIM function in T-SQL parser and related tests --- SqlScriptDom/Parser/TSql/TSql140.g | 16 ++++++++ SqlScriptDom/Parser/TSql/TSql150.g | 16 ++++++++ SqlScriptDom/Parser/TSql/TSql160.g | 21 +++++++++++ SqlScriptDom/Parser/TSql/TSql170.g | 21 +++++++++++ SqlScriptDom/Parser/TSql/TSql180.g | 21 +++++++++++ SqlScriptDom/Parser/TSql/TSqlFabricDW.g | 21 +++++++++++ .../Baselines140/TrimFromReturnTests140.sql | 7 ++++ .../Baselines160/TrimFromReturnTests160.sql | 37 +++++++++++++++++++ .../Baselines170/TrimFromReturnTests160.sql | 37 +++++++++++++++++++ .../Baselines180/TrimFromReturnTests160.sql | 37 +++++++++++++++++++ Test/SqlDom/Only140SyntaxTests.cs | 1 + Test/SqlDom/Only160SyntaxTests.cs | 1 + Test/SqlDom/Only170SyntaxTests.cs | 3 +- Test/SqlDom/Only180SyntaxTests.cs | 3 +- Test/SqlDom/ParserErrorsTests.cs | 2 +- .../TestScripts/TrimFromReturnTests140.sql | 7 ++++ .../TestScripts/TrimFromReturnTests160.sql | 31 ++++++++++++++++ 17 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 Test/SqlDom/Baselines140/TrimFromReturnTests140.sql create mode 100644 Test/SqlDom/Baselines160/TrimFromReturnTests160.sql create mode 100644 Test/SqlDom/Baselines170/TrimFromReturnTests160.sql create mode 100644 Test/SqlDom/Baselines180/TrimFromReturnTests160.sql create mode 100644 Test/SqlDom/TestScripts/TrimFromReturnTests140.sql create mode 100644 Test/SqlDom/TestScripts/TrimFromReturnTests160.sql diff --git a/SqlScriptDom/Parser/TSql/TSql140.g b/SqlScriptDom/Parser/TSql/TSql140.g index 3139191..2424bd5 100644 --- a/SqlScriptDom/Parser/TSql/TSql140.g +++ b/SqlScriptDom/Parser/TSql/TSql140.g @@ -29739,6 +29739,9 @@ expressionPrimary [ExpressionFlags expressionFlags] returns [PrimaryExpression v | {NextTokenMatches(CodeGenerationSupporter.IIf) && (LA(2) == LeftParenthesis)}? vResult=iIfCall + | + {NextTokenMatches(CodeGenerationSupporter.Trim) && (LA(2) == LeftParenthesis) && (LA(3) != Star)}? + vResult=trimCall | (Identifier LeftParenthesis)=> vResult=builtInFunctionCall @@ -30230,6 +30233,19 @@ builtInFunctionCall returns [FunctionCall vResult = FragmentFactory.CreateFragme ) ; +trimCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; +} + : vIdentifier=nonQuotedIdentifier + { + Match(vIdentifier, CodeGenerationSupporter.Trim); + vResult.FunctionName = vIdentifier; + } + LeftParenthesis + trimBuiltInFunctionCall[vResult] + ; + regularBuiltInFunctionCall [FunctionCall vParent] { ColumnReferenceExpression vColumn; diff --git a/SqlScriptDom/Parser/TSql/TSql150.g b/SqlScriptDom/Parser/TSql/TSql150.g index 35a14f1..ea468dc 100644 --- a/SqlScriptDom/Parser/TSql/TSql150.g +++ b/SqlScriptDom/Parser/TSql/TSql150.g @@ -30885,6 +30885,9 @@ expressionPrimary [ExpressionFlags expressionFlags] returns [PrimaryExpression v | {NextTokenMatches(CodeGenerationSupporter.IIf) && (LA(2) == LeftParenthesis)}? vResult=iIfCall + | + {NextTokenMatches(CodeGenerationSupporter.Trim) && (LA(2) == LeftParenthesis) && (LA(3) != Star)}? + vResult=trimCall | (Identifier LeftParenthesis)=> vResult=builtInFunctionCall @@ -31383,6 +31386,19 @@ builtInFunctionCall returns [FunctionCall vResult = FragmentFactory.CreateFragme ) ; +trimCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; +} + : vIdentifier=nonQuotedIdentifier + { + Match(vIdentifier, CodeGenerationSupporter.Trim); + vResult.FunctionName = vIdentifier; + } + LeftParenthesis + trimBuiltInFunctionCall[vResult] + ; + regularBuiltInFunctionCall [FunctionCall vParent] { ColumnReferenceExpression vColumn; diff --git a/SqlScriptDom/Parser/TSql/TSql160.g b/SqlScriptDom/Parser/TSql/TSql160.g index 4d794ca..1fbb91e 100644 --- a/SqlScriptDom/Parser/TSql/TSql160.g +++ b/SqlScriptDom/Parser/TSql/TSql160.g @@ -31482,6 +31482,9 @@ expressionPrimary [ExpressionFlags expressionFlags] returns [PrimaryExpression v | {NextTokenMatches(CodeGenerationSupporter.JsonArray) && (LA(2) == LeftParenthesis)}? vResult=jsonArrayCall + | + {NextTokenMatches(CodeGenerationSupporter.Trim) && (LA(2) == LeftParenthesis) && (LA(3) != Star)}? + vResult=trimCall | (Identifier LeftParenthesis)=> vResult=builtInFunctionCall @@ -32528,6 +32531,24 @@ jsonArrayCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragmen jsonArrayBuiltInFunctionCall[vResult] ; +trimCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; +} + : vIdentifier=nonQuotedIdentifier + { + Match(vIdentifier, CodeGenerationSupporter.Trim); + vResult.FunctionName = vIdentifier; + } + LeftParenthesis + ( + {NextTokenMatches(CodeGenerationSupporter.Leading) || NextTokenMatches(CodeGenerationSupporter.Trailing) || NextTokenMatches(CodeGenerationSupporter.Both)}? + trim3ArgsBuiltInFunctionCall[vResult] + | + trimBuiltInFunctionCall[vResult] + ) + ; + coalesceExpression [ExpressionFlags expressionFlags] returns [CoalesceExpression vResult = this.FragmentFactory.CreateFragment()] { ScalarExpression vExpression; diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g index 88ce660..0fb0514 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -32229,6 +32229,9 @@ expressionPrimary [ExpressionFlags expressionFlags] returns [PrimaryExpression v | {NextTokenMatches(CodeGenerationSupporter.JsonArray) && (LA(2) == LeftParenthesis)}? vResult=jsonArrayCall + | + {NextTokenMatches(CodeGenerationSupporter.Trim) && (LA(2) == LeftParenthesis) && (LA(3) != Star)}? + vResult=trimCall | (Identifier LeftParenthesis)=> vResult=builtInFunctionCall @@ -33656,6 +33659,24 @@ jsonArrayCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragmen jsonArrayBuiltInFunctionCall[vResult] ; +trimCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; +} + : vIdentifier=nonQuotedIdentifier + { + Match(vIdentifier, CodeGenerationSupporter.Trim); + vResult.FunctionName = vIdentifier; + } + LeftParenthesis + ( + {NextTokenMatches(CodeGenerationSupporter.Leading) || NextTokenMatches(CodeGenerationSupporter.Trailing) || NextTokenMatches(CodeGenerationSupporter.Both)}? + trim3ArgsBuiltInFunctionCall[vResult] + | + trimBuiltInFunctionCall[vResult] + ) + ; + coalesceExpression [ExpressionFlags expressionFlags] returns [CoalesceExpression vResult = this.FragmentFactory.CreateFragment()] { ScalarExpression vExpression; diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g index 19e66ad..4af433a 100644 --- a/SqlScriptDom/Parser/TSql/TSql180.g +++ b/SqlScriptDom/Parser/TSql/TSql180.g @@ -32229,6 +32229,9 @@ expressionPrimary [ExpressionFlags expressionFlags] returns [PrimaryExpression v | {NextTokenMatches(CodeGenerationSupporter.JsonArray) && (LA(2) == LeftParenthesis)}? vResult=jsonArrayCall + | + {NextTokenMatches(CodeGenerationSupporter.Trim) && (LA(2) == LeftParenthesis) && (LA(3) != Star)}? + vResult=trimCall | (Identifier LeftParenthesis)=> vResult=builtInFunctionCall @@ -33656,6 +33659,24 @@ jsonArrayCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragmen jsonArrayBuiltInFunctionCall[vResult] ; +trimCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; +} + : vIdentifier=nonQuotedIdentifier + { + Match(vIdentifier, CodeGenerationSupporter.Trim); + vResult.FunctionName = vIdentifier; + } + LeftParenthesis + ( + {NextTokenMatches(CodeGenerationSupporter.Leading) || NextTokenMatches(CodeGenerationSupporter.Trailing) || NextTokenMatches(CodeGenerationSupporter.Both)}? + trim3ArgsBuiltInFunctionCall[vResult] + | + trimBuiltInFunctionCall[vResult] + ) + ; + coalesceExpression [ExpressionFlags expressionFlags] returns [CoalesceExpression vResult = this.FragmentFactory.CreateFragment()] { ScalarExpression vExpression; diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g index 21fd33d..21590b6 100644 --- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g +++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g @@ -31467,6 +31467,9 @@ expressionPrimary [ExpressionFlags expressionFlags] returns [PrimaryExpression v | {NextTokenMatches(CodeGenerationSupporter.JsonArray) && (LA(2) == LeftParenthesis)}? vResult=jsonArrayCall + | + {NextTokenMatches(CodeGenerationSupporter.Trim) && (LA(2) == LeftParenthesis) && (LA(3) != Star)}? + vResult=trimCall | {NextTokenMatches(CodeGenerationSupporter.AIAnalyzeSentiment) && (LA(2) == LeftParenthesis)}? vResult=aiAnalyzeSentimentFunctionCall @@ -32694,6 +32697,24 @@ jsonArrayCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragmen jsonArrayBuiltInFunctionCall[vResult] ; +trimCall returns [FunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; +} + : vIdentifier=nonQuotedIdentifier + { + Match(vIdentifier, CodeGenerationSupporter.Trim); + vResult.FunctionName = vIdentifier; + } + LeftParenthesis + ( + {NextTokenMatches(CodeGenerationSupporter.Leading) || NextTokenMatches(CodeGenerationSupporter.Trailing) || NextTokenMatches(CodeGenerationSupporter.Both)}? + trim3ArgsBuiltInFunctionCall[vResult] + | + trimBuiltInFunctionCall[vResult] + ) + ; + coalesceExpression [ExpressionFlags expressionFlags] returns [CoalesceExpression vResult = this.FragmentFactory.CreateFragment()] { ScalarExpression vExpression; diff --git a/Test/SqlDom/Baselines140/TrimFromReturnTests140.sql b/Test/SqlDom/Baselines140/TrimFromReturnTests140.sql new file mode 100644 index 0000000..d2cdb10 --- /dev/null +++ b/Test/SqlDom/Baselines140/TrimFromReturnTests140.sql @@ -0,0 +1,7 @@ +CREATE FUNCTION dbo.TrimTest +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM('x' FROM @str); +END diff --git a/Test/SqlDom/Baselines160/TrimFromReturnTests160.sql b/Test/SqlDom/Baselines160/TrimFromReturnTests160.sql new file mode 100644 index 0000000..6a31f07 --- /dev/null +++ b/Test/SqlDom/Baselines160/TrimFromReturnTests160.sql @@ -0,0 +1,37 @@ +CREATE FUNCTION dbo.TrimUnicodeSpace +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM(NCHAR(12288) FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimLeadingX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( LEADING 'x' FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimTrailingX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( TRAILING 'x' FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimBothX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( BOTH 'x' FROM @str); +END diff --git a/Test/SqlDom/Baselines170/TrimFromReturnTests160.sql b/Test/SqlDom/Baselines170/TrimFromReturnTests160.sql new file mode 100644 index 0000000..6a31f07 --- /dev/null +++ b/Test/SqlDom/Baselines170/TrimFromReturnTests160.sql @@ -0,0 +1,37 @@ +CREATE FUNCTION dbo.TrimUnicodeSpace +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM(NCHAR(12288) FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimLeadingX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( LEADING 'x' FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimTrailingX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( TRAILING 'x' FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimBothX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( BOTH 'x' FROM @str); +END diff --git a/Test/SqlDom/Baselines180/TrimFromReturnTests160.sql b/Test/SqlDom/Baselines180/TrimFromReturnTests160.sql new file mode 100644 index 0000000..6a31f07 --- /dev/null +++ b/Test/SqlDom/Baselines180/TrimFromReturnTests160.sql @@ -0,0 +1,37 @@ +CREATE FUNCTION dbo.TrimUnicodeSpace +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM(NCHAR(12288) FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimLeadingX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( LEADING 'x' FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimTrailingX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( TRAILING 'x' FROM @str); +END + + +GO +CREATE FUNCTION dbo.TrimBothX +(@str NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN TRIM( BOTH 'x' FROM @str); +END diff --git a/Test/SqlDom/Only140SyntaxTests.cs b/Test/SqlDom/Only140SyntaxTests.cs index b24a14a..d782b3b 100644 --- a/Test/SqlDom/Only140SyntaxTests.cs +++ b/Test/SqlDom/Only140SyntaxTests.cs @@ -22,6 +22,7 @@ public partial class SqlDomTests new ParserTest140("OptimizerHintsTests140.sql", 6, 6, 6, 6, 6, 0), new ParserTest140("WithinGroupTests140.sql", 4, 4, 4, 2, 2, 0), new ParserTest140("TrimBuiltInTest140.sql", 10, 2, 10, 10, 10, 10), + new ParserTest140("TrimFromReturnTests140.sql"), new ParserTest140("AlterTableAlterColumnStatementTests140.sql", 14, 14, 14, 14, 14, 12), new ParserTest140("BulkInsertStatementTests140.sql", 12, 12, 12, 12, 12, 12), new ParserTest140("OpenRowsetBulkStatementTests140.sql", 9, 9, 9, 9, 9, 9), diff --git a/Test/SqlDom/Only160SyntaxTests.cs b/Test/SqlDom/Only160SyntaxTests.cs index 552152b..ad033d2 100644 --- a/Test/SqlDom/Only160SyntaxTests.cs +++ b/Test/SqlDom/Only160SyntaxTests.cs @@ -41,6 +41,7 @@ public partial class SqlDomTests new ParserTest160("AlterFunctionJsonObjectTests160.sql", nErrors80: 1, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 1, nErrors140: 1, nErrors150: 1), new ParserTest160("ComplexJsonObjectFunctionTests160.sql"), new ParserTest160("TestTrimReturn160.sql", nErrors80: 1, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), + new ParserTest160("TrimFromReturnTests160.sql"), new ParserTest160("TestJsonArrayReturn160.sql", nErrors80: 1, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), new ParserTest160(scriptFilename: "IgnoreRespectNullsSyntaxTests160.sql", nErrors80: 12, nErrors90: 8, nErrors100: 8, nErrors110: 8, nErrors120: 8, nErrors130: 8, nErrors140: 8, nErrors150: 8), new ParserTest160(scriptFilename: "FuzzyStringMatchingTests160.sql", nErrors80: 0, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs index aca9dee..3dbed24 100644 --- a/Test/SqlDom/Only170SyntaxTests.cs +++ b/Test/SqlDom/Only170SyntaxTests.cs @@ -24,6 +24,7 @@ public partial class SqlDomTests new ParserTest170("JsonArrayAggOrderBy170.sql", nErrors80: 10, nErrors90: 9, nErrors100: 9, nErrors110: 9, nErrors120: 9, nErrors130: 9, nErrors140: 9, nErrors150: 9, nErrors160: 9), new ParserTest170("JsonObjectAggOverClause170.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4), new ParserTest170("ComplexJsonObjectFunctionTests170.sql"), + new ParserTest170("TrimFromReturnTests160.sql"), new ParserTest170("AiGenerateEmbeddingsTests170.sql", nErrors80: 14, nErrors90: 11, nErrors100: 11, nErrors110: 11, nErrors120: 11, nErrors130: 11, nErrors140: 11, nErrors150: 11, nErrors160: 11), new ParserTest170("CreateExternalModelStatementTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4), new ParserTest170("AlterExternalModelStatementTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5), @@ -392,4 +393,4 @@ public void TSql100SyntaxIn170ParserTest() } } } -} \ No newline at end of file +} diff --git a/Test/SqlDom/Only180SyntaxTests.cs b/Test/SqlDom/Only180SyntaxTests.cs index 6fdad70..b68b9ba 100644 --- a/Test/SqlDom/Only180SyntaxTests.cs +++ b/Test/SqlDom/Only180SyntaxTests.cs @@ -12,7 +12,8 @@ public partial class SqlDomTests // Note: These filenames are case sensitive, make sure they match the checked-in file exactly private static readonly ParserTest[] Only180TestInfos = { - new ParserTest180("AutomaticIndexCompactionTests180.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 2, nErrors170: 2) + new ParserTest180("AutomaticIndexCompactionTests180.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 2, nErrors170: 2), + new ParserTest180("TrimFromReturnTests160.sql") }; private static readonly ParserTest[] SqlAzure180_TestInfos = diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs index 54e2322..5c88f7c 100644 --- a/Test/SqlDom/ParserErrorsTests.cs +++ b/Test/SqlDom/ParserErrorsTests.cs @@ -456,7 +456,7 @@ public void JsonObjectSyntaxNegativeTest() // Cannot use key:value pair with another non JSON_OBJECT Syntax ParserTestUtils.ErrorTest160("SELECT TRIM('name':'value','type':1)", - new ParserErrorInfo(18, "SQL46010", ":")); + new ParserErrorInfo(12, "SQL46010", "'name'")); // Cannot use Absent On Null with another non JSON_OBJECT Syntax ParserTestUtils.ErrorTest160("SELECT TRIM('name' ABSENT ON NULL)", diff --git a/Test/SqlDom/TestScripts/TrimFromReturnTests140.sql b/Test/SqlDom/TestScripts/TrimFromReturnTests140.sql new file mode 100644 index 0000000..8678e36 --- /dev/null +++ b/Test/SqlDom/TestScripts/TrimFromReturnTests140.sql @@ -0,0 +1,7 @@ +CREATE FUNCTION dbo.TrimTest(@str NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN TRIM('x' FROM @str); +END; +GO diff --git a/Test/SqlDom/TestScripts/TrimFromReturnTests160.sql b/Test/SqlDom/TestScripts/TrimFromReturnTests160.sql new file mode 100644 index 0000000..7a4c0b9 --- /dev/null +++ b/Test/SqlDom/TestScripts/TrimFromReturnTests160.sql @@ -0,0 +1,31 @@ +CREATE FUNCTION dbo.TrimUnicodeSpace(@str NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN TRIM(NCHAR(12288) FROM @str); +END; +GO + +CREATE FUNCTION dbo.TrimLeadingX(@str NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN TRIM(LEADING 'x' FROM @str); +END; +GO + +CREATE FUNCTION dbo.TrimTrailingX(@str NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN TRIM(TRAILING 'x' FROM @str); +END; +GO + +CREATE FUNCTION dbo.TrimBothX(@str NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN TRIM(BOTH 'x' FROM @str); +END; +GO