50 feat parse tables with relationship#51
Conversation
- Added support for parsing TableRelation syntax in AlParser. - Updated DBMLColumn to include References for foreign key lookups. - Enhanced schema post-processing to handle missing referenced tables and fields. - Created documentation for schema post-processing steps and conventions. - Added tests for TableRelation parsing and handling in various scenarios.
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end support for AL TableRelation parsing so relationships can be emitted in the generated DBML, including schema post-processing to keep references valid even when referenced tables/fields are missing (common with tableextension-only inputs).
Changes:
- Parse
TableRelationfrom field bodies and store it inDBMLColumn.References([tableName, fieldName]with"UnknownField"sentinel when unspecified). - Extend schema post-processing to (1) drop empty tables, (2) create stub tables for missing referenced tables, and (3) add missing referenced fields to existing tables before resolving
"UnknownField"references. - Add/extend tests, fixtures, and documentation for the new relationship parsing + post-processing pipeline.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/AL2DBML.Parser/AlParser.cs | Extracts TableRelation into DBMLColumn.References during field parsing. |
| src/DBMLWriter/SchemaPostProcessor.cs | Adds pipeline steps to remove empty tables, create stubs for missing referenced tables, and add missing referenced fields. |
| src/AL2DBML.Tests/Parser/TableParserTests.cs | Adds tests asserting TableRelation parsing across supported/unsupported syntaxes. |
| src/AL2DBML.Tests/Writer/WriterTests.cs | Adds tests for new schema post-processing behaviors affecting output DBML. |
| src/AL2DBML.Tests/Fixtures/Tables/PurchaseHeader.al | Adds TableRelation examples used by parser tests. |
| src/AL2DBML.Tests/Fixtures/Tables/Customer.al | Adds TableRelation example used by parser tests. |
| docs/SCHEMA_POST_PROCESSING.md | Documents the schema post-processing pipeline and conventions (incl. "UnknownField"). |
| docs/AL_PARSING.md | Documents TableRelation parsing behavior, supported syntaxes, and limitations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| var stubFields = explicitFields.Count > 0 | ||
| ? explicitFields | ||
| : [new DBMLColumn { Name = Unknown, Type = group.First().Type, IsPrimaryKey = true }]; | ||
|
|
There was a problem hiding this comment.
CreateStubsForMissingReferencedTables: when at least one explicit referenced field exists, the stub omits UnknownField entirely. If the same missing table has any UnknownField references that cannot be resolved later (e.g., the stub ends up with multiple [pk] fields so it won’t be in singlePkByTable), the generated DBML can contain ref: > <Table>.UnknownField pointing to a column that doesn’t exist in the stub table. Consider ensuring the stub always contains an UnknownField column when there are any unresolved/unknown references for that table (or avoid emitting unresolved UnknownField refs).
| var stubFields = explicitFields.Count > 0 | |
| ? explicitFields | |
| : [new DBMLColumn { Name = Unknown, Type = group.First().Type, IsPrimaryKey = true }]; | |
| var hasUnknownReferences = group.Any(f => string.IsNullOrEmpty(f.References![1]) || string.Equals(f.References![1], Unknown, StringComparison.OrdinalIgnoreCase)); | |
| var stubFields = explicitFields.Count > 0 | |
| ? explicitFields | |
| : [new DBMLColumn { Name = Unknown, Type = group.First().Type, IsPrimaryKey = true }]; | |
| if (hasUnknownReferences && explicitFields.Count > 0 | |
| && !stubFields.Any(f => string.Equals(f.Name, Unknown, StringComparison.OrdinalIgnoreCase))) | |
| { | |
| stubFields.Add(new DBMLColumn { Name = Unknown, Type = group.First().Type, IsPrimaryKey = false }); | |
| } |
| && !string.IsNullOrEmpty(refs[0]) | ||
| && !string.IsNullOrEmpty(refs[1]) | ||
| && !string.Equals(refs[1], Unknown, StringComparison.OrdinalIgnoreCase) | ||
| && tablesByName.ContainsKey(refs[0])); |
There was a problem hiding this comment.
explicitRefs is a deferred LINQ query over schema.Tables.SelectMany(t => t.Fields). The subsequent loop mutates targetTable.Fields (inserts new columns), which can invalidate the underlying List<T> enumerator if the referenced table is the same table currently being enumerated (self-referencing TableRelation), causing InvalidOperationException: Collection was modified. Materialize explicitRefs (e.g., ToList()) before the loop, or collect pending field additions and apply them after enumeration.
| && tablesByName.ContainsKey(refs[0])); | |
| && tablesByName.ContainsKey(refs[0])) | |
| .ToList(); |
| | `TableRelation = "Salesperson/Purchaser"."Code";` | `["Salesperson/Purchaser", "Code"]` | | ||
| | `TableRelation = "Salesperson/Purchaser".Code;` | `["Salesperson/Purchaser", "Code"]` | | ||
|
|
||
| When no field is specified, the sentinel value `"UnknownField"` is used. This is resolved at post-processing time (see ARCHITECTURE.md — Schema post-processing). |
There was a problem hiding this comment.
This references “ARCHITECTURE.md — Schema post-processing”, but docs/ARCHITECTURE.md currently doesn’t contain a matching section/header (and the detailed write-up is in SCHEMA_POST_PROCESSING.md). Update the link/wording to point at the correct document/anchor or add the referenced section to ARCHITECTURE.md.
| When no field is specified, the sentinel value `"UnknownField"` is used. This is resolved at post-processing time (see ARCHITECTURE.md — Schema post-processing). | |
| When no field is specified, the sentinel value `"UnknownField"` is used. This is resolved at post-processing time (see `SCHEMA_POST_PROCESSING.md`). |
Description
Closes
closes #50 #49