From 336a47b74b0f062255dcc20c38826da5264889a2 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Thu, 18 Jul 2024 08:02:53 +0100 Subject: [PATCH] Added string_split support Fixes #412 --- .../Images/StringSplitNode.ico | Bin 0 -> 4286 bytes .../MarkMpn.Sql4Cds.Controls.csproj | 3 + .../MarkMpn.Sql4Cds.Engine.Tests.csproj | 1 + .../StringSplitTests.cs | 249 ++++++++++++++++++ MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs | 35 +++ .../ExecutionPlan/StringSplitNode.cs | 205 ++++++++++++++ .../ExecutionPlanBuilder.cs | 60 +++-- .../Visitors/RewriteVisitorBase.cs | 16 ++ 8 files changed, 547 insertions(+), 22 deletions(-) create mode 100644 MarkMpn.Sql4Cds.Controls/Images/StringSplitNode.ico create mode 100644 MarkMpn.Sql4Cds.Engine.Tests/StringSplitTests.cs create mode 100644 MarkMpn.Sql4Cds.Engine/ExecutionPlan/StringSplitNode.cs diff --git a/MarkMpn.Sql4Cds.Controls/Images/StringSplitNode.ico b/MarkMpn.Sql4Cds.Controls/Images/StringSplitNode.ico new file mode 100644 index 0000000000000000000000000000000000000000..41ad4bcfe22c6b3354572134a901079bd26fcd9e GIT binary patch literal 4286 zcmeHKA(DeY5ZwF0sne17jH`G&9?t_%XYhGIJg4&jJRZ*j@K|npt2(vWs;)^0a3_+~ zrZYV|-LnG-oO|=%!@>E_!Tr2D_u>D2hRC@uk3Q>vo%{B=M?cdv&08+3pDn$ieXsxV zc%0Wrfj=K1J>! z&lo-QeIM#BmrLjYG5=#<*L62>>_Z)5s!yJ%@_C-mMNtHQcq6W=Y959m^oCqKBc-Ew z%fGJc-~&JI#T?KFG~2el^2jS6#Zyz|p^scVchtZi_Xq2VN=NZl9q&cV9D6YbTzBYU z_Lxu8G}k_qkK!$VoEzsvJ+3oWS(ag;H^!FEQh(_3yyx>dJQMJxUy8~{@vOJgG z>mJXW^+ctkc&m>2dr|o)p1M|^`z%yCil-j)Yaw-s)McC^^J}5fPx-xKo@+0^)Mqb$ y<)iqt&u3%hqxfh2(t71XelJvgz5LgG&rtJx&41hX+H}7EHO32YysY=pwe!EavEsJ? literal 0 HcmV?d00001 diff --git a/MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj b/MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj index b28f5128..0228aa86 100644 --- a/MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj +++ b/MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj @@ -170,6 +170,9 @@ MarkMpn.Sql4Cds.Engine + + + copy $(TargetDir)MarkMpn.Sql4Cds.Controls.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds diff --git a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj index 4f1f95e9..04f7900c 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj +++ b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj @@ -102,6 +102,7 @@ + diff --git a/MarkMpn.Sql4Cds.Engine.Tests/StringSplitTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/StringSplitTests.cs new file mode 100644 index 00000000..e533baa1 --- /dev/null +++ b/MarkMpn.Sql4Cds.Engine.Tests/StringSplitTests.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MarkMpn.Sql4Cds.Engine.Tests +{ + [TestClass] + public class StringSplitTests : FakeXrmEasyTestsBase + { + [TestMethod] + public void InsufficientParameters() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM string_split('hello,world')"; + + try + { + cmd.ExecuteNonQuery(); + Assert.Fail("Expected an exception"); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(313, ex.Number); + } + } + } + + [TestMethod] + public void TooManyParameters() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM string_split('hello,world', ',', 1, 'test')"; + + try + { + cmd.ExecuteNonQuery(); + Assert.Fail("Expected an exception"); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(8144, ex.Number); + } + } + } + + [TestMethod] + public void DefaultsToNoOrdinal() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM string_split('hello,world', ',')"; + + using (var reader = cmd.ExecuteReader()) + { + Assert.AreEqual(1, reader.FieldCount); + Assert.AreEqual("value", reader.GetName(0)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("hello", reader.GetString(0)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("world", reader.GetString(0)); + Assert.IsFalse(reader.Read()); + } + } + } + + [TestMethod] + public void IncludesOrdinal() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM string_split('hello,world', ',', 1)"; + + using (var reader = cmd.ExecuteReader()) + { + Assert.AreEqual(2, reader.FieldCount); + Assert.AreEqual("value", reader.GetName(0)); + Assert.AreEqual("ordinal", reader.GetName(1)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("hello", reader.GetString(0)); + Assert.AreEqual(1, reader.GetInt32(1)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("world", reader.GetString(0)); + Assert.AreEqual(2, reader.GetInt32(1)); + Assert.IsFalse(reader.Read()); + } + } + } + + [TestMethod] + public void InputAndSeparatorCanBeParameters() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM string_split(@input, @separator, 1)"; + cmd.Parameters.Add(cmd.CreateParameter()); + cmd.Parameters.Add(cmd.CreateParameter()); + + cmd.Parameters[0].ParameterName = "@input"; + cmd.Parameters[0].Value = "hello,world"; + cmd.Parameters[1].ParameterName = "@separator"; + cmd.Parameters[1].Value = ","; + + using (var reader = cmd.ExecuteReader()) + { + Assert.AreEqual(2, reader.FieldCount); + Assert.AreEqual("value", reader.GetName(0)); + Assert.AreEqual("ordinal", reader.GetName(1)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("hello", reader.GetString(0)); + Assert.AreEqual(1, reader.GetInt32(1)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("world", reader.GetString(0)); + Assert.AreEqual(2, reader.GetInt32(1)); + Assert.IsFalse(reader.Read()); + } + } + } + + [TestMethod] + public void OrdinalCannotBeParameter() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM string_split('hello,world', ',', @ordinal)"; + cmd.Parameters.Add(cmd.CreateParameter()); + cmd.Parameters[0].ParameterName = "@ordinal"; + cmd.Parameters[0].Value = true; + + try + { + cmd.ExecuteNonQuery(); + Assert.Fail("Expected an exception"); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(8748, ex.Number); + } + } + } + + [DataTestMethod] + [DataRow("123", 4199)] + [DataRow("'123'", 8116)] + [DataRow("1.0", 8116)] + public void OrdinalMustBeBit(string ordinal, int expectedError) + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = $"SELECT * FROM string_split('hello,world', ',', {ordinal})"; + + try + { + cmd.ExecuteNonQuery(); + Assert.Fail("Expected an exception"); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(expectedError, ex.Number); + } + } + } + + [TestMethod] + public void SeparatorCannotBeNull() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM string_split('hello,world', null)"; + + try + { + cmd.ExecuteNonQuery(); + Assert.Fail("Expected an exception"); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(214, ex.Number); + } + } + } + + [TestMethod] + public void NullInputGivesEmptyResult() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM string_split(null, ',')"; + + using (var reader = cmd.ExecuteReader()) + { + Assert.AreEqual(1, reader.FieldCount); + Assert.AreEqual("value", reader.GetName(0)); + Assert.IsFalse(reader.Read()); + } + } + } + + [TestMethod] + public void CrossApply() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @" +select * from (values ('a;b'), ('c;d')) as t1 (col) +cross apply string_split(t1.col, ';', 1) s"; + + using (var reader = cmd.ExecuteReader()) + { + Assert.AreEqual(3, reader.FieldCount); + Assert.AreEqual("col", reader.GetName(0)); + Assert.AreEqual("value", reader.GetName(1)); + Assert.AreEqual("ordinal", reader.GetName(2)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("a;b", reader.GetString(0)); + Assert.AreEqual("a", reader.GetString(1)); + Assert.AreEqual(1, reader.GetInt32(2)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("a;b", reader.GetString(0)); + Assert.AreEqual("b", reader.GetString(1)); + Assert.AreEqual(2, reader.GetInt32(2)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("c;d", reader.GetString(0)); + Assert.AreEqual("c", reader.GetString(1)); + Assert.AreEqual(1, reader.GetInt32(2)); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("c;d", reader.GetString(0)); + Assert.AreEqual("d", reader.GetString(1)); + Assert.AreEqual(2, reader.GetInt32(2)); + Assert.IsFalse(reader.Read()); + } + } + } + } +} diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs index 19e2c564..77fb26d0 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs @@ -193,6 +193,12 @@ internal static Sql4CdsError InvalidObjectName(SchemaObjectName obj) return Create(208, obj, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name)); } + internal static Sql4CdsError InvalidObjectName(Identifier obj) + { + var name = obj.ToSql(); + return Create(208, obj, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name)); + } + internal static Sql4CdsError NonFunctionCalledWithParameters(SchemaObjectName obj) { var name = obj.ToSql(); @@ -277,6 +283,12 @@ internal static Sql4CdsError InsufficientArguments(SchemaObjectName sproc) return Create(313, sproc, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name)); } + internal static Sql4CdsError InsufficientArguments(Identifier function) + { + var name = function.ToSql(); + return Create(313, function, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name)); + } + internal static Sql4CdsError TooManyArguments(SchemaObjectName sprocOrFunc, bool isSproc) { var name = sprocOrFunc.ToSql(); @@ -288,6 +300,14 @@ internal static Sql4CdsError TooManyArguments(SchemaObjectName sprocOrFunc, bool return err; } + internal static Sql4CdsError TooManyArguments(Identifier function) + { + var name = function.ToSql(); + var err = Create(8144, function, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name)); + + return err; + } + internal static Sql4CdsError NamedParametersRequiredAfter(ExecuteParameter param, int paramIndex) { return Create(119, param, (SqlInt32)paramIndex); @@ -322,6 +342,11 @@ internal static Sql4CdsError InvalidArgumentType(TSqlFragment fragment, DataType return Create(8116, fragment, Collation.USEnglish.ToSqlString(GetTypeName(type)), (SqlInt32)paramNum, Collation.USEnglish.ToSqlString(function)); } + internal static Sql4CdsError InvalidArgumentValue(TSqlFragment fragment, int value, int paramNum, string function) + { + return Create(4199, fragment, (SqlInt32)value, (SqlInt32)paramNum, Collation.USEnglish.ToSqlString(function)); + } + internal static Sql4CdsError StringTruncation(TSqlFragment fragment, string table, string column, string value) { return Create(2628, fragment, (SqlInt32)table.Length, Collation.USEnglish.ToSqlString(table), (SqlInt32)column.Length, Collation.USEnglish.ToSqlString(column), (SqlInt32)value.Length, Collation.USEnglish.ToSqlString(value)); @@ -745,6 +770,16 @@ internal static Sql4CdsError IncompatibleDataTypesForOperator(TSqlFragment fragm return Create(402, fragment, Collation.USEnglish.ToSqlString(GetTypeName(type1)), Collation.USEnglish.ToSqlString(GetTypeName(type2)), Collation.USEnglish.ToSqlString(op)); } + internal static Sql4CdsError StringSplitOrdinalRequiresLiteral(TSqlFragment fragment) + { + return Create(8748, fragment); + } + + internal static Sql4CdsError InvalidProcedureParameterType(TSqlFragment fragment, string parameter, string type) + { + return Create(214, fragment, parameter, type); + } + private static string GetTypeName(DataTypeReference type) { if (type is SqlDataTypeReference sqlType) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/StringSplitNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/StringSplitNode.cs new file mode 100644 index 00000000..0ebd5e94 --- /dev/null +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/StringSplitNode.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data.SqlTypes; +using Microsoft.SqlServer.TransactSql.ScriptDom; +using Microsoft.Xrm.Sdk; + +namespace MarkMpn.Sql4Cds.Engine.ExecutionPlan +{ + class StringSplitNode : BaseDataNode + { + private Func _inputExpression; + private Func _separatorExpression; + private Collation _inputCollation; + + private StringSplitNode() + { + } + + public StringSplitNode(GlobalFunctionTableReference function, NodeCompilationContext context) + { + if (function.Parameters.Count > 3) + throw new NotSupportedQueryFragmentException(Sql4CdsError.TooManyArguments(function.Name)); + + if (function.Parameters.Count < 2) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InsufficientArguments(function.Name)); + + Alias = function.Alias?.Value; + Input = function.Parameters[0].Clone(); + Separator = function.Parameters[1].Clone(); + + // Check expressions are string types and add conversions if not + var ecc = new ExpressionCompilationContext(context, null, null); + if (Input.GetType(ecc, out _) != typeof(SqlString)) + Input = new ConvertCall { Parameter = Input, DataType = DataTypeHelpers.NVarChar(Int32.MaxValue, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault) }; + if (Separator != null && Separator.GetType(ecc, out _) != typeof(SqlString)) + Separator = new ConvertCall { Parameter = Separator, DataType = DataTypeHelpers.NVarChar(1, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault) }; + + // If an ordinal is specified, it must be a constant + if (function.Parameters.Count == 3) + { + if (!(function.Parameters[2] is Literal ordinalLiteral)) + throw new NotSupportedQueryFragmentException(Sql4CdsError.StringSplitOrdinalRequiresLiteral(function.Parameters[2])); + + if (ordinalLiteral.LiteralType != LiteralType.Integer) + { + ordinalLiteral.GetType(ecc, out var ordinalType); + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidArgumentType(ordinalLiteral, ordinalType, 3, "string_split")); + } + + var ordinalValue = Int32.Parse(ordinalLiteral.Value); + if (ordinalValue != 0 && ordinalValue != 1) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidArgumentValue(ordinalLiteral, ordinalValue, 3, "string_split")); + + EnableOrdinal = ordinalValue == 1; + } + } + + /// + /// The alias for the data source + /// + [Category("String Split")] + [Description("The alias for the data source")] + public string Alias { get; set; } + + /// + /// The expression that provides the string to split + /// + [Category("String Split")] + [Description("The expression that provides the string to split")] + public ScalarExpression Input { get; set; } + + /// + /// The expression that defines the separator to split on + /// + [Category("String Split")] + [Description("The expression that defines the separator to split on")] + public ScalarExpression Separator { get; set; } + + /// + /// Indicates whether the output should contain an ordinal column + /// + [Category("String Split")] + [Description("Indicates whether the output should contain an ordinal column")] + public bool EnableOrdinal { get; set; } + + public override void AddRequiredColumns(NodeCompilationContext context, IList requiredColumns) + { + } + + public override IDataExecutionPlanNodeInternal FoldQuery(NodeCompilationContext context, IList hints) + { + var ecc = new ExpressionCompilationContext(context, null, null); + _inputExpression = Input.Compile(ecc); + _separatorExpression = Separator?.Compile(ecc); + + return this; + } + + public override INodeSchema GetSchema(NodeCompilationContext context) + { + var columns = new ColumnList(); + var aliases = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + if (_inputCollation == null) + { + var ecc = new ExpressionCompilationContext(context, null, null); + Input.GetType(ecc, out var inputType); + _inputCollation = (inputType as SqlDataTypeReferenceWithCollation)?.Collation ?? context.PrimaryDataSource.DefaultCollation; + } + + columns.Add(PrefixWithAlias("value", aliases), new ColumnDefinition(DataTypeHelpers.NVarChar(Int32.MaxValue, _inputCollation, CollationLabel.Implicit), false, false)); + + if (EnableOrdinal) + columns.Add(PrefixWithAlias("ordinal", aliases), new ColumnDefinition(DataTypeHelpers.Int, false, false)); + + var schema = new NodeSchema( + columns, + aliases, + null, + null + ); + + return schema; + } + + private string PrefixWithAlias(string name, IDictionary> aliases) + { + name = name.EscapeIdentifier(); + + var fullName = Alias == null ? name : (Alias.EscapeIdentifier() + "." + name); + + if (aliases != null) + { + if (!aliases.TryGetValue(name, out var alias)) + { + alias = new List(); + aliases[name] = alias; + } + + ((List)alias).Add(fullName); + } + + return fullName; + } + + public override IEnumerable GetSources() + { + return Array.Empty(); + } + + protected override RowCountEstimate EstimateRowsOutInternal(NodeCompilationContext context) + { + return new RowCountEstimate(10); + } + + protected override IEnumerable ExecuteInternal(NodeExecutionContext context) + { + var eec = new ExpressionExecutionContext(context); + + var input = (SqlString) _inputExpression(eec); + var separator = (SqlString) _separatorExpression(eec); + + if (separator.IsNull || separator.Value.Length != 1) + throw new QueryExecutionException(Sql4CdsError.InvalidProcedureParameterType(Separator, "separator", "nchar(1)/nvarchar(1)")); + + if (input.IsNull || input.Value.Length == 0) + yield break; + + var parts = input.Value.Split(separator.Value[0]); + var ordinal = 1; + + foreach (var part in parts) + { + var entity = new Entity + { + [PrefixWithAlias("value", null)] = _inputCollation.ToSqlString(part), + [PrefixWithAlias("ordinal", null)] = (SqlInt32)ordinal + }; + + yield return entity; + ordinal++; + } + } + + public override object Clone() + { + return new StringSplitNode + { + Alias = Alias, + Input = Input.Clone(), + Separator = Separator.Clone(), + EnableOrdinal = EnableOrdinal, + _inputExpression = _inputExpression, + _separatorExpression = _separatorExpression, + _inputCollation = _inputCollation + }; + } + + public override string ToString() + { + return "Table Valued Function\r\n[STRING_SPLIT]"; + } + } +} diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index c94b428c..75b0ae45 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -4873,18 +4873,23 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe }; } - if (reference is SchemaObjectFunctionTableReference tvf) + var tvf = reference as SchemaObjectFunctionTableReference; + var gf = reference as GlobalFunctionTableReference; + + if (tvf != null || gf != null) { + var parameters = tvf?.Parameters ?? gf.Parameters; + // Capture any references to data from an outer query - CaptureOuterReferences(outerSchema, null, tvf, context, outerReferences); + CaptureOuterReferences(outerSchema, null, reference, context, outerReferences); // Convert any scalar subqueries in the parameters to its own execution plan, and capture the references from those plans // as parameters to be passed to the function IDataExecutionPlanNodeInternal source = new ConstantScanNode { Values = { new Dictionary() } }; var computeScalar = new ComputeScalarNode { Source = source }; - foreach (var param in tvf.Parameters.ToList()) - ConvertScalarSubqueries(param, hints, ref source, computeScalar, context, tvf); + foreach (var param in parameters.ToList()) + ConvertScalarSubqueries(param, hints, ref source, computeScalar, context, reference); if (source is ConstantScanNode) source = null; @@ -4893,34 +4898,45 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe var scalarSubquerySchema = source?.GetSchema(context); var scalarSubqueryReferences = new Dictionary(); - CaptureOuterReferences(scalarSubquerySchema, null, tvf, context, scalarSubqueryReferences); + CaptureOuterReferences(scalarSubquerySchema, null, reference, context, scalarSubqueryReferences); - var dataSource = SelectDataSource(tvf.SchemaObject); IDataExecutionPlanNodeInternal execute; - if (String.IsNullOrEmpty(tvf.SchemaObject.SchemaIdentifier?.Value) || - tvf.SchemaObject.SchemaIdentifier.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase)) + if (tvf != null) { - execute = ExecuteMessageNode.FromMessage(tvf, dataSource, GetExpressionContext(null, context)); - } - else if (tvf.SchemaObject.SchemaIdentifier.Value.Equals("sys", StringComparison.OrdinalIgnoreCase)) - { - if (!Enum.TryParse(tvf.SchemaObject.BaseIdentifier.Value, true, out var systemFunction)) - throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(tvf.SchemaObject)); + var dataSource = SelectDataSource(tvf.SchemaObject); - if (typeof(SystemFunction).GetField(systemFunction.ToString()).GetCustomAttribute().Type != SystemObjectType.Function) - throw new NotSupportedQueryFragmentException(Sql4CdsError.NonFunctionCalledWithParameters(tvf.SchemaObject)); + if (String.IsNullOrEmpty(tvf.SchemaObject.SchemaIdentifier?.Value) || + tvf.SchemaObject.SchemaIdentifier.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase)) + { + execute = ExecuteMessageNode.FromMessage(tvf, dataSource, GetExpressionContext(null, context)); + } + else if (tvf.SchemaObject.SchemaIdentifier.Value.Equals("sys", StringComparison.OrdinalIgnoreCase)) + { + if (!Enum.TryParse(tvf.SchemaObject.BaseIdentifier.Value, true, out var systemFunction)) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(tvf.SchemaObject)); - execute = new SystemFunctionNode + if (typeof(SystemFunction).GetField(systemFunction.ToString()).GetCustomAttribute().Type != SystemObjectType.Function) + throw new NotSupportedQueryFragmentException(Sql4CdsError.NonFunctionCalledWithParameters(tvf.SchemaObject)); + + execute = new SystemFunctionNode + { + DataSource = dataSource.Name, + Alias = tvf.Alias?.Value, + SystemFunction = systemFunction + }; + } + else { - DataSource = dataSource.Name, - Alias = tvf.Alias?.Value, - SystemFunction = systemFunction - }; + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(tvf.SchemaObject)); + } } else { - throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(tvf.SchemaObject)); + if (gf.Name.Value.Equals("string_split", StringComparison.OrdinalIgnoreCase)) + execute = new StringSplitNode(gf, context); + else + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(gf.Name)); } if (source == null) diff --git a/MarkMpn.Sql4Cds.Engine/Visitors/RewriteVisitorBase.cs b/MarkMpn.Sql4Cds.Engine/Visitors/RewriteVisitorBase.cs index c0259916..1c0d74bb 100644 --- a/MarkMpn.Sql4Cds.Engine/Visitors/RewriteVisitorBase.cs +++ b/MarkMpn.Sql4Cds.Engine/Visitors/RewriteVisitorBase.cs @@ -228,6 +228,22 @@ public override void ExplicitVisit(SchemaObjectFunctionTableReference node) } } + public override void ExplicitVisit(GlobalFunctionTableReference node) + { + base.ExplicitVisit(node); + + for (var i = 0; i < node.Parameters.Count; i++) + { + var replaced = ReplaceExpression(node.Parameters[i], out _); + + if (node.Parameters[i] != replaced) + { + node.Parameters[i] = replaced; + node.ScriptTokenStream = null; + } + } + } + public override void ExplicitVisit(OpenJsonTableReference node) { base.ExplicitVisit(node);