Skip to content

Commit

Permalink
Added filter StartupExpression for more efficient filtering of CROSS …
Browse files Browse the repository at this point in the history
…APPLY queries
  • Loading branch information
MarkMpn committed Nov 15, 2023
1 parent 2535ab6 commit 0056a42
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 0 deletions.
118 changes: 118 additions & 0 deletions MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
}
}
24 changes: 24 additions & 0 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ class FilterNode : BaseDataNode, ISingleSourceExecutionPlanNode
[Description("The filter to apply")]
public BooleanExpression Filter { get; set; }

/// <summary>
/// Indicates if the filter should be evaluated during startup only
/// </summary>
[Category("Filter")]
[DisplayName("Startup Expression")]
[Description("Indicates if the filter shold be evaluated during startup only")]
public bool StartupExpression { get; set; }

/// <summary>
/// The data source to select from
/// </summary>
Expand All @@ -40,6 +48,9 @@ protected override IEnumerable<Entity> 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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1836,6 +1859,7 @@ public override object Clone()
var clone = new FilterNode
{
Filter = Filter,
StartupExpression = StartupExpression,
Source = (IDataExecutionPlanNodeInternal)Source.Clone()
};

Expand Down

0 comments on commit 0056a42

Please sign in to comment.