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()
};