-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Enhance AL Parser with Table and Field Parsing Capabilities #25
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,11 @@ | ||||||
| namespace AL2DBML.Core.Models; | ||||||
|
|
||||||
| public class DBMLColumn | ||||||
| { | ||||||
| public string Name { get; set; } = string.Empty; | ||||||
| public string Type { get; set; } = string.Empty; | ||||||
| public bool IsPrimaryKey { get; set; } = false; | ||||||
| public string[]? References { get; set; } = new string[2]; | ||||||
| public bool IsFlowfield { get; set; } = false; | ||||||
|
||||||
| public bool IsFlowfield { get; set; } = false; | |
| public bool IsFlowField { get; set; } = false; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,3 @@ | ||
| using System; | ||
|
|
||
| namespace AL2DBML.Core.Models; | ||
|
|
||
| public class DBMLEnum | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| namespace AL2DBML.Core.Models; | ||
|
|
||
| public class DBMLTable | ||
| { | ||
| public string Name { get; set; } = string.Empty; | ||
| public List<DBMLColumn> Fields { get; set; } = []; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,7 @@ | ||
| using System; | ||
|
|
||
| namespace AL2DBML.Core.Models; | ||
|
|
||
| public class OutputSchema | ||
| { | ||
| public List<DBMLEnum> Enums { get; set; } = []; | ||
| public List<DBMLTable> Tables { get; set; } = []; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using AL2DBML.Application.Interfaces; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using AL2DBML.Core.Enums; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using AL2DBML.Core.Models; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using AL2DBML.Parser.Helpers; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace AL2DBML.Parser; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -14,6 +15,8 @@ private static class Patterns | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public const string EnumValue = @"^\s*value\(\s*\d+;\s*(""[^""]+""|\w+)"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public const string Table = @"^\s*table\s+\d+\s+(""[^""]+""|\w+)"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public const string TableExtension = @"^\s*tableextension\s+\d+\s+(""[^""]+""|\w+)\s+extends\s+(""[^""]+""|\w+)"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public const string Field = @"^\s*field\s*\(\s*(\d+);\s*(""[^""]+""|\w+)\s*;\s*(Enum)?\s*(""[^""]+""|\w+(\[\d+\])?)\s*\)\s*{([^{}]*)}"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public const string Key = @"^\s*key\s*\(\s*(""[^""]+""|\w+)\s*;\s*([^)]*)\s*\)"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly OutputSchema _outputSchema = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -53,4 +56,122 @@ private DBMLEnum GetOrCreateEnum(string name, out bool isNew) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isNew = existing is null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return existing ?? new DBMLEnum { Name = name, Values = [] }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public DBMLTable ParseTable(string alTableFileContent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var name = AlSyntaxHelper.ExtractMatch(alTableFileContent, Patterns.Table); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var dbmlTable = GetOrCreateTable(name, out bool isNew); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var primaryKeys = GetPrimaryKeys(alTableFileContent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ParseFields(alTableFileContent, dbmlTable, primaryKeys); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isNew) _outputSchema.Tables.Add(dbmlTable); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return dbmlTable; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public DBMLTable ParseTableExtension(string alTableExtensionFileContent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var tableName = AlSyntaxHelper.ExtractMatch(alTableExtensionFileContent, Patterns.TableExtension, 2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var dbmlTable = GetOrCreateTable(tableName, out bool isNew); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var primaryKeys = GetPrimaryKeys(alTableExtensionFileContent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ParseFields(alTableExtensionFileContent, dbmlTable, primaryKeys); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isNew) _outputSchema.Tables.Add(dbmlTable); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return dbmlTable; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private List<string> GetPrimaryKeys(string content) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var keyMatches = Regex.Matches(content, Patterns.Key, RegexOptions.Multiline); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (Match keyMatch in keyMatches) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (keyMatch.Success) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var keyName = AlSyntaxHelper.CleanName(keyMatch.Groups[1].Value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var keyFields = keyMatch.Groups[2].Value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if this is the primary key by looking for "PK" or "Clustered = true" after it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (keyName.Equals("PK", StringComparison.OrdinalIgnoreCase) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content.Substring(keyMatch.Index).Contains("Clustered = true")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return keyFields.Split(',') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Select(k => AlSyntaxHelper.CleanName(k.Trim())) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ToList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+88
to
+103
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (Match keyMatch in keyMatches) | |
| { | |
| if (keyMatch.Success) | |
| { | |
| var keyName = AlSyntaxHelper.CleanName(keyMatch.Groups[1].Value); | |
| var keyFields = keyMatch.Groups[2].Value; | |
| // Check if this is the primary key by looking for "PK" or "Clustered = true" after it | |
| if (keyName.Equals("PK", StringComparison.OrdinalIgnoreCase) || | |
| content.Substring(keyMatch.Index).Contains("Clustered = true")) | |
| { | |
| return keyFields.Split(',') | |
| .Select(k => AlSyntaxHelper.CleanName(k.Trim())) | |
| .ToList(); | |
| } | |
| } | |
| } | |
| // First, prefer a key explicitly named "PK" | |
| foreach (Match keyMatch in keyMatches) | |
| { | |
| if (!keyMatch.Success) | |
| continue; | |
| var keyName = AlSyntaxHelper.CleanName(keyMatch.Groups[1].Value); | |
| if (!keyName.Equals("PK", StringComparison.OrdinalIgnoreCase)) | |
| continue; | |
| var keyFields = keyMatch.Groups[2].Value; | |
| return keyFields.Split(',') | |
| .Select(k => AlSyntaxHelper.CleanName(k.Trim())) | |
| .ToList(); | |
| } | |
| // If no "PK" key is found, look for a clustered key within each key block | |
| foreach (Match keyMatch in keyMatches) | |
| { | |
| if (!keyMatch.Success) | |
| continue; | |
| var keyDefinition = keyMatch.Value; | |
| if (!keyDefinition.Contains("Clustered = true", StringComparison.OrdinalIgnoreCase)) | |
| continue; | |
| var keyFields = keyMatch.Groups[2].Value; | |
| return keyFields.Split(',') | |
| .Select(k => AlSyntaxHelper.CleanName(k.Trim())) | |
| .ToList(); | |
| } |
Copilot
AI
Mar 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ParseFields does an O(n) lookup (FirstOrDefault) for every parsed field, which can become O(n^2) for large tables. Also primaryKeys.Contains(columnName) is a linear, case-sensitive search. Consider building a Dictionary<string, DBMLColumn> (or using table.Fields keyed by name) and converting primaryKeys to a HashSet<string> with StringComparer.OrdinalIgnoreCase to avoid performance issues and case-sensitivity surprises.
Copilot
AI
Mar 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ParseField is a new public API on IAlParser but doesn’t appear to have any unit coverage (unlike ParseTable/ParseTableExtension). Adding a focused test for a representative field (including an enum type and a FlowField/CalcFormula case) would help prevent regressions in the regex and group indexing.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| enumextension 50003 "Gen. Journal Document Type Ext" extends "Gen. Journal Document Type" | ||
| { | ||
| value(10; "Finance Charge Memo") | ||
| { | ||
| Caption = 'Finance Charge Memo'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| enumextension 50002 "Salesperson/Purchaser Type Ext" extends "Salesperson/Purchaser Type" | ||
| { | ||
| value(10; "Agency") | ||
| { | ||
| Caption = 'Agency'; | ||
| } | ||
| value(11; "Freelance") | ||
| { | ||
| Caption = 'Freelance'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| enum 50002 "Gen. Journal Document Type" | ||
| { | ||
| Caption = 'Gen. Journal Document Type'; | ||
|
|
||
| value(0; " ") | ||
| { | ||
| Caption = ' '; | ||
| } | ||
| value(1; "Payment") | ||
| { | ||
| Caption = 'Payment'; | ||
| } | ||
| value(2; "Invoice") | ||
| { | ||
| Caption = 'Invoice'; | ||
| } | ||
| value(3; "Credit Memo") | ||
| { | ||
| Caption = 'Credit Memo'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| enum 50003 "Payment Terms Calc. Type" implements "IPaymentCalc" | ||
| { | ||
| Caption = 'Payment Terms Calc. Type'; | ||
|
|
||
| value(0; " ") | ||
| { | ||
| Caption = ' '; | ||
| } | ||
| value(1; "Fixed Day") | ||
| { | ||
| Caption = 'Fixed Day'; | ||
| } | ||
| value(2; "Current Month") | ||
| { | ||
| Caption = 'Current Month'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| enum 50001 "Salesperson/Purchaser Type" | ||
| { | ||
| Caption = 'Salesperson/Purchaser Type'; | ||
|
|
||
| value(0; " ") | ||
| { | ||
| Caption = ' '; | ||
| } | ||
| value(1; "Internal") | ||
| { | ||
| Caption = 'Internal'; | ||
| } | ||
| value(2; "External") | ||
| { | ||
| Caption = 'External'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| enum 50004 "VAT Business Posting Group" implements "IVATPostable", "IPostingGroup" | ||
| { | ||
| Caption = 'VAT Business Posting Group'; | ||
|
|
||
| value(0; " ") | ||
| { | ||
| Caption = ' '; | ||
| } | ||
| value(1; "Domestic") | ||
| { | ||
| Caption = 'Domestic'; | ||
| } | ||
| value(2; "EU") | ||
| { | ||
| Caption = 'EU'; | ||
| } | ||
| value(3; "Export") | ||
| { | ||
| Caption = 'Export'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| tableextension 50000 "Customer Extension" extends "Customer" | ||
| { | ||
| fields | ||
| { | ||
| field(50001; "Customer Category"; Code[20]) | ||
| { | ||
| Caption = 'Customer Category'; | ||
| ToolTip = 'Specifies the customer category for segmentation'; | ||
| } | ||
| field(50002; "Tax ID"; Text[20]) | ||
| { | ||
| Caption = 'Tax ID'; | ||
| ToolTip = 'Specifies the customer tax identification number'; | ||
| } | ||
| field(50003; "Preferred Contact Method"; Option) | ||
| { | ||
| Caption = 'Preferred Contact Method'; | ||
| OptionMembers = Email,Phone,Mail,Other; | ||
| ToolTip = 'Specifies the preferred method to contact the customer'; | ||
| } | ||
| field(50004; "InvoiceAmount"; Decimal) | ||
| { | ||
| Caption = 'Invoice Amount'; | ||
| FieldClass = FlowField; | ||
| CalcFormula = sum("Customer Ledger Entry"."Amount" where("Customer No." = field("No."), "Document Type" = const(Invoice))); | ||
| } | ||
| } | ||
|
|
||
| trigger OnAfterInsert() | ||
| begin | ||
| if "Customer Category" = '' then | ||
| "Customer Category" := 'GENERAL'; | ||
| end; | ||
|
|
||
| local procedure ValidateTaxID() | ||
| begin | ||
| if "Tax ID" <> '' and StrLen("Tax ID") < 5 then | ||
| Error('Tax ID must be at least 5 characters'); | ||
| end; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| tableextension 50003 "Gen. Journal Line Extension" extends "Gen. Journal Line" | ||
| { | ||
| fields | ||
| { | ||
| field(50001; "Project Code"; Code[20]) | ||
| { | ||
| Caption = 'Project Code'; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| tableextension 50002 "Purchase Header Extension" extends "Purchase Header" | ||
| { | ||
| fields | ||
| { | ||
| field(50001; "Custom Reference No."; Code[30]) | ||
| { | ||
| Caption = 'Custom Reference No.'; | ||
| } | ||
| field(50002; "Approval Status"; Enum "Approval Status") | ||
| { | ||
| Caption = 'Approval Status'; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| tableextension 50001 "Salesperson/Purchaser Extension" extends "Salesperson/Purchaser" | ||
| { | ||
| fields | ||
| { | ||
| field(50001; "Phone No."; Text[30]) | ||
| { | ||
| Caption = 'Phone No.'; | ||
| } | ||
| field(50002; "Global Dimension 1 Code"; Code[20]) | ||
| { | ||
| Caption = 'Global Dimension 1 Code'; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referencesis declared nullable (string[]?) but is initialized tonew string[2], which yields an array containing two null elements. This is a surprising default and can lead to null entries at runtime. Either make it non-nullable and initialize with meaningful values, or default it tonull/Array.Empty<string>()until references are actually populated.