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 #452

Merged
merged 5 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 23 additions & 1 deletion MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ internal Sql4CdsError(byte @class, int number, string message, TSqlFragment frag
/// </summary>
public TSqlFragment Fragment { get; }

internal static IEnumerable<Sql4CdsError> GetAllErrors()
{
return _errorNumberToDetails.Values;
}

internal static Sql4CdsError Create(int number, TSqlFragment fragment)
{
var template = _errorNumberToDetails[number];
Expand Down Expand Up @@ -183,6 +188,18 @@ internal static Sql4CdsError InvalidObjectName(SchemaObjectName obj)
return Create(208, obj, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name));
}

internal static Sql4CdsError NonFunctionCalledWithParameters(SchemaObjectName obj)
{
var name = obj.ToSql();
return Create(215, obj, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name));
}

internal static Sql4CdsError FunctionCalledWithoutParameters(SchemaObjectName obj)
{
var name = obj.ToSql();
return Create(216, obj, (SqlInt32)name.Length, Collation.USEnglish.ToSqlString(name));
}

internal static Sql4CdsError InvalidSprocName(SchemaObjectName sproc)
{
var name = sproc.ToSql();
Expand Down Expand Up @@ -386,7 +403,7 @@ internal static Sql4CdsError InvalidOptionValue(TSqlFragment value, string type)

internal static Sql4CdsError InvalidFunction(Identifier identifier)
{
return Create(195, identifier, (SqlInt32)identifier.Value.Length, Collation.USEnglish.ToSqlString(identifier.Value), "built-in function name");
return Create(195, identifier, (SqlInt32)identifier.Value.Length, Collation.USEnglish.ToSqlString(identifier.Value), Collation.USEnglish.ToSqlString("built-in function name"));
}

internal static Sql4CdsError InvalidFunctionParameterCount(Identifier identifier, int expectedArgs)
Expand Down Expand Up @@ -569,6 +586,11 @@ internal static Sql4CdsError InvalidColumnForFullTextSearch(ColumnReferenceExpre
return Create(7670, col, (SqlInt32)colName.Length, Collation.USEnglish.ToSqlString(colName));
}

internal static Sql4CdsError InvalidErrorNumber(int number)
{
return Create(2732, null, (SqlInt32)number);
}

internal static Sql4CdsError InvalidSeverityLevel(int maxSeverity)
{
return Create(2754, null, (SqlInt32)maxSeverity);
Expand Down
2 changes: 1 addition & 1 deletion MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1241,7 +1241,7 @@ private void AddNotNullFilters(ColumnList schema, Dictionary<string, IReadOnlyLi

private void AddSchemaAttribute(DataSource dataSource, ColumnList schema, Dictionary<string, IReadOnlyList<string>> aliases, string fullName, string simpleName, DataTypeReference type, EntityMetadata entityMetadata, AttributeMetadata attrMetadata, bool innerJoin)
{
var notNull = innerJoin && (attrMetadata.LogicalName == entityMetadata.PrimaryIdAttribute || attrMetadata.LogicalName == "createdon" || (attrMetadata.EntityLogicalName != "systemuser" && (attrMetadata.LogicalName == "createdby" || attrMetadata.AttributeOf == "createdby")));
var notNull = innerJoin && (attrMetadata.LogicalName == entityMetadata.PrimaryIdAttribute || attrMetadata.LogicalName == "createdon");

// Add the logical attribute
AddSchemaAttribute(schema, aliases, fullName, simpleName, type, notNull);
Expand Down
78 changes: 55 additions & 23 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/RaiseErrorNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ class RaiseErrorNode : BaseNode, IDmlQueryExecutionPlanNode
private int _executionCount;
private readonly Timer _timer = new Timer();

[Category("Raise Error")]
[Description("The error number that is generated")]
[DisplayName("Error Number")]
public ScalarExpression ErrorNumber { get; set; }

[Category("Raise Error")]
[Description("The error message that is generated")]
[DisplayName("Error Message")]
Expand Down Expand Up @@ -72,7 +77,8 @@ public object Clone()
{
return new RaiseErrorNode
{
ErrorMessage = ErrorMessage.Clone(),
ErrorNumber = ErrorNumber?.Clone(),
ErrorMessage = ErrorMessage?.Clone(),
Severity = Severity.Clone(),
State = State.Clone(),
Parameters = Parameters.Select(p => p.Clone()).ToArray(),
Expand All @@ -95,29 +101,55 @@ public override IEnumerable<IExecutionPlanNode> GetSources()

public void Execute(NodeExecutionContext context, out int recordsAffected, out string message)
{
var ecc = new ExpressionCompilationContext(context, null, null);
var eec = new ExpressionExecutionContext(context);

var msg = Execute<SqlString>(ErrorMessage, ecc, eec, DataTypeHelpers.NVarChar(Int32.MaxValue, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault));
var severity = Execute<SqlInt32>(Severity, ecc, eec, DataTypeHelpers.Int);
var state = Execute<SqlInt32>(State, ecc, eec, DataTypeHelpers.Int);

if (severity.Value > 18)
throw new QueryExecutionException(Sql4CdsError.InvalidSeverityLevel(18));

if (severity.Value < 0)
severity = 0;

if (state.Value > 255)
state = 255;
else if (state.Value < 0)
state = 1;

msg = ExpressionFunctions.FormatMessage(msg, eec, Parameters.Select(p => (INullable)p.Compile(ecc)(eec)).ToArray());
try
{
var ecc = new ExpressionCompilationContext(context, null, null);
var eec = new ExpressionExecutionContext(context);

SqlInt32 num;
SqlString msg;

if (ErrorNumber == null)
{
num = 50000;
msg = Execute<SqlString>(ErrorMessage, ecc, eec, DataTypeHelpers.NVarChar(Int32.MaxValue, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault));
}
else
{
num = Execute<SqlInt32>(ErrorNumber, ecc, eec, DataTypeHelpers.Int);
msg = Sql4CdsError.GetAllErrors().SingleOrDefault(e => e.Number == num.Value)?.Message;

if (num < 13000 || num == 50000)
throw new QueryExecutionException(Sql4CdsError.InvalidErrorNumber(num.Value));
}

var severity = Execute<SqlInt32>(Severity, ecc, eec, DataTypeHelpers.Int);
var state = Execute<SqlInt32>(State, ecc, eec, DataTypeHelpers.Int);

if (severity.Value > 18)
throw new QueryExecutionException(Sql4CdsError.InvalidSeverityLevel(18));

if (severity.Value < 0)
severity = 0;

if (state.Value > 255)
state = 255;
else if (state.Value < 0)
state = 1;

msg = ExpressionFunctions.FormatMessage(msg, eec, Parameters.Select(p => (INullable)p.Compile(ecc)(eec)).ToArray());

context.Log(new Sql4CdsError((byte)severity.Value, -1, num.Value, null, null, (byte)state.Value, msg.IsNull ? null : msg.Value));
recordsAffected = 0;
message = null;
}
catch (QueryExecutionException ex)
{
if (ex.Node == null)
ex.Node = this;

context.Log(new Sql4CdsError((byte)severity.Value, -1, 50000, null, null, (byte)state.Value, msg.IsNull ? null : msg.Value));
recordsAffected = 0;
message = null;
throw;
}
}

private T Execute<T>(ScalarExpression expression, ExpressionCompilationContext ecc, ExpressionExecutionContext eec, DataTypeReference dataType)
Expand Down
50 changes: 49 additions & 1 deletion MarkMpn.Sql4Cds.Engine/ExecutionPlan/SystemFunctionNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ public override INodeSchema GetSchema(NodeCompilationContext context)
aliases: null,
primaryKey: null,
sortOrder: null);

case SystemFunction.messages:
return new NodeSchema(
schema: new ColumnList
{
[PrefixWithAlias("message_id")] = new ColumnDefinition(DataTypeHelpers.Int, false, false),
[PrefixWithAlias("language_id")] = new ColumnDefinition(DataTypeHelpers.SmallInt, false, false),
[PrefixWithAlias("severity")] = new ColumnDefinition(DataTypeHelpers.TinyInt, false, false),
[PrefixWithAlias("is_event_logged")] = new ColumnDefinition(DataTypeHelpers.Bit, false, false),
[PrefixWithAlias("text")] = new ColumnDefinition(DataTypeHelpers.NVarChar(2048, dataSource.DefaultCollation, CollationLabel.CoercibleDefault), false, false)
},
aliases: null,
primaryKey: null,
sortOrder: null);
}

throw new NotSupportedException("Unsupported function " + SystemFunction);
Expand Down Expand Up @@ -98,6 +112,20 @@ protected override IEnumerable<Entity> ExecuteInternal(NodeExecutionContext cont
}
break;

case SystemFunction.messages:
foreach (var err in Sql4CdsError.GetAllErrors())
{
yield return new Entity
{
[PrefixWithAlias("message_id")] = (SqlInt32)err.Number,
[PrefixWithAlias("language_id")] = (SqlInt16)1033,
[PrefixWithAlias("severity")] = (SqlByte)err.Class,
[PrefixWithAlias("is_event_logged")] = SqlBoolean.False,
[PrefixWithAlias("text")] = dataSource.DefaultCollation.ToSqlString(err.Message)
};
}
break;

default:
throw new NotSupportedException("Unsupported function " + SystemFunction);
}
Expand All @@ -119,6 +147,26 @@ public override string ToString()

enum SystemFunction
{
fn_helpcollations
[SystemObjectType(SystemObjectType.Function)]
fn_helpcollations,

[SystemObjectType(SystemObjectType.View)]
messages
}

enum SystemObjectType
{
Function,
View
}

class SystemObjectTypeAttribute : Attribute
{
public SystemObjectTypeAttribute(SystemObjectType type)
{
Type = type;
}

public SystemObjectType Type { get; }
}
}
27 changes: 24 additions & 3 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using MarkMpn.Sql4Cds.Engine.ExecutionPlan;
using MarkMpn.Sql4Cds.Engine.Visitors;
Expand Down Expand Up @@ -644,8 +645,9 @@ private IRootExecutionPlanNodeInternal[] ConvertRaiseErrorStatement(RaiseErrorSt
raiserror.FirstParameter.GetType(ecc, out var msgType);

// T-SQL supports using integer values for RAISERROR but we don't have sys.messages available so require a string
if (!(msgType is SqlDataTypeReference msgSqlType) || !msgSqlType.SqlDataTypeOption.IsStringType())
throw new NotSupportedQueryFragmentException(Sql4CdsError.NotSupported(raiserror.FirstParameter, "predefined error number")) { Suggestion = "Define a message string instead of a number" };
if ((!(msgType is SqlDataTypeReference msgSqlType) || !msgSqlType.SqlDataTypeOption.IsStringType()) &&
!msgType.IsSameAs(DataTypeHelpers.Int))
throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidRaiseErrorParameterType(raiserror.FirstParameter, msgType, 1));

// Severity and State must be integers
raiserror.SecondParameter.GetType(ecc, out var severityType);
Expand Down Expand Up @@ -691,7 +693,8 @@ private IRootExecutionPlanNodeInternal[] ConvertRaiseErrorStatement(RaiseErrorSt
{
new RaiseErrorNode
{
ErrorMessage = raiserror.FirstParameter,
ErrorNumber = msgType.IsSameAs(DataTypeHelpers.Int) ? raiserror.FirstParameter : null,
ErrorMessage = msgType.IsSameAs(DataTypeHelpers.Int) ? null : raiserror.FirstParameter,
Severity = raiserror.SecondParameter,
State = raiserror.ThirdParameter,
Parameters = raiserror.OptionalParameters.ToArray()
Expand Down Expand Up @@ -4129,6 +4132,21 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe

throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(table.SchemaObject));
}
else if (table.SchemaObject.SchemaIdentifier?.Value.Equals("sys", StringComparison.OrdinalIgnoreCase) == true)
{
if (!Enum.TryParse<SystemFunction>(table.SchemaObject.BaseIdentifier.Value, true, out var systemFunction))
throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(table.SchemaObject));

if (typeof(SystemFunction).GetField(systemFunction.ToString()).GetCustomAttribute<SystemObjectTypeAttribute>().Type != SystemObjectType.View)
throw new NotSupportedQueryFragmentException(Sql4CdsError.FunctionCalledWithoutParameters(table.SchemaObject));

return new SystemFunctionNode
{
DataSource = dataSource.Name,
Alias = table.Alias?.Value ?? systemFunction.ToString(),
SystemFunction = systemFunction
};
}

if (!String.IsNullOrEmpty(table.SchemaObject.SchemaIdentifier?.Value) &&
!table.SchemaObject.SchemaIdentifier.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase) &&
Expand Down Expand Up @@ -4425,6 +4443,9 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe
if (!Enum.TryParse<SystemFunction>(tvf.SchemaObject.BaseIdentifier.Value, true, out var systemFunction))
throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(tvf.SchemaObject));

if (typeof(SystemFunction).GetField(systemFunction.ToString()).GetCustomAttribute<SystemObjectTypeAttribute>().Type != SystemObjectType.Function)
throw new NotSupportedQueryFragmentException(Sql4CdsError.NonFunctionCalledWithParameters(tvf.SchemaObject));

execute = new SystemFunctionNode
{
DataSource = dataSource.Name,
Expand Down
Loading