diff --git a/AzureDataStudioExtension/CHANGELOG.md b/AzureDataStudioExtension/CHANGELOG.md
index 903b2dfa..cebc68cb 100644
--- a/AzureDataStudioExtension/CHANGELOG.md
+++ b/AzureDataStudioExtension/CHANGELOG.md
@@ -1,5 +1,23 @@
# Change Log
+## [v9.1.0](https://github.com/MarkMpn/Sql4Cds/releases/tag/v9.1.0) - 2024-06-10
+
+Enabled access to recycle bin records via the `bin` schema
+Enabled `INSERT`, `UPDATE` and `DELETE` statements on `principalobjectaccess` table
+Enabled use of subqueries within `ON` clause of `JOIN` statements
+Added support for `___pid` virtual column for lookups to elastic tables
+Improved folding of queries using index spools
+Improved primary key calculation when using joins on non-key columns
+Apply column order setting to parameters for stored procedures and table-valued functions
+Fixed error with DeleteMultiple requests
+Fixed paging error with `DISTINCT` queries causing results to be limited to 50,000 records
+Fixed paging errors when sorting by optionset values causing some results to be skipped
+Fixed errors when using joins inside `[NOT] EXISTS` subqueries
+Fixed incorrect results when applying aliases to `___name` and `___type` virtual columns
+Fixed max length calculation for string columns
+Fixed display of error messages
+Fixed "invalid program" errors when combining type conversions with `AND` or `OR`
+
## [v9.0.1](https://github.com/MarkMpn/Sql4Cds/releases/tag/v9.0.1) - 2024-05-08
Fixed `NullReferenceException` errors when:
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/ExecutionPlanObjectSource.cs b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/ExecutionPlanObjectSource.cs
new file mode 100644
index 00000000..8b7c6f37
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/ExecutionPlanObjectSource.cs
@@ -0,0 +1,35 @@
+using System.Text;
+using MarkMpn.Sql4Cds.Engine;
+using MarkMpn.Sql4Cds.Engine.ExecutionPlan;
+using Microsoft.VisualStudio.DebuggerVisualizers;
+
+namespace MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide
+{
+ public class ExecutionPlanObjectSource : VisualizerObjectSource
+ {
+ public override void GetData(object target, Stream outgoingData)
+ {
+ if (target is IRootExecutionPlanNode root)
+ {
+ WritePlan(outgoingData, root);
+ }
+ else if (target is IExecutionPlanNode node)
+ {
+ WritePlan(outgoingData, new UnknownRootNode { Source = node });
+ }
+ else if (target is Sql4CdsCommand cmd)
+ {
+ if (cmd.Plan != null)
+ WritePlan(outgoingData, cmd.Plan.First());
+ else
+ WritePlan(outgoingData, cmd.GeneratePlan(false).First());
+ }
+ }
+
+ private void WritePlan(Stream outgoingData, IRootExecutionPlanNode source)
+ {
+ var json = ExecutionPlanSerializer.Serialize(source);
+ SerializeAsJson(outgoingData, new SerializedPlan { Plan = json });
+ }
+ }
+}
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.csproj b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.csproj
new file mode 100644
index 00000000..77abd04b
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net6.0;net462
+ enable
+ enable
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/SerializedPlan.cs b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/SerializedPlan.cs
new file mode 100644
index 00000000..19024ed2
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/SerializedPlan.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide
+{
+ public class SerializedPlan
+ {
+ public string Plan { get; set; }
+ }
+}
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/UnknownRootNode.cs b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/UnknownRootNode.cs
new file mode 100644
index 00000000..7cc59105
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/UnknownRootNode.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MarkMpn.Sql4Cds.Engine.ExecutionPlan;
+
+namespace MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide
+{
+ internal class UnknownRootNode : IRootExecutionPlanNode
+ {
+ public string Sql { get; set; }
+ public int Index { get; set; }
+ public int Length { get; set; }
+ public int LineNumber { get; set; }
+
+ public IExecutionPlanNode Parent => null;
+
+ public int ExecutionCount => 0;
+
+ public TimeSpan Duration => TimeSpan.Zero;
+
+ public IExecutionPlanNode Source { get; set; }
+
+ public IEnumerable GetSources()
+ {
+ yield return Source;
+ }
+
+ public override string ToString()
+ {
+ return "< Unknown Root >";
+ }
+ }
+}
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/.vsextension/string-resources.json b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/.vsextension/string-resources.json
new file mode 100644
index 00000000..6fb565e4
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/.vsextension/string-resources.json
@@ -0,0 +1,3 @@
+{
+ "MarkMpn.Sql4Cds.DebugVisualizer.DisplayName": "SQL 4 CDS Visualizer"
+}
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/ExecutionPlanDebuggerVisualizerProvider.cs b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/ExecutionPlanDebuggerVisualizerProvider.cs
new file mode 100644
index 00000000..d9e9a85e
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/ExecutionPlanDebuggerVisualizerProvider.cs
@@ -0,0 +1,87 @@
+using System.Diagnostics;
+using MarkMpn.Sql4Cds.Engine;
+using Microsoft;
+using Microsoft.VisualStudio.Extensibility;
+using Microsoft.VisualStudio.Extensibility.Commands;
+using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
+using Microsoft.VisualStudio.Extensibility.Shell;
+using Microsoft.VisualStudio.Extensibility.VSSdkCompatibility;
+using Microsoft.VisualStudio.RpcContracts.RemoteUI;
+using Microsoft.VisualStudio.Shell;
+
+namespace MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide
+{
+ ///
+ /// Debugger visualizer provider for .
+ ///
+ [VisualStudioContribution]
+ internal class ExecutionPlanDebuggerVisualizerProvider : DebuggerVisualizerProvider
+ {
+ private const string DisplayName = "MarkMpn.Sql4Cds.DebugVisualizer.DisplayName";
+
+ public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new(
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.AdaptiveIndexSpoolNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.AliasNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.AssertNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.AssignVariablesNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.BulkDeleteJobNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.ComputeScalarNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.ConcatenateNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.ConditionalNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.ConstantScanNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.ContinueBreakNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.DeclareVariablesNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.DeleteNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.DistinctNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.ExecuteAsNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.ExecuteMesageNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.FetchXmlScan, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.FilterNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.GlobalOptionSetQueryNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.GotoLabelNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.GoToNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.HashJoinNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.HashMatchAggregateNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.IndexSpoolNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.InsertNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.MergeJoinNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.MetadataQueryNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.NestedLoopNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.OffsetFetchNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.OpenJsonNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.PartitionedAggregateNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.PrintNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.RaiseErrorNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.RetrieveTotalRecordCountNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.RevertNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.SelectNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.SortNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.SqlNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.StreamAggregateNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.SystemFunctionNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.TableSpoolNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.ThrowNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.TopNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.BeginTryNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.EndTryNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.BeginCatchNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.EndCatchNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.UpdateNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.WaitForNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.ExecutionPlan.XmlWriterNode, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"),
+ new VisualizerTargetType($"%{DisplayName}%", "MarkMpn.Sql4Cds.Engine.Sql4CdsCommand, MarkMpn.Sql4Cds.Engine, Version=0.0.0.0, Culture=neutral"))
+ {
+ VisualizerObjectSourceType = new("MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.ExecutionPlanObjectSource, MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide")
+ };
+
+ public override async Task CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
+ {
+ var serializedPlan = await visualizerTarget.ObjectSource.RequestDataAsync(null, cancellationToken);
+ var plan = ExecutionPlanSerializer.Deserialize(serializedPlan.Plan);
+
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ var wrapper = new WpfControlWrapper(new QueryPlanUserControl(plan));
+ return wrapper;
+ }
+ }
+}
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/ExtensionEntrypoint.cs b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/ExtensionEntrypoint.cs
new file mode 100644
index 00000000..614ca611
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/ExtensionEntrypoint.cs
@@ -0,0 +1,26 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.VisualStudio.Extensibility;
+
+namespace MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide
+{
+ ///
+ /// Extension entrypoint for the VisualStudio.Extensibility extension.
+ ///
+ [VisualStudioContribution]
+ internal class ExtensionEntrypoint : Extension
+ {
+ ///
+ public override ExtensionConfiguration ExtensionConfiguration => new()
+ {
+ RequiresInProcessHosting = true
+ };
+
+ ///
+ protected override void InitializeServices(IServiceCollection serviceCollection)
+ {
+ base.InitializeServices(serviceCollection);
+
+ // You can configure dependency injection here by adding services to the serviceCollection.
+ }
+ }
+}
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/Images/IconSmall.png b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/Images/IconSmall.png
new file mode 100644
index 00000000..62e059bc
Binary files /dev/null and b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/Images/IconSmall.png differ
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj
new file mode 100644
index 00000000..decc789f
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj
@@ -0,0 +1,48 @@
+
+
+ net472
+ enable
+ enable
+ latest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+
+
+
+ runtime
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/QueryPlanUserControl.xaml b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/QueryPlanUserControl.xaml
new file mode 100644
index 00000000..ac13c802
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/QueryPlanUserControl.xaml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/QueryPlanUserControl.xaml.cs b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/QueryPlanUserControl.xaml.cs
new file mode 100644
index 00000000..bec02be5
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/QueryPlanUserControl.xaml.cs
@@ -0,0 +1,58 @@
+using MarkMpn.Sql4Cds.Engine;
+using MarkMpn.Sql4Cds.Engine.ExecutionPlan;
+using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
+using Microsoft.VisualStudio.PlatformUI;
+//using Microsoft.Web.WebView2.Core;
+using System.Buffers;
+using System.Diagnostics;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Forms;
+
+namespace MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide
+{
+ public partial class QueryPlanUserControl : System.Windows.Controls.UserControl
+ {
+ private readonly IRootExecutionPlanNode _plan;
+
+ public QueryPlanUserControl(IRootExecutionPlanNode plan)
+ {
+ InitializeComponent();
+ DataContext = this;
+
+ _plan = plan;
+ this.Loaded += QueryPlanUserControl_Loaded;
+ }
+
+ private void QueryPlanUserControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ // Create the interop host control.
+ var host = new System.Windows.Forms.Integration.WindowsFormsHost();
+
+ // Create the control.
+ var control = new MarkMpn.Sql4Cds.Controls.ExecutionPlanView { Plan = _plan };
+ control.Dock = DockStyle.Fill;
+ var propertyGrid = new PropertyGrid();
+ propertyGrid.Dock = DockStyle.Fill;
+ control.NodeSelected += (s, e) =>
+ {
+ if (control.Selected == null)
+ propertyGrid.SelectedObject = null;
+ else
+ propertyGrid.SelectedObject = new ExecutionPlanNodeTypeDescriptor(control.Selected, true, null);
+ };
+ var splitter = new SplitContainer { Dock = DockStyle.Fill };
+ splitter.Panel1.Controls.Add(control);
+ splitter.Panel2.Controls.Add(propertyGrid);
+ splitter.FixedPanel = FixedPanel.Panel2;
+
+ // Assign the MaskedTextBox control as the host control's child.
+ host.Child = splitter;
+
+ // Add the interop host control to the Grid
+ // control's collection of child controls.
+ this.grid.Children.Add(host);
+ }
+ }
+}
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/SerializedPlan.cs b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/SerializedPlan.cs
new file mode 100644
index 00000000..2521ea6c
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/SerializedPlan.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide
+{
+ public class SerializedPlan
+ {
+ public string Plan { get; set; }
+ }
+}
diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/source.extension.vsixmanifest b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/source.extension.vsixmanifest
new file mode 100644
index 00000000..656133fa
--- /dev/null
+++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/source.extension.vsixmanifest
@@ -0,0 +1,39 @@
+
+
+
+
+ SQL 4 CDS Visualizer
+ View SQL 4 CDS query plan directly inside Visual Studio.
+ false
+ https://github.com/MarkMpn.Sql4Cds
+ https://github.com/MarkMpn/Sql4Cds/blob/master/README.md
+ Images\IconSmall.png
+ SQL 4 CDS,SQL4CDS, Visualizer, Query Plan
+
+
+
+
+
+ amd64
+
+
+ arm64
+
+
+ amd64
+
+
+ arm64
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs
index f1c51508..31eb0aff 100644
--- a/MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs
+++ b/MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs
@@ -597,6 +597,9 @@ UNION ALL
+
+
+
");
}
diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanNodeTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanNodeTests.cs
index 09c12097..86dbaf33 100644
--- a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanNodeTests.cs
+++ b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanNodeTests.cs
@@ -1044,5 +1044,232 @@ public void NestedLoopJoinLeftOuterTest()
Assert.AreEqual("Joe", ((SqlString)results[2]["f.firstname"]).Value);
Assert.IsTrue(((SqlString)results[2]["l.lastname"]).IsNull);
}
+
+ [TestMethod]
+ public void FetchXmlSingleTablePrimaryKey()
+ {
+ var fetch = new FetchXmlScan
+ {
+ DataSource = _localDataSource.Name,
+ Alias = "account",
+ FetchXml = new FetchXml.FetchType
+ {
+ Items = new object[]
+ {
+ new FetchXml.FetchEntityType
+ {
+ name = "account",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "accountid" },
+ new FetchXml.FetchAttributeType { name = "name" }
+ }
+ }
+ }
+ }
+ };
+ var schema = fetch.GetSchema(new NodeCompilationContext(_localDataSources, new StubOptions(), null, null));
+ Assert.AreEqual("account.accountid", schema.PrimaryKey);
+ }
+
+ [TestMethod]
+ public void FetchXmlChildTablePrimaryKey()
+ {
+ var fetch = new FetchXmlScan
+ {
+ DataSource = _localDataSource.Name,
+ Alias = "account",
+ FetchXml = new FetchXml.FetchType
+ {
+ Items = new object[]
+ {
+ new FetchXml.FetchEntityType
+ {
+ name = "account",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "accountid" },
+ new FetchXml.FetchAttributeType { name = "name" },
+ new FetchXml.FetchLinkEntityType
+ {
+ name = "contact",
+ from = "parentcustomerid",
+ to = "accountid",
+ alias = "contact",
+ linktype = "inner",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "contactid" },
+ new FetchXml.FetchAttributeType { name = "fullname" }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ var schema = fetch.GetSchema(new NodeCompilationContext(_localDataSources, new StubOptions(), null, null));
+ Assert.AreEqual("contact.contactid", schema.PrimaryKey);
+ }
+
+ [TestMethod]
+ public void FetchXmlChildTableOuterJoinPrimaryKey()
+ {
+ var fetch = new FetchXmlScan
+ {
+ DataSource = _localDataSource.Name,
+ Alias = "account",
+ FetchXml = new FetchXml.FetchType
+ {
+ Items = new object[]
+ {
+ new FetchXml.FetchEntityType
+ {
+ name = "account",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "accountid" },
+ new FetchXml.FetchAttributeType { name = "name" },
+ new FetchXml.FetchLinkEntityType
+ {
+ name = "contact",
+ from = "parentcustomerid",
+ to = "accountid",
+ alias = "contact",
+ linktype = "outer",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "contactid" },
+ new FetchXml.FetchAttributeType { name = "fullname" }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ var schema = fetch.GetSchema(new NodeCompilationContext(_localDataSources, new StubOptions(), null, null));
+ Assert.IsNull(schema.PrimaryKey);
+ }
+
+ [TestMethod]
+ public void FetchXmlParentTablePrimaryKey()
+ {
+ var fetch = new FetchXmlScan
+ {
+ DataSource = _localDataSource.Name,
+ Alias = "account",
+ FetchXml = new FetchXml.FetchType
+ {
+ Items = new object[]
+ {
+ new FetchXml.FetchEntityType
+ {
+ name = "account",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "accountid" },
+ new FetchXml.FetchAttributeType { name = "name" },
+ new FetchXml.FetchLinkEntityType
+ {
+ name = "contact",
+ from = "contactid",
+ to = "primarycontactid",
+ alias = "contact",
+ linktype = "inner",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "contactid" },
+ new FetchXml.FetchAttributeType { name = "fullname" }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ var schema = fetch.GetSchema(new NodeCompilationContext(_localDataSources, new StubOptions(), null, null));
+ Assert.AreEqual("account.accountid", schema.PrimaryKey);
+ }
+
+ [TestMethod]
+ public void FetchXmlParentTableOuterJoinPrimaryKey()
+ {
+ var fetch = new FetchXmlScan
+ {
+ DataSource = _localDataSource.Name,
+ Alias = "account",
+ FetchXml = new FetchXml.FetchType
+ {
+ Items = new object[]
+ {
+ new FetchXml.FetchEntityType
+ {
+ name = "account",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "accountid" },
+ new FetchXml.FetchAttributeType { name = "name" },
+ new FetchXml.FetchLinkEntityType
+ {
+ name = "contact",
+ from = "contactid",
+ to = "primarycontactid",
+ alias = "contact",
+ linktype = "inner",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "contactid" },
+ new FetchXml.FetchAttributeType { name = "fullname" }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ var schema = fetch.GetSchema(new NodeCompilationContext(_localDataSources, new StubOptions(), null, null));
+ Assert.AreEqual("account.accountid", schema.PrimaryKey);
+ }
+
+ [TestMethod]
+ public void FetchXmlChildTableFreeTextJoinPrimaryKey()
+ {
+ var fetch = new FetchXmlScan
+ {
+ DataSource = _localDataSource.Name,
+ Alias = "account",
+ FetchXml = new FetchXml.FetchType
+ {
+ Items = new object[]
+ {
+ new FetchXml.FetchEntityType
+ {
+ name = "account",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "accountid" },
+ new FetchXml.FetchAttributeType { name = "name" },
+ new FetchXml.FetchLinkEntityType
+ {
+ name = "contact",
+ from = "fullname",
+ to = "name",
+ alias = "contact",
+ linktype = "inner",
+ Items = new object[]
+ {
+ new FetchXml.FetchAttributeType { name = "contactid" },
+ new FetchXml.FetchAttributeType { name = "fullname" }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ var schema = fetch.GetSchema(new NodeCompilationContext(_localDataSources, new StubOptions(), null, null));
+ Assert.IsNull(schema.PrimaryKey);
+ }
}
}
diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
index 854836db..230e7084 100644
--- a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
+++ b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
@@ -2823,8 +2823,7 @@ ORDER BY firstname
");
var innerAlias = AssertNode(loop.RightSource);
var innerTop = AssertNode(innerAlias.Source);
- var innerSort = AssertNode(innerTop.Source);
- var innerIndexSpool = AssertNode(innerSort.Source);
+ var innerIndexSpool = AssertNode(innerTop.Source);
Assert.AreEqual("contact.parentcustomerid", innerIndexSpool.KeyColumn);
Assert.AreEqual("@Expr1", innerIndexSpool.SeekValue);
var innerFetch = AssertNode(innerIndexSpool.Source);
@@ -2837,6 +2836,7 @@ ORDER BY firstname
+
");
}
@@ -3714,7 +3714,7 @@ GROUP BY firstname
},
};
- var result = select.Execute(new NodeExecutionContext(_localDataSources, this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var result = select.Execute(new NodeExecutionContext(_localDataSources, this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(result);
@@ -4192,7 +4192,7 @@ DECLARE @test int
Assert.AreEqual(1, selectConstantScan.Values.Count);
var parameterTypes = new Dictionary();
- var parameterValues = new Dictionary();
+ var parameterValues = new Dictionary();
foreach (var plan in plans)
{
@@ -4246,7 +4246,7 @@ public void SetVariableInDeclaration()
Assert.AreEqual(1, selectConstantScan.Values.Count);
var parameterTypes = new Dictionary();
- var parameterValues = new Dictionary();
+ var parameterValues = new Dictionary();
foreach (var plan in plans)
{
@@ -4304,7 +4304,7 @@ DECLARE @test varchar(3)
var plans = planBuilder.Build(query, null, out _);
var parameterTypes = new Dictionary();
- var parameterValues = new Dictionary();
+ var parameterValues = new Dictionary();
foreach (var plan in plans)
{
@@ -4338,7 +4338,7 @@ DECLARE @test varchar(3)
var plans = planBuilder.Build(query, null, out _);
var parameterTypes = new Dictionary();
- var parameterValues = new Dictionary();
+ var parameterValues = new Dictionary();
foreach (var plan in plans)
{
@@ -4430,7 +4430,7 @@ DECLARE @test varchar(3)
};
var parameterTypes = new Dictionary();
- var parameterValues = new Dictionary();
+ var parameterValues = new Dictionary();
foreach (var plan in plans)
{
@@ -4464,7 +4464,7 @@ DECLARE @test varchar
var plans = planBuilder.Build(query, null, out _);
var parameterTypes = new Dictionary();
- var parameterValues = new Dictionary();
+ var parameterValues = new Dictionary();
foreach (var plan in plans)
{
@@ -5330,13 +5330,14 @@ public void DistinctOrderByOptionSet()
Assert.AreEqual(1, plans.Length);
var select = AssertNode(plans[0]);
- var fetch = AssertNode(select.Source);
+ var sort = AssertNode(select.Source);
+ Assert.AreEqual("new_customentity.new_optionsetvalue", sort.Sorts[0].ToSql());
+ var fetch = AssertNode(sort.Source);
AssertFetchXml(fetch, @"
-
+
-
");
}
@@ -6780,10 +6781,13 @@ AND [union. all].logicalname IN ('createdon')
var sort = AssertNode(select.Source);
- var join1 = AssertNode(sort.Source);
+ var filter1 = AssertNode(sort.Source);
+ Assert.AreEqual("[union. all].logicalname = a2.logicalname", filter1.Filter.ToSql());
+
+ var join1 = AssertNode(filter1.Source);
Assert.AreEqual("a2.entitylogicalname", join1.LeftAttribute.ToSql());
Assert.AreEqual("[union. all].eln", join1.RightAttribute.ToSql());
- Assert.AreEqual("[union. all].logicalname = a2.logicalname", join1.AdditionalJoinCriteria.ToSql());
+ Assert.IsNull(join1.AdditionalJoinCriteria);
var mq1 = AssertNode(join1.LeftSource);
Assert.AreEqual("french", mq1.DataSource);
@@ -7210,33 +7214,36 @@ public void OrderByOptionSetName()
}
[TestMethod]
- public void OrderByOptionSetValue()
+ public void OrderByOptionSetValueWithUseRawOrderBy()
{
- var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+ using (_localDataSource.SetUseRawOrderByReliable(true))
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
- var query = @"SELECT new_customentityid FROM new_customentity ORDER BY new_optionsetvalue";
+ var query = @"SELECT new_customentityid FROM new_customentity ORDER BY new_optionsetvalue";
- var plans = planBuilder.Build(query, null, out _);
+ var plans = planBuilder.Build(query, null, out _);
- Assert.AreEqual(1, plans.Length);
+ Assert.AreEqual(1, plans.Length);
- var select = AssertNode(plans[0]);
- var fetch = AssertNode(select.Source);
- AssertFetchXml(fetch, @"
-
-
-
-
-
- ");
+ var select = AssertNode(plans[0]);
+ var fetch = AssertNode(select.Source);
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+ ");
+ }
}
[TestMethod]
- public void OrderByOptionSetValueAndName()
+ public void OrderByOptionSetValueWithoutUseRawOrderBy()
{
var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
- var query = @"SELECT new_customentityid FROM new_customentity ORDER BY new_optionsetvalue, new_optionsetvaluename";
+ var query = @"SELECT new_customentityid FROM new_customentity ORDER BY new_optionsetvalue";
var plans = planBuilder.Build(query, null, out _);
@@ -7244,20 +7251,47 @@ public void OrderByOptionSetValueAndName()
var select = AssertNode(plans[0]);
var sort = AssertNode(select.Source);
- Assert.AreEqual(1, sort.PresortedCount);
- Assert.AreEqual(2, sort.Sorts.Count);
- Assert.AreEqual("new_customentity.new_optionsetvaluename", sort.Sorts[1].Expression.ToSql());
+ Assert.AreEqual("new_customentity.new_optionsetvalue", sort.Sorts[0].Expression.ToSql());
var fetch = AssertNode(sort.Source);
AssertFetchXml(fetch, @"
-
+
-
");
}
+ [TestMethod]
+ public void OrderByOptionSetValueAndName()
+ {
+ using (_localDataSource.SetUseRawOrderByReliable(true))
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"SELECT new_customentityid FROM new_customentity ORDER BY new_optionsetvalue, new_optionsetvaluename";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var sort = AssertNode(select.Source);
+ Assert.AreEqual(1, sort.PresortedCount);
+ Assert.AreEqual(2, sort.Sorts.Count);
+ Assert.AreEqual("new_customentity.new_optionsetvaluename", sort.Sorts[1].Expression.ToSql());
+ var fetch = AssertNode(sort.Source);
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+
+ ");
+ }
+ }
+
[TestMethod]
public void ExistsOrInAndColumnComparisonOrderByEntityName()
{
@@ -7396,5 +7430,408 @@ ORDER BY
");
}
}
+
+ [TestMethod]
+ public void DistinctUsesCustomPaging()
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select distinct
+account.name, contact.firstname
+from account
+left outer join contact ON account.accountid = contact.parentcustomerid";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var fetch = AssertNode(select.Source);
+
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+
+
+
+
+");
+ Assert.IsTrue(fetch.UsingCustomPaging);
+ }
+
+ [TestMethod]
+ public void NotExistWithJoin()
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select top 10 a2.name
+from account a2
+where not exists (
+ select top 10 a.accountid
+ from account a
+ inner join contact c on c.parentcustomerid = a.accountid
+ where a.accountid = a2.accountid
+)
+";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var top = AssertNode(select.Source);
+ var filter = AssertNode(top.Source);
+ var join = AssertNode(filter.Source);
+ var fetch1 = AssertNode(join.LeftSource);
+ var fetch2 = AssertNode(join.RightSource);
+
+ Assert.AreEqual("a2", fetch1.Alias);
+ AssertFetchXml(fetch1, @"
+
+
+
+
+
+
+");
+ Assert.IsFalse(fetch1.UsingCustomPaging);
+
+ Assert.AreEqual("Expr2", fetch2.Alias);
+ AssertFetchXml(fetch2, @"
+
+
+
+
+
+
+
+");
+ Assert.IsTrue(fetch2.UsingCustomPaging);
+ Assert.AreEqual(1, fetch2.ColumnMappings.Count);
+ Assert.AreEqual("Expr2.accountid", fetch2.ColumnMappings[0].OutputColumn);
+ Assert.AreEqual("a.accountid", fetch2.ColumnMappings[0].SourceColumn);
+
+ Assert.AreEqual("a2.accountid", join.LeftAttribute.ToSql());
+ Assert.AreEqual("Expr2.accountid", join.RightAttribute.ToSql());
+ Assert.AreEqual(QualifiedJoinType.LeftOuter, join.JoinType);
+ Assert.IsTrue(join.SemiJoin);
+ Assert.AreEqual(1, join.DefinedValues.Count);
+ Assert.AreEqual("Expr2.accountid", join.DefinedValues["Expr3"]);
+
+ Assert.AreEqual("Expr3 IS NULL", filter.Filter.ToSql());
+
+ Assert.AreEqual("10", top.Top.ToSql());
+
+ Assert.AreEqual(1, select.ColumnSet.Count);
+ Assert.AreEqual("a2.name", select.ColumnSet[0].SourceColumn);
+ Assert.AreEqual("name", select.ColumnSet[0].OutputColumn);
+ }
+
+ [TestMethod]
+ public void ScalarSubquery()
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select top 10 * from (
+select fullname, (select name from account where accountid = parentcustomerid) from contact
+) a";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var fetch = AssertNode(select.Source);
+
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+
+
+");
+ }
+
+ [TestMethod]
+ public void SubqueryInJoinCriteriaRHS()
+ {
+ using (_localDataSource.EnableJoinOperator(JoinOperator.In))
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select
+*
+from account
+inner join contact ON account.accountid = contact.parentcustomerid AND contact.firstname IN (SELECT new_name FROM new_customentity)";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var fetch = AssertNode(select.Source);
+
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+
+
+
+");
+ }
+ }
+
+ [TestMethod]
+ public void SubqueryInJoinCriteriaRHSCorrelatedExists()
+ {
+ using (_localDataSource.EnableJoinOperator(JoinOperator.In))
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select
+*
+from account
+inner join contact ON account.accountid = contact.parentcustomerid AND EXISTS(SELECT * FROM new_customentity WHERE new_name = contact.firstname)";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var fetch = AssertNode(select.Source);
+
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+
+
+
+");
+ }
+ }
+
+ [TestMethod]
+ public void SubqueryInJoinCriteriaLHS()
+ {
+ using (_localDataSource.EnableJoinOperator(JoinOperator.In))
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select
+*
+from account
+inner join contact ON account.accountid = contact.parentcustomerid AND account.name IN (SELECT new_name FROM new_customentity)";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var fetch = AssertNode(select.Source);
+
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+
+
+
+");
+ }
+ }
+
+ [TestMethod]
+ public void SubqueryInJoinCriteriaLHSCorrelatedExists()
+ {
+ using (_localDataSource.EnableJoinOperator(JoinOperator.In))
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select
+*
+from account
+inner join contact ON account.accountid = contact.parentcustomerid AND EXISTS(SELECT * FROM new_customentity WHERE new_name = account.name)";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var fetch = AssertNode(select.Source);
+
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+
+
+
+");
+ }
+ }
+
+ [TestMethod]
+ public void SubqueryInJoinCriteriaLHSAndRHSInnerJoin()
+ {
+ using (_localDataSource.EnableJoinOperator(JoinOperator.In))
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select
+*
+from account
+inner join contact ON account.accountid = contact.parentcustomerid AND contact.fullname IN (SELECT new_name FROM new_customentity WHERE account.turnover = new_customentity.new_decimalprop)";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var filter = AssertNode(select.Source);
+ Assert.AreEqual("Expr2 IS NOT NULL", filter.Filter.ToSql());
+ var loop = AssertNode(filter.Source);
+ Assert.AreEqual(QualifiedJoinType.LeftOuter, loop.JoinType);
+ Assert.IsTrue(loop.SemiJoin);
+ Assert.AreEqual("contact.fullname = new_customentity.new_name", loop.JoinCondition.ToSql());
+ Assert.AreEqual(1, loop.OuterReferences.Count);
+ Assert.AreEqual("@Expr1", loop.OuterReferences["account.turnover"]);
+ Assert.AreEqual("new_customentity.new_name", loop.DefinedValues["Expr2"]);
+ var fetch1 = AssertNode(loop.LeftSource);
+ AssertFetchXml(fetch1, @"
+
+
+
+
+
+
+
+");
+ var spool = AssertNode(loop.RightSource);
+ Assert.AreEqual("new_customentity.new_decimalprop", spool.KeyColumn);
+ Assert.AreEqual("@Expr1", spool.SeekValue);
+ var fetch2 = AssertNode(spool.Source);
+ AssertFetchXml(fetch2, @"
+
+
+
+
+
+
+
+
+");
+ }
+ }
+
+ [TestMethod]
+ public void SubqueryInJoinCriteriaLHSAndRHSOuterJoin()
+ {
+ using (_localDataSource.EnableJoinOperator(JoinOperator.In))
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select
+*
+from account
+left outer join contact ON account.accountid = contact.parentcustomerid AND contact.fullname IN (SELECT new_name FROM new_customentity WHERE account.turnover = new_customentity.new_decimalprop)";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var loop1 = AssertNode(select.Source);
+ Assert.AreEqual(QualifiedJoinType.LeftOuter, loop1.JoinType);
+ Assert.AreEqual("account.accountid = contact.parentcustomerid\r\nAND Expr3 IS NOT NULL", loop1.JoinCondition.ToSql());
+ Assert.IsFalse(loop1.SemiJoin);
+ Assert.AreEqual(1, loop1.OuterReferences.Count);
+ Assert.AreEqual("@Expr1", loop1.OuterReferences["account.turnover"]);
+ var fetch1 = AssertNode(loop1.LeftSource);
+ AssertFetchXml(fetch1, @"
+
+
+
+
+");
+ var merge = AssertNode(loop1.RightSource);
+ Assert.AreEqual(1, merge.DefinedValues.Count);
+ Assert.AreEqual("Expr2.new_name", merge.DefinedValues["Expr3"]);
+ var tableSpool = AssertNode(merge.LeftSource);
+ var fetch2 = AssertNode(tableSpool.Source);
+ AssertFetchXml(fetch2, @"
+
+
+
+
+
+");
+ var sort = AssertNode(merge.RightSource);
+ var distinct = AssertNode(sort.Source);
+ var alias = AssertNode(distinct.Source);
+ var spool = AssertNode(alias.Source);
+ Assert.AreEqual("new_customentity.new_decimalprop", spool.KeyColumn);
+ Assert.AreEqual("@Expr1", spool.SeekValue);
+ var fetch3 = AssertNode(spool.Source);
+ AssertFetchXml(fetch3, @"
+
+
+
+
+
+
+
+
+");
+
+ }
+ }
+
+ [TestMethod]
+ public void VirtualAttributeAliases()
+ {
+ var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
+
+ var query = @"
+select statecodename [state], parentcustomerid x, parentcustomeridname from contact";
+
+ var plans = planBuilder.Build(query, null, out _);
+
+ Assert.AreEqual(1, plans.Length);
+
+ var select = AssertNode(plans[0]);
+ var fetch = AssertNode(select.Source);
+
+ AssertFetchXml(fetch, @"
+
+
+
+
+
+");
+ }
}
}
diff --git a/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmDataSource.cs b/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmDataSource.cs
index 52395a19..5ce9a63b 100644
--- a/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmDataSource.cs
+++ b/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmDataSource.cs
@@ -29,6 +29,7 @@ public void Dispose()
private bool _columnComparisonAvailable = true;
private bool _orderByEntityNameAvailable = false;
+ private bool _useRawOrderByReliable = false;
private List _joinOperators;
public FakeXrmDataSource()
@@ -42,6 +43,8 @@ public FakeXrmDataSource()
public override List JoinOperatorsAvailable => _joinOperators;
+ public override bool UseRawOrderByReliable => _useRawOrderByReliable;
+
public IDisposable SetColumnComparison(bool enable)
{
var original = _columnComparisonAvailable;
@@ -54,6 +57,12 @@ public IDisposable SetOrderByEntityName(bool enable)
return new Reset(this, x => x._orderByEntityNameAvailable = enable, x => x._orderByEntityNameAvailable = original);
}
+ public IDisposable SetUseRawOrderByReliable(bool enable)
+ {
+ var original = _useRawOrderByReliable;
+ return new Reset(this, x => x._useRawOrderByReliable = enable, x => x._useRawOrderByReliable = original);
+ }
+
public IDisposable EnableJoinOperator(JoinOperator op)
{
var add = !JoinOperatorsAvailable.Contains(op);
diff --git a/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs
index 8e601557..1f94a70a 100644
--- a/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs
+++ b/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs
@@ -538,11 +538,13 @@ public void InvalidSortOnLinkEntity()
+
-
+
+
");
@@ -702,7 +704,7 @@ public void SelectArithmetic()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -752,7 +754,7 @@ public void WhereComparingTwoFields()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -797,7 +799,7 @@ public void WhereComparingExpression()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -838,7 +840,7 @@ public void BackToFrontLikeExpression()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -873,7 +875,7 @@ public void UpdateFieldToField()
}
};
- ((UpdateNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), out _, out _);
+ ((UpdateNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), out _, out _);
Assert.AreEqual("Carrington", _context.Data["contact"][guid]["firstname"]);
}
@@ -905,7 +907,7 @@ public void UpdateFieldToExpression()
}
};
- ((UpdateNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), out _, out _);
+ ((UpdateNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), out _, out _);
Assert.AreEqual("Hello Carrington", _context.Data["contact"][guid]["firstname"]);
}
@@ -941,7 +943,7 @@ public void UpdateReplace()
}
};
- ((UpdateNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), out _, out _);
+ ((UpdateNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), out _, out _);
Assert.AreEqual("--CDS--", _context.Data["contact"][guid]["firstname"]);
}
@@ -972,7 +974,7 @@ public void StringFunctions()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1012,7 +1014,7 @@ public void SelectExpression()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1062,7 +1064,7 @@ public void SelectExpressionNullValues()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1107,7 +1109,7 @@ public void OrderByExpression()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1152,7 +1154,7 @@ public void OrderByAliasedField()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1196,7 +1198,7 @@ public void OrderByCalculatedField()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1240,7 +1242,7 @@ public void OrderByCalculatedFieldByIndex()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1282,7 +1284,7 @@ public void DateCalculations()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1370,7 +1372,7 @@ public void CustomFilterAggregateHavingProjectionSortAndTop()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1433,7 +1435,7 @@ public void FilterCaseInsensitive()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1490,7 +1492,7 @@ public void GroupCaseInsensitive()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1543,7 +1545,7 @@ public void AggregateExpressionsWithoutGrouping()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1598,7 +1600,7 @@ public void AggregateQueryProducesAlternative()
}
};
- var dataReader = alternativeQuery.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = alternativeQuery.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1654,7 +1656,7 @@ public void GuidEntityReferenceInequality()
}
};
- var dataReader = select.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = select.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -1706,7 +1708,7 @@ public void UpdateGuidToEntityReference()
}
};
- update.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), out _, out _);
+ update.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), out _, out _);
Assert.AreEqual(new EntityReference("contact", contact1), _context.Data["account"][account1].GetAttributeValue("primarycontactid"));
Assert.AreEqual(new EntityReference("contact", contact2), _context.Data["account"][account2].GetAttributeValue("primarycontactid"));
@@ -1721,9 +1723,8 @@ public void CompareDateFields()
var queries = planBuilder.Build(query, null, out _);
AssertFetchXml(queries, @"
-
+
-
@@ -1731,7 +1732,6 @@ public void CompareDateFields()
-
");
@@ -1981,7 +1981,7 @@ public void ImplicitTypeConversion()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -2014,7 +2014,7 @@ public void ImplicitTypeConversionComparison()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -2038,7 +2038,7 @@ public void GlobalOptionSet()
Assert.AreEqual("globaloptionset.name = 'test'", filterNode.Filter.ToSql());
var optionsetNode = (GlobalOptionSetQueryNode)filterNode.Source;
- var dataReader = selectNode.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = selectNode.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -2059,7 +2059,7 @@ public void EntityDetails()
var sortNode = (SortNode)selectNode.Source;
var metadataNode = (MetadataQueryNode)sortNode.Source;
- var dataReader = selectNode.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = selectNode.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -2077,7 +2077,7 @@ public void AttributeDetails()
var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);
var queries = planBuilder.Build(query, null, out _);
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -2135,7 +2135,7 @@ public void OptionSetNameSelect()
CollectionAssert.AreEqual(new[] { "new_optionsetvalue", "new_optionsetvaluename" }, select.ColumnSet.Select(c => c.OutputColumn).ToList());
- var dataReader = select.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = select.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -2459,7 +2459,7 @@ public void CharIndex()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -2489,7 +2489,7 @@ public void CastDateTimeToDate()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
@@ -2527,7 +2527,7 @@ public void GroupByPrimaryFunction()
}
};
- var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
+ var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default);
var dataTable = new DataTable();
dataTable.Load(dataReader);
diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs
index f491f2b6..6987088f 100644
--- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs
+++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs
@@ -29,6 +29,13 @@ public class Sql4CdsCommand : DbCommand
private bool _cancelledManually;
private string _lastDatabase;
+ static Sql4CdsCommand()
+ {
+ // Ensure the FetchXmlScan class is loaded - avoids multithreading issues when using the custom debug visualizer
+ // on the command object.
+ new FetchXmlScan();
+ }
+
public Sql4CdsCommand(Sql4CdsConnection connection) : this(connection, string.Empty)
{
}
diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsConnection.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsConnection.cs
index 15578c28..167f66ac 100644
--- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsConnection.cs
+++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsConnection.cs
@@ -29,7 +29,7 @@ public class Sql4CdsConnection : DbConnection
private readonly IDictionary _dataSources;
private readonly ChangeDatabaseOptionsWrapper _options;
private readonly Dictionary _globalVariableTypes;
- private readonly Dictionary _globalVariableValues;
+ private readonly Dictionary _globalVariableValues;
private readonly TelemetryClient _ai;
///
@@ -74,7 +74,7 @@ public Sql4CdsConnection(IDictionary dataSources)
["@@VERSION"] = DataTypeHelpers.NVarChar(Int32.MaxValue, _dataSources[_options.PrimaryDataSource].DefaultCollation, CollationLabel.CoercibleDefault),
["@@ERROR"] = DataTypeHelpers.Int,
};
- _globalVariableValues = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ _globalVariableValues = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["@@IDENTITY"] = SqlEntityReference.Null,
["@@ROWCOUNT"] = (SqlInt32)0,
@@ -291,7 +291,7 @@ public ColumnOrdering ColumnOrdering
internal Dictionary GlobalVariableTypes => _globalVariableTypes;
- internal Dictionary GlobalVariableValues => _globalVariableValues;
+ internal Dictionary GlobalVariableValues => _globalVariableValues;
internal TelemetryClient TelemetryClient => _ai;
diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsDataReader.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsDataReader.cs
index 3354c182..a88c27d0 100644
--- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsDataReader.cs
+++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsDataReader.cs
@@ -22,7 +22,7 @@ class Sql4CdsDataReader : DbDataReader
private readonly IQueryExecutionOptions _options;
private readonly CommandBehavior _behavior;
private readonly Dictionary _parameterTypes;
- private readonly Dictionary _parameterValues;
+ private readonly Dictionary _parameterValues;
private Dictionary _labelIndexes;
private int _recordsAffected;
private int _instructionPointer;
@@ -57,7 +57,7 @@ public Sql4CdsDataReader(Sql4CdsCommand command, IQueryExecutionOptions options,
Close();
}
- internal Dictionary ParameterValues => _parameterValues;
+ internal Dictionary ParameterValues => _parameterValues;
private Dictionary LabelIndexes
{
@@ -75,7 +75,7 @@ private Dictionary LabelIndexes
}
}
- private bool ExecuteWithExceptionHandling(Dictionary parameterTypes, Dictionary parameterValues)
+ private bool ExecuteWithExceptionHandling(Dictionary parameterTypes, Dictionary parameterValues)
{
while (true)
{
@@ -109,7 +109,7 @@ private bool ExecuteWithExceptionHandling(Dictionary
}
}
- private bool Execute(Dictionary parameterTypes, Dictionary parameterValues)
+ private bool Execute(Dictionary parameterTypes, Dictionary parameterValues)
{
IRootExecutionPlanNode logNode = null;
var context = new NodeExecutionContext(_connection.DataSources, _options, parameterTypes, parameterValues, msg => _connection.OnInfoMessage(logNode, msg));
diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs
index c5ea90eb..705df781 100644
--- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs
+++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
+using System.Data.SqlTypes;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.TransactSql.ScriptDom;
@@ -50,11 +51,11 @@ internal Dictionary GetParameterTypes()
.ToDictionary(param => param.FullParameterName, param => param.GetDataType(), StringComparer.OrdinalIgnoreCase);
}
- internal Dictionary GetParameterValues()
+ internal Dictionary GetParameterValues()
{
return _parameters
.Cast()
- .ToDictionary(param => param.FullParameterName, param => (object) param.GetValue(), StringComparer.OrdinalIgnoreCase);
+ .ToDictionary(param => param.FullParameterName, param => param.GetValue(), StringComparer.OrdinalIgnoreCase);
}
public override bool Contains(string value)
diff --git a/MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs b/MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs
index f13e619b..095e7104 100644
--- a/MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs
+++ b/MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs
@@ -2,6 +2,7 @@
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Metadata.Query;
+using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -21,6 +22,7 @@ public class AttributeMetadataCache : IAttributeMetadataCache
private readonly IDictionary _minimalMetadata;
private readonly ISet _minimalLoading;
private readonly IDictionary _invalidEntities;
+ private readonly Lazy _recycleBinEntities;
///
/// Creates a new
@@ -34,6 +36,46 @@ public AttributeMetadataCache(IOrganizationService org)
_minimalMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase);
_minimalLoading = new HashSet(StringComparer.OrdinalIgnoreCase);
_invalidEntities = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _recycleBinEntities = new Lazy