Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error Reporting #430

Merged
merged 31 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7b94973
Removed restriction on EXECUTE AS requiring a string literal
MarkMpn Jan 24, 2024
292daf1
Improved error reporting for missing or duplicate users
MarkMpn Jan 24, 2024
aaa3dde
Simplified EXECUTE AS query
MarkMpn Jan 26, 2024
5988e8b
Wrap SqlException in Sql4CdsException for consistency
MarkMpn Jan 31, 2024
32296e7
Fixed passing literal nulls to collation-aware functions
MarkMpn Jan 31, 2024
0448593
Expose type conversion errors in folded filters at runtime in same wa…
MarkMpn Jan 31, 2024
fe9e3c1
Improved display of error line numbers
MarkMpn Jan 31, 2024
9f594e7
Fixed type conversion error with DML statements
MarkMpn Jan 31, 2024
b51f4f5
More efficient OUTER/CROSS APPLY queries with small number of outer r…
MarkMpn Jan 31, 2024
896eb4c
Added validation for missing anchor query in recursive CTE
MarkMpn Feb 1, 2024
c83b00d
Fixed use of SELECT * in CTE without defined column names
MarkMpn Feb 1, 2024
74b43aa
Refactored handling of inline derived tables to avoid stack overflow …
MarkMpn Feb 1, 2024
0f567cf
Fixed typo in error message
MarkMpn Feb 5, 2024
c34e3a8
Fixed use of variables inside inner loop
MarkMpn Feb 5, 2024
4443f85
Normalize column names
MarkMpn Feb 5, 2024
cbf3239
Reuse compiled expressions
MarkMpn Feb 5, 2024
0ec8ffc
Fixed error with string literal column aliases
MarkMpn Feb 5, 2024
452d938
Avoid recalculating column prefixes for each row
MarkMpn Feb 5, 2024
edd5093
Avoid creating unnecessary lambda expressions
MarkMpn Feb 5, 2024
b334568
Removed unnecessary usings
MarkMpn Feb 5, 2024
d1f6217
Improved folding of multiple UNION statements
MarkMpn Feb 6, 2024
5968e34
Avoid creating Linq expressions where possible for improved performance
MarkMpn Feb 8, 2024
04b968f
Reduced query rewriting of select elements for improved performance
MarkMpn Feb 8, 2024
ff4310c
Fixed NullReferenceException when folding nested loop with null outer…
MarkMpn Feb 8, 2024
a19da3c
Fixed using TOP 1 within IN clause
MarkMpn Feb 8, 2024
fb014aa
Added support for rethrowing errors
MarkMpn Feb 8, 2024
6cb0700
Fold join condition on nested loop to the source nodes if it doesn't …
MarkMpn Feb 10, 2024
42df033
Expose warning on nested loops without join condition in execution pl…
MarkMpn Feb 10, 2024
d4717f0
Improved handling of errors from elastic tables
MarkMpn Feb 11, 2024
7c11bef
Improved query cancellation
MarkMpn Feb 11, 2024
a62c677
Merged from master
MarkMpn Feb 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 44 additions & 18 deletions MarkMpn.Sql4Cds.Controls/ExecutionPlanView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class Line
private List<Line> _lines;
private int _maxY;
private int _maxBottom;
private ToolTip _toolTip;
private IExecutionPlanNode _tooltipNode;

const int _offset = 32;
private readonly Size _iconSize = new Size(32, 32);
Expand All @@ -38,6 +40,13 @@ public ExecutionPlanView()
{
AutoScroll = true;
DoubleBuffered = true;

_toolTip = new ToolTip
{
InitialDelay = 1000, // Hover the mouse for 1 second before showing the tooltip
AutoPopDelay = 5000, // Show the tooltip for 5 seconds once triggered
ReshowDelay = 1000, // Delay 1 second between different controls
};
}

public bool Executed { get; set; }
Expand Down Expand Up @@ -167,7 +176,7 @@ protected override void OnPaint(PaintEventArgs e)
e.Graphics.DrawImage(image, iconRect);
}

