Skip to content

Commit

Permalink
Allow using RAISERROR with predefined error numbers
Browse files Browse the repository at this point in the history
Allow using sys.messages view to list available messages
  • Loading branch information
MarkMpn committed Apr 12, 2024
1 parent 5c688bc commit 2a3bcbb
Show file tree
Hide file tree
Showing 5 changed files with 16,443 additions and 121 deletions.
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
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

0 comments on commit 2a3bcbb

Please sign in to comment.