Skip to content

feat(data): merge several declaration folders into one model - #191

Draft
david-hudec-networg wants to merge 8 commits into
TALXIS:masterfrom
david-hudec-networg:feat/data-model-convert-multiple-inputs
Draft

feat(data): merge several declaration folders into one model#191
david-hudec-networg wants to merge 8 commits into
TALXIS:masterfrom
david-hudec-networg:feat/data-model-convert-multiple-inputs

Conversation

@david-hudec-networg

Copy link
Copy Markdown

Stacked on #190. The first four commits are that PR's defect fixes; review and merge it first. Only the last commit belongs to this one. GitHub cannot express a cross-fork stack, so the base has to be master.

The problem

txc data model convert converts one solution's declarations. A delivery project's data model is never one solution: the product ships several modules that each declare part of a shared table, and the project layers its own on top. In one real project, account draws attributes from five different modules.

Converting each folder separately and concatenating the results does not work — each output declares the same table, so a naive merge keeps one and silently drops the rest. Producing a usable diagram therefore meant 10–25 separate exports and a merge by hand.

The change

--input becomes repeatable:

txc data model convert -i ./src/Modules.Core/Model/Declarations \
                       -i ../TALXIS/src/Areas/Service/Project/Model/Declarations \
                       --target dbml

The seam already existed. ParseModel(List<string>) builds one Module per zip and calls ParseModules once; folders never got the same treatment. This extracts folder→Module and zip→Module and routes both through it, so inputs can be mixed — folders, project files and .zip in one invocation.

Merge policy

Where two inputs declare the same attribute:

  • First input wins, so the result is deterministic in the order the caller gave, and the caller controls that order. Documented in the option's own description.
  • A divergent type warns rather than aborting. Several modules extending one shared table is the normal case for a layered product, not an error worth failing a conversion over.
  • Text lengths widen, never narrow — a downstream consumer breaks on too little room, not too much.

This also fixes a latent bug: Table.ParseMultipleRowsFromXml appended rows with no dedup by name. Harmless with a single input; routine with several.

Two things that fell out

Modules are now named after the folders that own their declarations, so a merged diagram attributes each table to its source instead of rendering an empty // comment.

Module.Colorhex became a computed property. Assigned in the constructor, it was evaluated before an object initializer had set ModuleName — so every module came out the same colour, which is invisible with one input and obvious with ten.

Back-compat

The single --input form, the no-input default to the working directory, and the existing ConvertModel(string, …) / ParseModelFolder(string) signatures all behave as before — the latter two are kept as thin wrappers. Verified across three solutions × five targets (dbml, sql, plainsql, edmx, ribbon): all exit 0 and are idempotent by hash.

Measured

The ten declaration folders behind one real project, in a single invocation:

time 1.7s
tables / refs 211 / 408
columns / enums 5005 / 577
distinct module colours 11
@dbml/core PARSE_OK
merge warnings 0

Tests

tests/TALXIS.CLI.Tests/Data/DataModelConverter/MultipleInputMergeTests.cs — seven tests over in-memory Modules and temp folders, no new dependencies: two modules merge into one table with both columns; a repeated attribute produces one column; a conflicting type keeps the first; lengths widen regardless of input order; modules colour apart; and one folder through the list entry point matches the single-folder entry point.

🤖 Generated with Claude Code

david-hudec-networg and others added 8 commits September 1, 2026 15:15
A table with several lookups to one target rendered a single edge, while all
its lookup columns still appeared - understating the model without looking
broken.

The duplicate guard keyed on (LeftSideTable, RighSideTable), ignoring which
column the relationship ran through.

The key now includes LeftSideRow. Genuine duplicates still collapse, which is
what the guard is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Required, form-visible columns disappeared from the output with no warning, and
a module whose only contribution was such an attribute read as contributing
nothing.

Rows of an optionset kind were deleted outright when their OptionSetName did not
resolve. Three causes seen in real solutions: the global option set declares
<options />, it is declared in a different module, or it is platform-owned.

The row is kept and only OptionSetName is cleared. That is what
ToDbDiagramNotation prefers over RowType, so leaving it set would reference an
Enum that was never emitted; RowType is left alone so sql and edmx keep their
own handling for the kind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same unchanged solution converted to a different file on every run, at
identical length - so a generated diagram could not be committed, diffed, or
compared across a model change.

Two causes. Module seeded Colorhex from new Random(). And three file
enumerations used Directory.GetFiles, which guarantees no ordering, so table,
relationship and enum order followed the filesystem.

Colour now derives from the module name with FNV-1a - not string.GetHashCode,
which is randomised per process on .NET Core - and all three enumerations are
ordered ordinally. Everyone's colours change; nothing could have depended on
the old values.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Where an entity has a many-to-many with itself, the intersect table carried the
same column twice, the same Ref twice, and the same EDMX navigation property
twice. A DBML parser rejects the first two outright.

Both sides resolved to <entity>id, and both legs carried the relationship name.

The second column and the second leg's name are suffixed positionally. The real
per-side names live in metadata (Entity1/Entity2IntersectAttribute) and are
author-chosen - the platform's own example pairs connectionroleid with
associatedconnectionroleid - so they cannot be derived from solution XML and are
not guessed at here.

Known limit: on the entity side EDMX still names the navigation property after
the primary key row, so one duplicate remains there. The intersect side is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rejected

Every plainsql conversion failed, and the error listed the formats it did
support - contradicting the option's own help.

The format is declared in the option's AllowedValues and fully implemented in
the conversion switch, but was missing from the service's SupportedFormats
guard three lines earlier.

Added. A test now asserts every value the option advertises actually converts,
so the two lists cannot drift apart again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…once

A table declared by two modules ended up with the same column listed twice.

ParseMultipleRowsFromXml appended every parsed row without checking whether the
table already carried one of that name. Harmless while only one input could be
given; routine as soon as several can.

Rows are matched case-insensitively. Where two declarations disagree the first
input wins, so the result is deterministic in the order the caller gave; a
differing type warns rather than aborting, because several modules extending one
shared table is normal for a layered product; and text lengths widen but never
narrow, since a consumer breaks on too little room, not too much.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onstructor

Every module would come out the same colour once modules carry distinct names.

Colorhex was assigned in the constructor, which runs before an object
initializer sets ModuleName - so the colour was derived from an empty name.
Invisible while there was only ever one module.

Colorhex is now a computed property, so it always reflects the name in effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A delivery project's model is spread across the modules a product ships plus the
project's own layer, and several of them declare part of the same table.
Converting each separately and concatenating the output keeps only the first
declaration of each table, so the merge had to be done by hand.

--input accepted a single path, and only zip inputs were ever built into more
than one Module.

--input is now repeatable. Folder and zip inputs both resolve to a Module and go
through the ParseModules seam that already existed for zips, so the two can be
mixed in one invocation. Modules are named after the folders that own their
declarations, so a merged diagram attributes each table to its source instead of
rendering an empty comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant