Dataverse normalization · migration plan

Untangling the Asset Register mega-table

A single Dataverse table, pathix_assetregister, currently stores four distinct entities discriminated by a string field, held together by six plugins that hand-roll the relational machinery the platform provides natively. This plan splits it into proper related tables and retires most of that code.

scan a09a7336…de2f scanned 2026-06-08 environment 5a25d5a0…11ad behavioral confidence High

01Executive summary

The asset register is not one table needing a few lookups — it is a mega-table. The discriminator column pathix_recordtype takes at least four values, each a separate business entity: Location, AssetType, Asset, and SubAsset. Records are linked not by relationships but by three string columns matched at runtime via FetchXML: pathix_locationcode, pathix_typename, and pathix_parentassettag.

Six custom plugins compensate for the missing structure. Five of them exist only because the data is denormalized — give the entities real tables and lookups, and those plugins have nothing left to do. The recommended end state is four core tables plus the already-existing (but denormalized) maintenance log, with native lookups, rollups, choice columns, and cascade behaviour replacing custom code.

Data-integrity caveat — read before migrating

Behavioral analysis (High confidence) shows that five of the six plugins silently swallow all exceptions via empty catch blocks; only MegaTableValidator throws. That means existing rollups, copied attributes, generated tags, cascaded statuses, and stamped maintenance dates may already be wrong in production wherever a plugin failed unnoticed. Do not trust-migrate these values — recompute them during the move.

02Current state

The register carries ~25 business columns (plus the usual system fields). What look like attributes of one record are really attributes of four different things flattened together:

Latent entities inside pathix_assetregister, by pathix_recordtype
recordtypeReally isEvidence
'Location'A physical locationStatusCascade pushes a Location's status to its assets; MegaTableValidator validates assets against existing Location rows
'AssetType'An asset type / modelMegaTableValidator requires manufacturer; TypeAttributeCopyDown reads type attributes off it
'Asset'A top-level assetTag generation, validation, rollup parent
'SubAsset'A child assetSubAssetRollup aggregates them onto the parent

The pathix_maintenancelog table already exists as a separate child table — but it is itself denormalized, carrying four "snapshot copy" columns and joining to assets by the pathix_assettag string rather than a real lookup.

03Target schema

Explode the mega-table into the four latent entities, repair the existing maintenance log, and let the platform enforce the relationships. Asset and SubAsset collapse into one table — the difference is simply whether parentassetid is set, so pathix_recordtype and pathix_issubasset both disappear.

Locationfrom recordtype = 'Location'
locationidPKsurrogate key
locationcodealt keybusiness key, preserved for integrations
site / building / floormoved off the asset; they describe the location, not the asset
parentlocationidlookup → Locationreplaces the parentlocationcode string
locationpathderivedcomputed from the hierarchy, not stored by hand
statuschoice; cascades to assets via relationship behaviour
AssetTypefrom recordtype = 'AssetType'
assettypeidPKsurrogate key
typenamealt keybusiness key used by tag generation
manufacturerrequired (was enforced by MegaTableValidator)
modelcanonical home — no longer copied onto each asset
expectedlifeyearstype-level attribute
maintenanceintervaldaystype-level maintenance cadence
Assetfrom recordtype = 'Asset' + 'SubAsset'
assetidPKsurrogate key
assettagalt keygenerated by AssetCodeGenerator (retained)
name / serialnumber / installdate / currentvalue / componentpositiongenuine per-instance fields — stay
assettypeidlookup → AssetTypetype attributes read through here
locationidlookup → Locationreplaces the locationcode string
parentassetidlookup → Assetself-reference; replaces parentassettag. Sub-asset = this is set
status / criticalitychoice columns (criticality can be a formula on currentvalue)
childcount / totalvaluerollupnative rollups over child assets
lastmaintenancedaterollupmax() over related maintenance logs
MaintenanceLogexisting table — repair in place
maintenancelogidPKexisting
assetidlookup → Assetreplaces the assettag string join
maintenancedate / costlabor / costparts / technician / workperformedgenuine event data — keep
assetnamecopy, assetstatuscopy, locationcodecopy, typenamecopydropsnapshot duplicates — read through the lookup instead