if (Exception?.Node == kvp.Key)
if (Exception?.Node == kvp.Key || (kvp.Key is IExecutionPlanNodeWarning warning && warning.Warning != null))
{
// Show an error icon overlay
e.Graphics.DrawImage(SystemIcons.Error.ToBitmap(), new Rectangle(iconRect.Location, new Size(16, 16)));
Expand Down Expand Up @@ -287,32 +296,49 @@ protected override void OnMouseClick(MouseEventArgs e)
base.OnMouseClick(e);
Focus();

var hit = false;
var hit = HitTest(e.Location);

foreach (var node in _nodeLocations)
{
if (OriginToClient(node.Value).Contains(e.Location))
{
if (Selected != null)
Invalidate(Selected);
if (Selected != null)
Invalidate(Selected);

Selected = node.Key;
Selected = hit;

Invalidate(Selected);
if (Selected != null)
Invalidate(Selected);

OnNodeSelected(EventArgs.Empty);
OnNodeSelected(EventArgs.Empty);
}

hit = true;
break;
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);

var hit = HitTest(e.Location);

if (hit != _tooltipNode)
{
if (Exception != null && Exception.Node == hit)
_toolTip.SetToolTip(this, Exception.Message);
else if (hit is IExecutionPlanNodeWarning warning && warning.Warning != null)
_toolTip.SetToolTip(this, warning.Warning);
else if (hit is IFetchXmlExecutionPlanNode fetch)
_toolTip.SetToolTip(this, fetch.FetchXmlString);
else
_toolTip.SetToolTip(this, null);

_tooltipNode = hit;
}
}

if (!hit && Selected != null)
private IExecutionPlanNode HitTest(Point pt)
{
foreach (var node in _nodeLocations)
{
Invalidate(Selected);
Selected = null;
OnNodeSelected(EventArgs.Empty);
if (OriginToClient(node.Value).Contains(pt))
return node.Key;
}

return null;
}

protected override void OnGotFocus(EventArgs e)
Expand Down
147 changes: 147 additions & 0 deletions MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1755,6 +1755,47 @@ END CATCH
}
}

[TestMethod]
public void Rethrow()
{
using (var con = new Sql4CdsConnection(_localDataSource))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = @"
BEGIN TRY
SELECT * FROM invalid_table;
END TRY
BEGIN CATCH
SELECT @@ERROR, ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(), ERROR_PROCEDURE(), ERROR_LINE(), ERROR_MESSAGE();
THROW;
END CATCH";

using (var reader = cmd.ExecuteReader())
{
Assert.IsTrue(reader.Read());
Assert.AreEqual(208, reader.GetInt32(0));
Assert.AreEqual(208, reader.GetInt32(1));
Assert.AreEqual(16, reader.GetInt32(2));
Assert.AreEqual(1, reader.GetInt32(3));
Assert.IsTrue(reader.IsDBNull(4));
Assert.AreEqual(2, reader.GetInt32(5));
Assert.AreEqual("Invalid object name 'invalid_table'", reader.GetString(6));
Assert.IsFalse(reader.Read());

try
{
reader.NextResult();
Assert.Fail();
}
catch (Sql4CdsException ex)
{
Assert.AreEqual(208, ex.Number);
Assert.AreEqual(2, ex.LineNumber);
}
}
}
}

[DataTestMethod]
[DataRow("SELECT FORMATMESSAGE('Signed int %i, %d %i, %d, %+i, %+d, %+i, %+d', 5, -5, 50, -50, -11, -11, 11, 11)", "Signed int 5, -5 50, -50, -11, -11, +11, +11")]
[DataRow("SELECT FORMATMESSAGE('Signed int with up to 3 leading zeros %03i', 5)", "Signed int with up to 3 leading zeros 005")]
Expand All @@ -1779,5 +1820,111 @@ public void FormatMessage(string query, string expected)
Assert.AreEqual(expected, actual);
}
}

[DataTestMethod]
[DataRow("accountid", "uniqueidentifier", 8169)]
[DataRow("employees", "int", 245)]
[DataRow("createdon", "datetime", 241)]
[DataRow("turnover", "money", 235)]
[DataRow("new_decimalprop", "decimal", 8114)]
[DataRow("new_doubleprop", "float", 8114)]
public void ConversionErrors(string column, string type, int expectedError)
{
var tableName = column.StartsWith("new_") ? "new_customentity" : "account";

using (var con = new Sql4CdsConnection(_localDataSource))
using (var cmd = con.CreateCommand())
{
var accountId = Guid.NewGuid();
_context.Data["account"] = new Dictionary<Guid, Entity>
{
[accountId] = new Entity("account", accountId)
{
["accountid"] = accountId,
["employees"] = 10,
["createdon"] = DateTime.Now,
["turnover"] = new Money(1_000_000),
["address1_latitude"] = 45.0D
}
};
_context.Data["new_customentity"] = new Dictionary<Guid, Entity>
{
[accountId] = new Entity("new_customentity", accountId)
{
["new_customentityid"] = accountId,
["new_decimalprop"] = 123.45M,
["new_doubleprop"] = 123.45D
}
};

var queries = new[]
{
// The error should be thrown when filtering by a column in FetchXML
$"SELECT * FROM {tableName} WHERE {column} = 'test'",

// The same error should also be thrown when comparing the values in an expression
$"SELECT CASE WHEN {column} = 'test' then 1 else 0 end FROM {tableName}",

// And also when converting a value directly without a comparison
$"SELECT CAST('test' AS {type})"
};

foreach (var query in queries)
{
// The error should not be thrown when generating an estimated plan
cmd.CommandText = query;
cmd.Prepare();

try
{
cmd.ExecuteNonQuery();
Assert.Fail();
}
catch (Sql4CdsException ex)
{
Assert.AreEqual(expectedError, ex.Number);
}
}
}
}

