Skip to content

Commit

Permalink
Cross apply progress
Browse files Browse the repository at this point in the history
Fixes #330
  • Loading branch information
MarkMpn committed Aug 13, 2023
1 parent 9c2c9e3 commit e523e13
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 20 deletions.
128 changes: 128 additions & 0 deletions MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2113,6 +2113,134 @@ FROM contact
</fetch>");
}

[TestMethod]
public void CrossApplyAllColumns()
{
var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this);

var query = @"
SELECT
name,
a.*
FROM
account
CROSS APPLY
(
SELECT *
FROM contact
WHERE primarycontactid = contactid
) a";

var plans = planBuilder.Build(query, null, out _);

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
Assert.AreEqual("account.name", select.ColumnSet[0].SourceColumn);
Assert.AreEqual("a.contactid", select.ColumnSet[1].SourceColumn);
Assert.AreEqual("a.createdon", select.ColumnSet[2].SourceColumn);
Assert.AreEqual("a.firstname", select.ColumnSet[3].SourceColumn);
Assert.AreEqual("a.fullname", select.ColumnSet[4].SourceColumn);
Assert.AreEqual("a.lastname", select.ColumnSet[5].SourceColumn);
Assert.AreEqual("a.parentcustomerid", select.ColumnSet[6].SourceColumn);
Assert.AreEqual("a.parentcustomeridname", select.ColumnSet[7].SourceColumn);
Assert.AreEqual("a.parentcustomeridtype", select.ColumnSet[8].SourceColumn);
var fetch = AssertNode<FetchXmlScan>(select.Source);
Assert.IsNull(fetch.ColumnMappings[0].SourceColumn);
Assert.IsTrue(fetch.ColumnMappings[0].AllColumns);
Assert.AreEqual("a", fetch.ColumnMappings[0].OutputColumn);
AssertFetchXml(fetch, @"
<fetch>
<entity name='account'>
<attribute name='name' />
<link-entity name='contact' alias='a' from='contactid' to='primarycontactid' link-type='inner'>
<all-attributes />
</link-entity>
</entity>
</fetch>");
}

[TestMethod]
public void CrossApplyRestrictedColumns()
{
var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this);

var query = @"
SELECT
name,
a.*
FROM
account
CROSS APPLY
(
SELECT firstname,
lastname
FROM contact
WHERE primarycontactid = contactid
) a";

var plans = planBuilder.Build(query, null, out _);

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
var fetch = AssertNode<FetchXmlScan>(select.Source);
AssertFetchXml(fetch, @"
<fetch>
<entity name='account'>
<attribute name='name' />
<link-entity name='contact' alias='a' from='contactid' to='primarycontactid' link-type='inner'>
<attribute name='firstname' />
<attribute name='lastname' />
</link-entity>
</entity>
</fetch>");
}

[TestMethod]
public void CrossApplyRestrictedColumnsWithAlias()
{
var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this);

var query = @"
SELECT
name,
a.*
FROM
account
CROSS APPLY
(
SELECT firstname AS fname,
lastname AS lname
FROM contact
WHERE primarycontactid = contactid
) a";

var plans = planBuilder.Build(query, null, out _);

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
Assert.AreEqual("account.name", select.ColumnSet[0].SourceColumn);
Assert.AreEqual("a.fname", select.ColumnSet[1].SourceColumn);
Assert.AreEqual("a.lname", select.ColumnSet[2].SourceColumn);
var fetch = AssertNode<FetchXmlScan>(select.Source);
Assert.AreEqual("a.fname", fetch.ColumnMappings[0].SourceColumn);
Assert.AreEqual("a.fname", fetch.ColumnMappings[0].OutputColumn);
Assert.AreEqual("a.lname", fetch.ColumnMappings[1].SourceColumn);
Assert.AreEqual("a.lname", fetch.ColumnMappings[1].OutputColumn);
AssertFetchXml(fetch, @"
<fetch>
<entity name='account'>
<attribute name='name' />
<link-entity name='contact' alias='a' from='contactid' to='primarycontactid' link-type='inner'>
<attribute name='firstname' alias='fname' />
<attribute name='lastname' alias='lname' />
</link-entity>
</entity>
</fetch>");
}

