Skip to content

feat: Enhance AL Parser with Table and Field Parsing Capabilities#25

Merged
OGR-67 merged 1 commit into
devfrom
13-test-parsing-table
Mar 19, 2026
Merged

feat: Enhance AL Parser with Table and Field Parsing Capabilities#25
OGR-67 merged 1 commit into
devfrom
13-test-parsing-table

Conversation

@OGR-67

@OGR-67 OGR-67 commented Mar 19, 2026

Copy link
Copy Markdown
Owner

Description

  • Added methods to IAlParser for parsing tables and fields.
  • Introduced DBMLTable and DBMLColumn models to represent parsed structures.
  • Implemented parsing logic for tables and table extensions in AlParser.
  • Enhanced OutputSchema to include a list of tables.
  • Added unit tests for table parsing and extensions, ensuring correct functionality.
  • Created fixture files for various tables and table extensions to support testing.

Closes

closes #13
closes #7
closes #14

- Added methods to IAlParser for parsing tables and fields.
- Introduced DBMLTable and DBMLColumn models to represent parsed structures.
- Implemented parsing logic for tables and table extensions in AlParser.
- Enhanced OutputSchema to include a list of tables.
- Added unit tests for table parsing and extensions, ensuring correct functionality.
- Created fixture files for various tables and table extensions to support testing.
Copilot AI review requested due to automatic review settings March 19, 2026 20:30
@OGR-67 OGR-67 merged commit 9cf6325 into dev Mar 19, 2026
2 checks passed
@OGR-67 OGR-67 deleted the 13-test-parsing-table branch March 19, 2026 20:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds table/table-extension parsing to the AL parser so parsed schemas can represent tables and their columns (including primary keys and FlowFields), alongside existing enum support.