[TestMethod]
public void MetadataGuidConversionErrors()
{
// Failures converting string to guid should be handled in the same way for metadata queries as for FetchXML
using (var con = new Sql4CdsConnection(_localDataSource))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = "SELECT logicalname FROM metadata.entity WHERE metadataid = 'test'";
cmd.Prepare();

try
{
cmd.ExecuteNonQuery();
Assert.Fail();
}
catch (Sql4CdsException ex)
{
Assert.AreEqual(8169, ex.Number);
}
}
}

[TestMethod]
public void MetadataEnumConversionErrors()
{
// Enum values are presented as simple strings, so there should be no error when converting invalid values
using (var con = new Sql4CdsConnection(_localDataSource))
using (var cmd = con.CreateCommand())
{
cmd.CommandText = "SELECT logicalname FROM metadata.entity WHERE ownershiptype = 'test'";
cmd.Prepare();

using (var reader = cmd.ExecuteReader())
{
Assert.IsFalse(reader.Read());
}
}
}
}
}
54 changes: 53 additions & 1 deletion MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,21 @@ WITH cte AS (
planBuilder.Build(query, null, out _);
}

[TestMethod]
[ExpectedException(typeof(NotSupportedQueryFragmentException))]
public void MissingAnchorQuery()
{
var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this);

var query = @"
WITH cte (x, y) AS (
SELECT x, y FROM cte
)
SELECT * FROM cte";

planBuilder.Build(query, null, out _);
}

[TestMethod]
public void AliasedAnonymousColumn()
{
Expand All @@ -475,7 +490,7 @@ WITH cte (id, fname, lname) AS (
Assert.AreEqual("lname", select.ColumnSet[2].OutputColumn);
Assert.AreEqual("contact.lastname", select.ColumnSet[2].SourceColumn);
var compute = AssertNode<ComputeScalarNode>(select.Source);
Assert.AreEqual("firstname + ''", compute.Columns["Expr1"].ToSql());
Assert.AreEqual("contact.firstname + ''", compute.Columns["Expr1"].ToSql());
var fetch = AssertNode<FetchXmlScan>(compute.Source);
AssertFetchXml(fetch, @"
<fetch>
Expand All @@ -490,6 +505,43 @@ WITH cte (id, fname, lname) AS (
</fetch>");
}

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

var query = @"
WITH source_data_cte AS (
SELECT *
FROM (VALUES
('M', 'B', '6152-000358'),
('M', 'B', '6152-000530'),
('M', 'B', '6152-000531'),
('B', 'C', '97048786'),
('C', 'D', '35528661'),
('A', 'B', '97680998')
) AS source_data (Column1, Column2, Column3)
)

SELECT *
FROM source_data_cte;";

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

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
Assert.AreEqual("Column1", select.ColumnSet[0].OutputColumn);
Assert.AreEqual("source_data_cte.Column1", select.ColumnSet[0].SourceColumn);
Assert.AreEqual("Column2", select.ColumnSet[1].OutputColumn);
Assert.AreEqual("source_data_cte.Column2", select.ColumnSet[1].SourceColumn);
Assert.AreEqual("Column3", select.ColumnSet[2].OutputColumn);
Assert.AreEqual("source_data_cte.Column3", select.ColumnSet[2].SourceColumn);

var constantScan = AssertNode<ConstantScanNode>(select.Source);
Assert.AreEqual("source_data_cte", constantScan.Alias);
}

[TestMethod]
public void SimpleRecursion()
{
Expand Down
Loading