Optional: a Manufacturer table if you want manufacturer normalized out of AssetType rather than left as a text field.

04Column disposition — asset register

Where each business column on pathix_assetregister ends up:

ColumnDispositionNotes
assettag, name, serialnumber, installdate, currentvalue, componentpositionstayper-instance attributes of the Asset
site, building, floor, locationcode, parentlocationcode→ Locationparentlocationcode becomes a self-lookup
typename, manufacturer, model, expectedlifeyears, maintenanceintervaldays→ AssetTypeAsset gets a lookup instead
parentassettag→ lookupbecomes parentassetid self-reference
status, criticality, recordtype-driven flags→ choicestatus partly duplicates system statecode/statuscode
childcount, totalvalue, lastmaintenancedaterolluprecompute on migrate; do not trust current values
locationpath, issubassetdrop / derivepath derives from hierarchy; issubasset = parent is set
recordtypedropbecomes table identity once entities are split

05Plugin disposition

eliminate rewire native replacement
PluginWhat it really doesFateReplaced by
MegaTableValidator
Update
Referential integrity (locationcode must exist) + required field (manufacturer on AssetType) eliminate Native lookup FK + required column. The only plugin that throws rather than swallowing.
TypeAttributeCopyDown
Create
Copies manufacturer / model / expectedlife / interval from AssetType onto each Asset eliminate AssetType lookup — attributes read through the relationship, never copied
SubAssetRollup
Create
Counts & sums child values onto the parent (create-only → goes stale) eliminate Native rollup columns over the parentassetid relationship
MaintenanceLogStamp
Create
Copies asset snapshot onto each log row; back-stamps lastmaintenancedate eliminate Asset lookup on the log + max() rollup for lastmaintenancedate; drop the 4 copy columns
StatusCascade
Update
Propagates a Location's status to its Assets & SubAssets rewire Repoint to the Location→Asset relationship. Field-value propagation isn't a standard cascade type, so this likely stays as (cleaner) logic on the real lookup
AssetCodeGenerator
Create
Generates assettag ({loc}-{type}-{seq:D3}) and sets criticality by value (≥100k Critical, ≥25k High, else Standard) rewire Keep as genuine logic; repoint its reads at the Location / AssetType lookups. Criticality could move to a formula column

Net: four plugins eliminated, two rewired. The eliminated four are also the fragile ones — all four silently swallow exceptions.

06Migration sequence

  1. Build the target tables

    Create Location, AssetType, and Asset. Add lookups (parentlocation, assettype, location, parentasset, log→asset), alternate keys on locationcode, typename, assettag, choice columns for status/criticality, and the rollups. Repair MaintenanceLog with the asset lookup.

  2. Migrate & resolve keys

    Split existing rows by recordtype into the new tables. Resolve the string keys (locationcode, typename, parentassettag, log assettag) into real lookup references using the alternate keys.

  3. Recompute, don't copy

    Because failed plugins were swallowed silently, treat childcount, totalvalue, lastmaintenancedate, and the copied AssetType attributes as untrusted. Let the new rollups and lookups recompute them rather than carrying the old values over.

  4. Replace the logic

    Delete the four eliminated plugins; rewire AssetCodeGenerator and StatusCascade to read through the new relationships. Verify native rollups and the FK/required-field constraints cover what SubAssetRollup and MegaTableValidator used to do.

  5. Rebuild surfaces & cut over

    Forms, views, charts and dashboards are recreated per entity (they were excluded from the logic footprint by design). Validate, then decommission the mega-table.

07Risks & things to confirm

Before you delete anything

The behavioral narrations here are High confidence but were generated by automated analysis of decompiled plugin code (2026-06-04). Confirm against the actual assembly source before retiring plugins.

Validate the discriminator. Four recordtype values are confirmed in code paths; query production for any additional values before assuming the entity set is complete.

Silent failures = dirty data. The empty catch blocks mean some records likely already hold stale rollups, missing tags, or un-copied attributes. Plan a reconciliation/cleansing pass — the migration is a good moment to surface and fix these.

Alternate keys are load-bearing. assettag, locationcode, and typename drive both the data-migration key resolution and any external integrations; keep them as alternate keys even after introducing surrogate PKs.