diff --git a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs index 999408a0..d203be81 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs @@ -1889,5 +1889,123 @@ FROM OPENJSON ( @JSON ) AS root } } } + + [TestMethod] + public void RecursiveCTEJson() + { + using (var con = new Sql4CdsConnection(_localDataSource)) + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = @" +DECLARE @JSON NVARCHAR(MAX) = N'[ +{ +""OrderNumber"":""SO43659"", +""OrderDate"":""2011-05-31T00:00:00"", +""AccountNumber"":""AW29825"", +""ItemPrice"":2024.9940, +""ItemQuantity"":1 +}, +{ +""OrderNumber"":""SO43661"", +""OrderDate"":""2011-06-01T00:00:00"", +""AccountNumber"":""AW73565"", +""ItemPrice"":2024.9940, +""ItemQuantity"":3 +} +]'; + + +with cte ([key], value, type) as ( + select '$[' + [key] + ']', value, type from OPENJSON(@json) + + union all + + select cte.[key] + case when cte.type = 4 then '[' + childvalues.[key] + ']' else '.' + childvalues.[key] end, childvalues.value, childvalues.type from cte cross apply OPENJSON(cte.value) as childvalues WHERE cte.type in (4, 5) +) + +SELECT * from CTE"; + + using (var reader = cmd.ExecuteReader()) + { + Assert.AreEqual("key", reader.GetName(0)); + Assert.AreEqual("value", reader.GetName(1)); + Assert.AreEqual("type", reader.GetName(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[0]", reader.GetString(0)); + Assert.AreEqual(@"{ +""OrderNumber"":""SO43659"", +""OrderDate"":""2011-05-31T00:00:00"", +""AccountNumber"":""AW29825"", +""ItemPrice"":2024.9940, +""ItemQuantity"":1 +}", reader.GetString(1)); + Assert.AreEqual(5, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[1]", reader.GetString(0)); + Assert.AreEqual(@"{ +""OrderNumber"":""SO43661"", +""OrderDate"":""2011-06-01T00:00:00"", +""AccountNumber"":""AW73565"", +""ItemPrice"":2024.9940, +""ItemQuantity"":3 +}", reader.GetString(1)); + Assert.AreEqual(5, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[1].OrderNumber", reader.GetString(0)); + Assert.AreEqual("SO43661", reader.GetString(1)); + Assert.AreEqual(1, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[1].OrderDate", reader.GetString(0)); + Assert.AreEqual("2011-06-01T00:00:00", reader.GetString(1)); + Assert.AreEqual(1, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[1].AccountNumber", reader.GetString(0)); + Assert.AreEqual("AW73565", reader.GetString(1)); + Assert.AreEqual(1, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[1].ItemPrice", reader.GetString(0)); + Assert.AreEqual("2024.9940", reader.GetString(1)); + Assert.AreEqual(2, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[1].ItemQuantity", reader.GetString(0)); + Assert.AreEqual("3", reader.GetString(1)); + Assert.AreEqual(2, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[0].OrderNumber", reader.GetString(0)); + Assert.AreEqual("SO43659", reader.GetString(1)); + Assert.AreEqual(1, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[0].OrderDate", reader.GetString(0)); + Assert.AreEqual("2011-05-31T00:00:00", reader.GetString(1)); + Assert.AreEqual(1, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[0].AccountNumber", reader.GetString(0)); + Assert.AreEqual("AW29825", reader.GetString(1)); + Assert.AreEqual(1, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[0].ItemPrice", reader.GetString(0)); + Assert.AreEqual("2024.9940", reader.GetString(1)); + Assert.AreEqual(2, reader.GetInt32(2)); + + Assert.IsTrue(reader.Read()); + Assert.AreEqual("$[0].ItemQuantity", reader.GetString(0)); + Assert.AreEqual("1", reader.GetString(1)); + Assert.AreEqual(2, reader.GetInt32(2)); + + Assert.IsFalse(reader.Read()); + } + } + } } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs index 976e39f5..2737a2e9 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs @@ -27,6 +27,14 @@ class FilterNode : BaseDataNode, ISingleSourceExecutionPlanNode [Description("The filter to apply")] public BooleanExpression Filter { get; set; } + /// + /// Indicates if the filter should be evaluated during startup only + /// + [Category("Filter")] + [DisplayName("Startup Expression")] + [Description("Indicates if the filter shold be evaluated during startup only")] + public bool StartupExpression { get; set; } + /// /// The data source to select from /// @@ -40,6 +48,9 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont var filter = Filter.Compile(expressionCompilationContext); var expressionContext = new ExpressionExecutionContext(context); + if (StartupExpression && !filter(expressionContext)) + yield break; + foreach (var entity in Source.Execute(context)) { expressionContext.Entity = entity; @@ -164,9 +175,21 @@ public override IDataExecutionPlanNodeInternal FoldQuery(NodeCompilationContext if (FoldScalarSubqueries(context, out var nestedLoop)) return nestedLoop.FoldQuery(context, hints); + // Check if we can apply the filter during startup instead of per-record + StartupExpression = CheckStartupExpression(); + return this; } + private bool CheckStartupExpression() + { + // We only need to apply the filter expression to individual rows if it references any fields + if (Filter.GetColumns().Any()) + return false; + + return true; + } + private BooleanExpression FoldNotIsNullToIsNotNull(BooleanExpression filter) { var visitor = new RefactorNotIsNullVisitor(filter); @@ -1836,6 +1859,7 @@ public override object Clone() var clone = new FilterNode { Filter = Filter, + StartupExpression = StartupExpression, Source = (IDataExecutionPlanNodeInternal)Source.Clone() };