[TestMethod]
public void OuterApply()
{
Expand Down
33 changes: 27 additions & 6 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/AliasNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,8 @@ public override IDataExecutionPlanNodeInternal FoldQuery(NodeCompilationContext

if (Source is FetchXmlScan fetchXml)
{
// Check if all the source and output column names match. If so, just change the alias of the source FetchXML
if (ColumnSet.All(col => col.SourceColumn == $"{fetchXml.Alias}.{col.OutputColumn}"))
{
fetchXml.Alias = Alias;
return fetchXml;
}
FoldToFetchXML(fetchXml);
return fetchXml;
}

if (Source is ConstantScanNode constant)
Expand Down Expand Up @@ -158,6 +154,31 @@ public override IDataExecutionPlanNodeInternal FoldQuery(NodeCompilationContext
return this;
}

internal void FoldToFetchXML(FetchXmlScan fetchXml)
{
// Add the mappings to the FetchXML to produce the columns with the expected names, and hide all other possible columns
var originalAlias = fetchXml.Alias;
fetchXml.Alias = Alias;

foreach (var col in ColumnSet)
{
if (col.SourceColumn != null && col.SourceColumn.StartsWith(originalAlias + "."))
col.SourceColumn = Alias + col.SourceColumn.Substring(originalAlias.Length);

if (col.AllColumns)
col.OutputColumn = Alias;
else if (col.OutputColumn != null)
col.OutputColumn = Alias + "." + col.OutputColumn;

fetchXml.ColumnMappings.Add(col);
}

fetchXml.HiddenAliases.Add(Alias);

foreach (var link in fetchXml.Entity.GetLinkEntities())
fetchXml.HiddenAliases.Add(link.alias);
}

public override INodeSchema GetSchema(NodeCompilationContext context)
{
// Map the base names to the alias names
Expand Down
2 changes: 1 addition & 1 deletion MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseJoinNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ protected virtual INodeSchema GetSchema(NodeCompilationContext context, bool inc
nullable = false;
}

schema[column.Key] = new ColumnDefinition(column.Value.Type, nullable, column.Value.IsCalculated);
schema[column.Key] = new ColumnDefinition(column.Value.Type, nullable, column.Value.IsCalculated, column.Value.IsVisible);
}

foreach (var alias in subSchema.Aliases)
Expand Down
107 changes: 103 additions & 4 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -103,6 +105,9 @@ public InvalidPagingException(string message) : base(message)
private string _lastSchemaAlias;
private NodeSchema _lastSchema;
private bool _lastFullSchema;
private string _lastHiddenAliases;
private string _lastColumnMappings;
private bool _lastBuildingSelectClause;
private bool _resetPage;
private string _startingPage;
private List<string> _pagingFields;
Expand Down Expand Up @@ -192,6 +197,16 @@ public FetchXmlScan()
[Description("Indicates if custom plugins should be skipped")]
public bool BypassCustomPluginExecution { get; set; }

/// <summary>
/// A list of table aliases that should be excluded from the schema
/// </summary>
public List<string> HiddenAliases { get; } = new List<string>();

/// <summary>
/// A list of additional columns that should be included in the schema
/// </summary>
public List<SelectColumn> ColumnMappings { get; } = new List<SelectColumn>();

public bool RequiresCustomPaging(IDictionary<string, DataSource> dataSources)
{
if (FetchXml.distinct)
Expand Down Expand Up @@ -546,6 +561,20 @@ private void OnRetrievedEntity(Entity entity, INodeSchema schema, IQueryExecutio
entity[col.Key] = sqlValue;
}

// Apply renamings
foreach (var mapping in ColumnMappings)
{
if (mapping.AllColumns)
{
foreach (var col in entity.Attributes.Where(c => mapping.SourceColumn == null || c.Key.StartsWith(mapping.SourceColumn.Replace(".*", "") + ".")).ToList())
entity[mapping.OutputColumn.Replace(".*", "") + "." + col.Key.Split('.').Last()] = col.Value;
}
else
{
entity[mapping.OutputColumn] = entity[mapping.SourceColumn];
}
}

if (_pagingFields != null)
{
_lastPageValues.Clear();
Expand Down Expand Up @@ -707,7 +736,15 @@ public override INodeSchema GetSchema(NodeCompilationContext context)
throw new NotSupportedQueryFragmentException("Missing datasource " + DataSource);

var fetchXmlString = FetchXmlString;
if (_lastSchema != null && Alias == _lastSchemaAlias && fetchXmlString == _lastSchemaFetchXml && ReturnFullSchema == _lastFullSchema)
var hiddenAliases = String.Join(",", HiddenAliases);
var columnMappings = String.Join(",", ColumnMappings.Select(map => map.OutputColumn + "=" + map.SourceColumn));

if (_lastSchema != null &&
Alias == _lastSchemaAlias &&
fetchXmlString == _lastSchemaFetchXml &&
ReturnFullSchema == _lastFullSchema &&
hiddenAliases == _lastHiddenAliases &&
columnMappings == _lastColumnMappings)
return _lastSchema;

_primaryKeyColumns = new Dictionary<string, string>();
Expand All @@ -723,19 +760,57 @@ public override INodeSchema GetSchema(NodeCompilationContext context)

AddSchemaAttributes(context, dataSource, schema, aliases, ref primaryKey, sortOrder, entity.name, Alias, entity.Items, true, false);

foreach (var mapping in ColumnMappings)
{
if (mapping.AllColumns)
{
foreach (var col in schema.Where(c => mapping.SourceColumn == null || c.Key.StartsWith(mapping.SourceColumn.Replace(".*", "") + ".")).ToList())
MapColumn(col.Key, mapping.OutputColumn.Replace(".*", "") + "." + col.Key.Split('.').Last(), schema, aliases, sortOrder);
}
else
{
MapColumn(mapping.SourceColumn, mapping.OutputColumn, schema, aliases, sortOrder);
}
}

_lastSchema = new NodeSchema(
primaryKey: primaryKey,
schema: schema,
aliases: aliases,
sortOrder: sortOrder
); ;
);
_lastSchemaFetchXml = fetchXmlString;
_lastSchemaAlias = Alias;
_lastFullSchema = ReturnFullSchema;
_lastHiddenAliases = hiddenAliases;
_lastColumnMappings = columnMappings;

return _lastSchema;
}

private void MapColumn(string sourceColumn, string outputColumn, ColumnList schema, Dictionary<string, IReadOnlyList<string>> aliases, List<string> sortOrder)
{
var src = schema[sourceColumn];
schema[outputColumn] = new ColumnDefinition(src.Type, src.IsNullable, src.IsCalculated);

var simpleName = outputColumn.Split('.').Last();

if (!aliases.TryGetValue(simpleName, out var aliasList))
{
aliasList = new List<string>();
aliases[simpleName] = aliasList;
}

if (!aliasList.Contains(outputColumn))
((List<string>)aliasList).Add(outputColumn);

for (var i = 0; i < sortOrder.Count; i++)
{
if (sortOrder[i] == sourceColumn)
sortOrder[i] = outputColumn;
}
}

internal void ResetSchemaCache()
{
_lastSchema = null;
Expand Down Expand Up @@ -1058,7 +1133,12 @@ private void AddSchemaAttribute(DataSource dataSource, ColumnList schema, Dictio

private void AddSchemaAttribute(ColumnList schema, Dictionary<string, IReadOnlyList<string>> aliases, string fullName, string simpleName, DataTypeReference type, bool notNull)
{
schema[fullName] = new ColumnDefinition(type, !notNull, false);
var parts = fullName.Split('.');
var visible = true;
if (parts.Length == 2 && HiddenAliases.Contains(parts[0]))
visible = false;

schema[fullName] = new ColumnDefinition(type, !notNull, false, visible);

if (simpleName == null)
return;
Expand Down Expand Up @@ -1537,7 +1617,20 @@ public override void AddRequiredColumns(NodeCompilationContext context, IList<st
if (parts.Length != 2)
continue;

AddAttribute(normalizedCol, null, dataSource.Metadata, out _, out _);
var mapping = ColumnMappings.SingleOrDefault(map => map.OutputColumn == normalizedCol);

if (mapping != null)
normalizedCol = mapping.SourceColumn;

var attr = AddAttribute(normalizedCol, null, dataSource.Metadata, out _, out var linkEntity);

if (mapping != null)
{
if (attr.name != parts[1])
attr.alias = parts[1];

mapping.SourceColumn = (linkEntity?.alias ?? Alias) + "." + (attr.alias ?? attr.name);
}
}

// If there is no attribute requested the server will return everything instead of nothing, so
Expand Down Expand Up @@ -1911,6 +2004,12 @@ public override object Clone()
cloneLink.RequireTablePrefix = link.RequireTablePrefix;
}

foreach (var alias in HiddenAliases)
clone.HiddenAliases.Add(alias);

foreach (var mapping in ColumnMappings)
clone.ColumnMappings.Add(mapping);

return clone;
}
}
Expand Down
7 changes: 7 additions & 0 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/FoldableJoinNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,14 @@ private bool FoldFetchXmlJoin(NodeCompilationContext context, IList<OptimizerHin
return true;
}

foreach (var alias in rightFetch.HiddenAliases)
leftFetch.HiddenAliases.Add(alias);

foreach (var mapping in rightFetch.ColumnMappings)
leftFetch.ColumnMappings.Add(mapping);

folded = leftFetch;

return true;
}

Expand Down
Loading

0 comments on commit e523e13

Please sign in to comment.