diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableExtRelationLonger.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableExtRelationLonger.al new file mode 100644 index 0000000..64aa29e --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableExtRelationLonger.al @@ -0,0 +1,49 @@ +table 50100 MyTable +{ + fields + { + field(1; [|MyText|]; Text[1]) + { + TableRelation = MyTable2.MyTextExt; + } + + field(2; [|MyCode|]; Text[1]) + { + TableRelation = if (MyCode = const('const')) MyTable2.MyCodeExt + else + MyTable2.MyTextExt; + } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +table 50101 MyTable2 +{ + fields + { + field(1; MyText; Text[1]) { } + + field(2; MyCode; Text[1]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +tableextension 50100 MyExtension extends MyTable2 +{ + fields + { + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationImplicitFieldPrimaryKey.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationImplicitFieldPrimaryKey.al new file mode 100644 index 0000000..149cf56 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationImplicitFieldPrimaryKey.al @@ -0,0 +1,17 @@ +table 50100 "My Table" +{ + fields + { + field(1; MyField; Code[20]) { } + + field(2; [|"My TableRelation Field"|]; Code[10]) + { + TableRelation = "My Table"; + } + } + + keys + { + key(PK; MyField) { } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationImplicitFieldPrimaryKeyWithNamespace.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationImplicitFieldPrimaryKeyWithNamespace.al new file mode 100644 index 0000000..a45cd85 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationImplicitFieldPrimaryKeyWithNamespace.al @@ -0,0 +1,22 @@ +namespace ALCops.PlatformCop.Setup; + +table 50100 "My Table" +{ + fields + { + field(1; MyField; Code[20]) { } + + field(2; [|"My TableRelation Field"|]; Code[10]) + { + TableRelation = ALCops.PlatformCop.Setup."My Other Table"; + } + } +} + +table 50101 "My Other Table" +{ + fields + { + field(1; MyField; Code[20]) { } + } +} diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationLonger.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationLonger.al new file mode 100644 index 0000000..6a575b3 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationLonger.al @@ -0,0 +1,25 @@ +table 50100 MyTable +{ + fields + { + field(1; [|MyText|]; Text[1]) + { + TableRelation = MyTable.MyTextExt; + } + + field(2; [|MyCode|]; Text[1]) + { + TableRelation = if (MyCode = const('const')) MyTable.MyCodeExt + else + MyTable.MyTextExt; + } + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationWithNamespace.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationWithNamespace.al new file mode 100644 index 0000000..8ca9917 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/HasDiagnostic/TableRelationWithNamespace.al @@ -0,0 +1,28 @@ +namespace ALCops.PlatformCop.Setup; + +table 50100 "My Table" +{ + fields + { + field(1; MyField; Code[20]) { } + + field(2; [|"My TableRelation Field"|]; Code[10]) + { + TableRelation = ALCops.PlatformCop.Setup."My Other Table"."My Field"; + } + } + + keys + { + key(PK; MyField) { } + } +} + + +table 50101 "My Other Table" +{ + fields + { + field(1; "My Field"; Code[20]) { } + } +} diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableExtRelationEqual.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableExtRelationEqual.al new file mode 100644 index 0000000..9abbfc3 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableExtRelationEqual.al @@ -0,0 +1,49 @@ +table 50100 MyTable +{ + fields + { + field(1; [|MyText|]; Text[100]) + { + TableRelation = MyTable2.MyTextExt; + } + + field(2; [|MyCode|]; Text[100]) + { + TableRelation = if (MyCode = const('const')) MyTable2.MyCodeExt + else + MyTable2.MyTextExt; + } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +table 50101 MyTable2 +{ + fields + { + field(1; MyText; Text[1]) { } + + field(2; MyCode; Text[1]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +tableextension 50100 MyExtension extends MyTable2 +{ + fields + { + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableExtRelationShorter.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableExtRelationShorter.al new file mode 100644 index 0000000..6800912 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableExtRelationShorter.al @@ -0,0 +1,48 @@ +table 50100 MyTable +{ + fields + { + field(1; [|MyText|]; Text[200]) + { + TableRelation = MyTable2.MyTextExt; + } + + field(2; [|MyCode|]; Text[200]) + { + TableRelation = if (MyCode = const('const')) MyTable2.MyCodeExt + else + MyTable2.MyTextExt; + } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + +table 50101 MyTable2 +{ + fields + { + field(1; MyText; Text[1]) { } + + field(2; MyCode; Text[1]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +tableextension 50100 MyExtension extends MyTable2 +{ + fields + { + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableRelationEqual.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableRelationEqual.al new file mode 100644 index 0000000..a473866 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableRelationEqual.al @@ -0,0 +1,25 @@ +table 50100 MyTable +{ + fields + { + field(1; [|MyText|]; Text[100]) + { + TableRelation = MyTable.MyTextExt; + } + + field(2; [|MyCode|]; Text[100]) + { + TableRelation = if (MyCode = const('const')) MyTable.MyCodeExt + else + MyTable.MyTextExt; + } + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableRelationShorter.al b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableRelationShorter.al new file mode 100644 index 0000000..e4a4c2e --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/NoDiagnostic/TableRelationShorter.al @@ -0,0 +1,25 @@ +table 50100 MyTable +{ + fields + { + field(1; [|MyText|]; Text[200]) + { + TableRelation = MyTable.MyTextExt; + } + + field(2; [|MyCode|]; Text[200]) + { + TableRelation = if (MyCode = const('const')) MyTable.MyCodeExt + else + MyTable.MyTextExt; + } + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/TableRelationFieldLength.cs b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/TableRelationFieldLength.cs new file mode 100644 index 0000000..c34e1d8 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TableRelationFieldLength/TableRelationFieldLength.cs @@ -0,0 +1,82 @@ +using ALCops.PlatformCop.CodeFixes; +using RoslynTestKit; + +namespace ALCops.PlatformCop.Test +{ + public class TableRelationFieldLength : NavCodeAnalysisBase + { + private AnalyzerTestFixture _fixture; + private static readonly Analyzers.TableRelationFieldLength _analyzer = new(); + private string _testCasePath; + + [SetUp] + public void Setup() + { + _fixture = RoslynFixtureFactory.Create(); + + _testCasePath = Path.Combine( + Directory.GetParent( + Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + Path.Combine("Rules", nameof(TableRelationFieldLength))); + } + + [Test] + [TestCase("TableRelationLonger")] + [TestCase("TableRelationImplicitFieldPrimaryKey")] + [TestCase("TableRelationImplicitFieldPrimaryKeyWithNamespace")] + [TestCase("TableExtRelationLonger")] + [TestCase("TableRelationWithNamespace")] + public async Task HasDiagnostic(string testCase) + { + SkipTestIfVersionIsTooLow( + ["TableExtRelationLonger"], + testCase, + "13.0", + "No support for tableextensions when target itself is already declared in the same module"); + + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.TableRelationFieldLength); + } + + [Test] + [TestCase("TableRelationEqual")] + [TestCase("TableRelationShorter")] + [TestCase("TableExtRelationEqual")] + [TestCase("TableExtRelationShorter")] + public async Task NoDiagnostic(string testCase) + { + SkipTestIfVersionIsTooLow( + ["TableExtRelationEqual", "TableExtRelationShorter"], + testCase, + "13.0", + "No support for tableextensions when target itself is already declared in the same module"); + + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.TableRelationFieldLength); + } + + // [Test] + // [TestCase("ReplaceSetRangeWithSetFilter")] + // [TestCase("ReplaceSetRangeWithSetFilterUsingRec")] + // public async Task HasFix(string testCase) + // { + // var currentCode = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasFix), testCase, "current.al")) + // .ConfigureAwait(false); + + // var expectedCode = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasFix), testCase, "expected.al")) + // .ConfigureAwait(false); + + // var fixture = RoslynFixtureFactory.Create( + // new CodeFixTestFixtureConfig + // { + // AdditionalAnalyzers = [_analyzer] + // }); + + // fixture.TestCodeFix(currentCode, expectedCode, DiagnosticDescriptors.TableRelationFieldLength); + // } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs b/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs index 1c98340..1914934 100644 --- a/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs +++ b/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs @@ -795,6 +795,33 @@ internal static string SetRangeWithFilterOperatorsTitle { } } + /// + /// Looks up a localized string similar to The field with table relation should have at least the same length as the referenced field.. + /// + internal static string TableRelationFieldLengthDescription { + get { + return ResourceManager.GetString("TableRelationFieldLengthDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The related field has length {0} ({1}) which is longer than the current field length {2} ({3}). + /// + internal static string TableRelationFieldLengthMessageFormat { + get { + return ResourceManager.GetString("TableRelationFieldLengthMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Table relation field length mismatch. + /// + internal static string TableRelationFieldLengthTitle { + get { + return ResourceManager.GetString("TableRelationFieldLengthTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Executing table triggers (Insert, Modify, Delete, DeleteAll, ModifyAll) or calling Validate on temporary record variables causes the temporary scope to be lost. This can result in unintended database changes because the record is no longer treated as temporary. Only execute triggers when the table itself is defined as temporary. . /// diff --git a/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx b/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx index 70c12ff..e232fad 100644 --- a/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx +++ b/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx @@ -363,6 +363,15 @@ ALCops: Replace SetRange with SetFilter + + Table relation field length mismatch + + + The related field has length {0} ({1}) which is longer than the current field length {2} ({3}) + + + The field with table relation should have at least the same length as the referenced field. + Avoid triggering table logic on temporary records diff --git a/src/ALCops.PlatformCop/Analyzers/TableRelationFieldLength.cs b/src/ALCops.PlatformCop/Analyzers/TableRelationFieldLength.cs new file mode 100644 index 0000000..5c8532d --- /dev/null +++ b/src/ALCops.PlatformCop/Analyzers/TableRelationFieldLength.cs @@ -0,0 +1,168 @@ +using System.Collections.Immutable; +using ALCops.Common.Extensions; +using ALCops.Common.Reflection; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; + +namespace ALCops.PlatformCop.Analyzers; + +[DiagnosticAnalyzer] +public sealed class TableRelationFieldLength : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.TableRelationFieldLength); + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction( + AnalyzeSymbol, + EnumProvider.SymbolKind.Field); + + private void AnalyzeSymbol(SymbolAnalysisContext ctx) + { + if (ctx.IsObsolete() || ctx.Symbol is not IFieldSymbol field) + return; + + if (!field.HasLength) + return; + + var tableRelation = field + .GetProperty(EnumProvider.PropertyKind.TableRelation) + ?.GetPropertyValueSyntax(); + + if (tableRelation is null) + return; + + AnalyzeTableRelations(ctx, field, tableRelation); + } + + private void AnalyzeTableRelations(SymbolAnalysisContext ctx, IFieldSymbol field, TableRelationPropertyValueSyntax? tableRelation) + { + while (tableRelation is not null) + { + var relatedFieldSymbol = ResolveRelatedField(ctx, tableRelation); + + if (relatedFieldSymbol is not null && ShouldReportDiagnostic(field, relatedFieldSymbol)) + { + ReportLengthMismatch(ctx, field, relatedFieldSymbol); + } + + tableRelation = tableRelation.ElseExpression?.ElseTableRelationCondition; + } + } + + private static bool ShouldReportDiagnostic(IFieldSymbol currentField, IFieldSymbol relatedField) => + relatedField.HasLength && currentField.Length < relatedField.Length; + + private static void ReportLengthMismatch(SymbolAnalysisContext ctx, IFieldSymbol currentField, IFieldSymbol relatedField) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.TableRelationFieldLength, + currentField.GetLocation(), + relatedField.Length, +#if NETSTANDARD2_1 + relatedField.ToDisplayStringWithReflection().QuoteIdentifierIfNeededWithReflection(), +#else + relatedField.ToDisplayString().QuoteIdentifierIfNeededWithReflection(), +#endif + currentField.Length, +#if NETSTANDARD2_1 + currentField.ToDisplayStringWithReflection().QuoteIdentifierIfNeededWithReflection())); +#else + currentField.ToDisplayString().QuoteIdentifierIfNeededWithReflection())); +#endif + } + + private IFieldSymbol? ResolveRelatedField(SymbolAnalysisContext ctx, TableRelationPropertyValueSyntax tableRelation) + { + return tableRelation.RelatedTableField switch + { + QualifiedNameSyntax qualifiedName => + ResolveQualifiedField(qualifiedName, ctx.Compilation), + + IdentifierNameSyntax identifierName => + ResolvePrimaryKeyField(identifierName.Identifier.ValueText?.UnquoteIdentifier(), ctx.Compilation), + + _ => null + }; + } + + private static IFieldSymbol? ResolveQualifiedField(QualifiedNameSyntax qualifiedName, Compilation compilation) + { + // Without namespaces + if (qualifiedName.Left is IdentifierNameSyntax tableNameSyntax && + qualifiedName.Right is IdentifierNameSyntax fieldNameSyntax) + { + var tableName = tableNameSyntax.GetIdentifierOrLiteralValue(); + var fieldName = fieldNameSyntax.GetIdentifierOrLiteralValue(); + + if (!string.IsNullOrEmpty(tableName) && !string.IsNullOrEmpty(fieldName)) + { + return GetFieldFromTable(tableName, fieldName, compilation) + ?? GetFieldFromTableExtension(tableName, fieldName, compilation); + } + } + + // With namespaces + if (qualifiedName.Left is QualifiedNameSyntax qualifiedNameLeft && + qualifiedName.Right is IdentifierNameSyntax qualifiedNameRight) + { + var leftIdentifier = qualifiedNameRight.GetIdentifierOrLiteralValue(); + var rightIdentifier = qualifiedNameLeft.Right.GetIdentifierOrLiteralValue(); + + if (!string.IsNullOrEmpty(rightIdentifier) && !string.IsNullOrEmpty(leftIdentifier)) + { + IFieldSymbol? field = GetFieldFromTable(rightIdentifier, leftIdentifier, compilation) + ?? GetFieldFromTableExtension(rightIdentifier, leftIdentifier, compilation); + + if (field?.ContainingNamespace?.ToString() == qualifiedNameLeft.Left.ToString()) + return field; + + // Try resolving the primary key field if previous lookup failed + IFieldSymbol? primaryKeyField = ResolvePrimaryKeyField(leftIdentifier, compilation); + if (primaryKeyField?.ContainingNamespace?.ToString() == qualifiedNameLeft.ToString()) + return primaryKeyField; + } + } + + return null; + } + + private static IFieldSymbol? ResolvePrimaryKeyField(string? tableName, Compilation compilation) + { + if (string.IsNullOrEmpty(tableName)) + return null; + +#if NETSTANDARD2_1 + var tableSymbols = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(EnumProvider.SymbolKind.Table, tableName); +#else + var tableSymbols = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModulesAndNamespaces(EnumProvider.SymbolKind.Table, tableName); +#endif + return tableSymbols.FirstOrDefault() is ITableTypeSymbol table && table.PrimaryKey.Fields.Length == 1 + ? table.PrimaryKey.Fields[0] + : null; + } + + private static IFieldSymbol? GetFieldFromTable(string tableName, string fieldName, Compilation compilation) + { +#if NETSTANDARD2_1 + var tableSymbols = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(EnumProvider.SymbolKind.Table, tableName); +#else + var tableSymbols = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModulesAndNamespaces(EnumProvider.SymbolKind.Table, tableName); +#endif + return tableSymbols.FirstOrDefault() is ITableTypeSymbol table + ? table.Fields.FirstOrDefault(f => f.Name == fieldName) + : null; + } + + private static IFieldSymbol? GetFieldFromTableExtension(string tableName, string fieldName, Compilation compilation) + { + return compilation.GetDeclaredApplicationObjectSymbols() + .OfType() + .Where(ext => ext.Target?.Name == tableName) + .SelectMany(ext => ext.AddedFields) + .FirstOrDefault(field => field.Name == fieldName); + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop/DiagnosticDescriptors.cs b/src/ALCops.PlatformCop/DiagnosticDescriptors.cs index cad4fb0..21e30f6 100644 --- a/src/ALCops.PlatformCop/DiagnosticDescriptors.cs +++ b/src/ALCops.PlatformCop/DiagnosticDescriptors.cs @@ -235,6 +235,16 @@ public static class DiagnosticDescriptors description: PlatformCopAnalyzers.SetRangeWithFilterOperatorsDescription, helpLinkUri: GetHelpUri(DiagnosticIds.SetRangeWithFilterOperators)); + public static readonly DiagnosticDescriptor TableRelationFieldLength = new( + id: DiagnosticIds.TableRelationFieldLength, + title: PlatformCopAnalyzers.TableRelationFieldLengthTitle, + messageFormat: PlatformCopAnalyzers.TableRelationFieldLengthMessageFormat, + category: Category.Design, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: PlatformCopAnalyzers.TableRelationFieldLengthDescription, + helpLinkUri: GetHelpUri(DiagnosticIds.TableRelationFieldLength)); + public static readonly DiagnosticDescriptor TemporaryRecordTriggerInvocation = new( id: DiagnosticIds.TemporaryRecordTriggerInvocation, title: PlatformCopAnalyzers.TemporaryRecordTriggerInvocationTitle, diff --git a/src/ALCops.PlatformCop/DiagnosticIds.cs b/src/ALCops.PlatformCop/DiagnosticIds.cs index f1d2671..dbf414f 100644 --- a/src/ALCops.PlatformCop/DiagnosticIds.cs +++ b/src/ALCops.PlatformCop/DiagnosticIds.cs @@ -28,4 +28,5 @@ public static class DiagnosticIds public static readonly string ODataKeyFieldsShouldUseSystemId = "PC0025"; public static readonly string MandatoryFieldMissingOnApiPage = "PC0026"; public static readonly string TemporaryRecordTriggerInvocation = "PC0027"; + public static readonly string TableRelationFieldLength = "PC0028"; } \ No newline at end of file