Changes:

  • Extend IAlParser/AlParser with table, table-extension, and field parsing methods.
  • Introduce DBMLTable/DBMLColumn models and expand OutputSchema to track parsed tables.
  • Add fixtures and unit tests covering table parsing, key detection, and extension merges (including names with slashes/dots).

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/AL2DBML.Tests/TestBase.cs Adds shared fixture loading and parser resolution for tests.
src/AL2DBML.Tests/Parser/TableParserTests.cs New unit tests for table and tableextension parsing/merging.
src/AL2DBML.Tests/Parser/EnumParserTests.cs Refactors to use TestBase and adds more enum/enumextension coverage.
src/AL2DBML.Tests/Fixtures/Tables/Customer.al Adds table fixture used by parsing tests.
src/AL2DBML.Tests/Fixtures/Tables/SalespersonPurchaser.al Adds table fixture to validate slash handling.
src/AL2DBML.Tests/Fixtures/Tables/PurchaseHeader.al Adds table fixture to validate composite PK handling.
src/AL2DBML.Tests/Fixtures/Tables/GenJournalLine.al Adds table fixture to validate dotted names and composite PKs.
src/AL2DBML.Tests/Fixtures/TableExts/CustomerExtension.al Adds tableextension fixture used for merge tests.
src/AL2DBML.Tests/Fixtures/TableExts/SalespersonPurchaserExtension.al Adds tableextension fixture used for slash-name merge tests.
src/AL2DBML.Tests/Fixtures/TableExts/PurchaseHeaderExtension.al Adds tableextension fixture used for enum-field addition tests.
src/AL2DBML.Tests/Fixtures/TableExts/GenJournalLineExtension.al Adds tableextension fixture used for dotted-name merge tests.
src/AL2DBML.Tests/Fixtures/Enums/SalespersonPurchaserType.al Adds enum fixture for slash-name parsing tests.
src/AL2DBML.Tests/Fixtures/Enums/GenJournalDocumentType.al Adds enum fixture for dotted-name parsing tests.
src/AL2DBML.Tests/Fixtures/Enums/PaymentTermsCalcType.al Adds enum fixture for implements parsing tests.
src/AL2DBML.Tests/Fixtures/Enums/VATBusinessPostingGroup.al Adds enum fixture for multi-implements parsing tests.
src/AL2DBML.Tests/Fixtures/EnumExts/SalespersonPurchaserTypeExtension.al Adds enumextension fixture for merge tests.
src/AL2DBML.Tests/Fixtures/EnumExts/GenJournalDocumentTypeExtension.al Adds enumextension fixture for merge tests.
src/AL2DBML.Parser/Helpers/AlSyntaxHelper.cs Moves helper into AL2DBML.Parser.Helpers namespace.
src/AL2DBML.Parser/AlParser.cs Implements table/tableextension/field parsing and PK/FlowField extraction.
src/AL2DBML.Core/Models/OutputSchema.cs Adds Tables collection to the schema model.
src/AL2DBML.Core/Models/DBMLTable.cs New model representing a parsed table.
src/AL2DBML.Core/Models/DBMLColumn.cs New model representing a parsed table column/field.
src/AL2DBML.Core/Models/DBMLEnum.cs Removes unused using lines (no behavior change).
src/AL2DBML.Application/Interfaces/IAlParser.cs Extends parser interface with table/extension/field APIs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +107 to +128
private void ParseFields(string content, DBMLTable table, List<string> primaryKeys)
{
var fieldMatches = Regex.Matches(content, Patterns.Field, RegexOptions.Multiline);

foreach (Match fieldMatch in fieldMatches)
{
var columnName = AlSyntaxHelper.CleanName(fieldMatch.Groups[2].Value);
var columnType = AlSyntaxHelper.CleanName(fieldMatch.Groups[4].Value);
var fieldBody = fieldMatch.Groups[6].Value;

var isFlowField = fieldBody.Contains("FieldClass") && fieldBody.Contains("FlowField");
var calcFormula = ExtractCalcFormula(fieldBody);

var existingColumn = table.Fields.FirstOrDefault(c => c.Name == columnName);
if (existingColumn == null)
{
table.Fields.Add(new DBMLColumn
{
Name = columnName,
Type = columnType,
IsPrimaryKey = primaryKeys.Contains(columnName),
IsFlowfield = isFlowField,

Copilot AI Mar 19, 2026

Copy link

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 uses AI. Check for mistakes.
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];

Copilot AI Mar 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

References is declared nullable (string[]?) but is initialized to new 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 to null/Array.Empty<string>() until references are actually populated.

Suggested change
public string[]? References { get; set; } = new string[2];
public string[]? References { get; set; } = System.Array.Empty<string>();

Copilot uses AI. Check for mistakes.
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;

Copilot AI Mar 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Property name IsFlowfield is inconsistently cased compared to IsPrimaryKey and the AL term FlowField. Renaming to IsFlowField would better align with standard C# PascalCase word boundaries and reduce confusion for consumers of the model.

Suggested change
public bool IsFlowfield { get; set; } = false;
public bool IsFlowField { get; set; } = false;

Copilot uses AI. Check for mistakes.
// Embed the fixture files so it's part of the assembly and can be loaded easily
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"AL2DBML.Tests.Fixtures.{path.Replace("/", ".")}";
using var stream = assembly.GetManifestResourceStream(resourceName)!;

Copilot AI Mar 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadFixture uses the null-forgiving operator on GetManifestResourceStream(resourceName). If the resource name is wrong or the file isn't embedded, this becomes a NullReferenceException without context. Consider checking for null and throwing an exception that includes resourceName (and maybe listing available resources) to make test failures actionable.

Suggested change
using var stream = assembly.GetManifestResourceStream(resourceName)!;
var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
var availableResources = string.Join(", ", assembly.GetManifestResourceNames());
throw new InvalidOperationException(
$"Embedded fixture resource '{resourceName}' was not found. " +
$"Available resources: {availableResources}");
}

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +175
public DBMLColumn ParseField(string alFieldContent)
{
var fieldMatch = Regex.Match(alFieldContent, Patterns.Field, RegexOptions.Multiline);

if (!fieldMatch.Success)
throw new FormatException("Field pattern not found or invalid");

var columnName = AlSyntaxHelper.CleanName(fieldMatch.Groups[2].Value);
var columnType = AlSyntaxHelper.CleanName(fieldMatch.Groups[4].Value);
var fieldBody = fieldMatch.Groups[6].Value;

var isFlowField = fieldBody.Contains("FieldClass") && fieldBody.Contains("FlowField");
var calcFormula = ExtractCalcFormula(fieldBody);

return new DBMLColumn
{
Name = columnName,
Type = columnType,
IsFlowfield = isFlowField,
CalcFormula = calcFormula
};

Copilot AI Mar 19, 2026

Copy link

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.

Copilot uses AI. Check for mistakes.
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();
}
}
}

Copilot AI Mar 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primary key detection uses content.Substring(keyMatch.Index).Contains("Clustered = true"), which scans the entire remainder of the file. If a non-PK key appears before a clustered PK (or any later clustered key), this will incorrectly treat the earlier key as the primary key and return the wrong field list. Consider (1) preferring keyName == "PK" first, and (2) limiting the Clustered = true check to the current key block (e.g., search only until the next key( match or end of the keys section).

Suggested change
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 uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants