mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-06 07:06:54 +01:00
Fix four merge-critical issues from Jesper's 2026-09-22 review
- pages-must-not-contain-business-logic.good.al/.bad.al: the "good" codeunit still directly assigned real Sales Line."Line Amount" and called Modify(), bypassing the field's normal Validate cascade (discount, VAT, related-amount maintenance) - persisting inconsistent document lines regardless of which object the code lived in. Replaced the real Sales Line example with a self-contained "Sample Order Line" table and switched the codeunit to Validate()/Modify(true), so the fixture demonstrates the page-vs-codeunit separation without teaching unsafe direct field writes to a real BC document table. - bcpt-scenarios-must-be-app-specific.good.al: Customer.FindFirst() assumed a pre-existing customer (fails against an empty environment), and a session-local NextNo counter for the header key collides across concurrent BCPT sessions and repeated runs. Creates its own customer when none exists, and generates keys from CreateGuid() instead of an in-memory counter. - upgrade-tag-logic-must-not-nest-deeply: the rule conflated two different things - nesting one tag's existence check inside another (the real anti-pattern Microsoft's guidance warns against) with having business-data safety conditions inside a single tagged migration's own loop body (which Microsoft's own worked example does, and its own design guidance explicitly requires: "Implement extra safety checks to avoid data corruption, even though you're using upgrade tags"). Rewrote the Description/Best Practice/Anti Pattern to scope the rule to actual tag nesting and migrations blended under one tag, and rewrote both fixtures: good.al now shows two safety conditions correctly nested inside one migration's own loop plus a second, genuinely separate migration as its own flat tagged procedure; bad.al now shows the real anti-pattern, one tag's check nested inside another's guarded body. - table-design-must-match-bc-table-type-conventions: the rule and its worklist cue fired on any new table with a keys block, forcing buffers, queues, logs, mapping tables, and staging tables into the nearest-looking one of nine business-record archetypes. Added an explicit scope note that these nine types aren't an exhaustive table catalogue, and narrowed the al-data-modeling-review.md cue to require positive evidence (a type-specific naming suffix, key shape, or usage) before worklisting, instead of a bare keys/primary-key declaration.
This commit is contained in:
parent
67962727f6
commit
4a95985c8c
8 changed files with 154 additions and 37 deletions
|
|
@ -1,14 +1,32 @@
|
|||
local procedure UpgradeCustomerDiscountField()
|
||||
local procedure UpgradeCustomerFields()
|
||||
begin
|
||||
if not UpgradeTag.HasUpgradeTag(GetCustomerDiscountFieldTag()) then begin
|
||||
if Customer.FindSet() then begin
|
||||
Customer.SetLoadFields("Discount %", "Customer Posting Group");
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
if Customer."Discount %" = 0 then begin
|
||||
if Customer."Customer Posting Group" <> '' then
|
||||
Customer."Discount %" := 5;
|
||||
if (Customer."Discount %" = 0) and (Customer."Customer Posting Group" <> '') then begin
|
||||
Customer."Discount %" := 5;
|
||||
Customer.Modify();
|
||||
end;
|
||||
until Customer.Next() = 0;
|
||||
end;
|
||||
UpgradeTag.SetUpgradeTag(GetCustomerDiscountFieldTag());
|
||||
|
||||
// BUG: a second, unrelated migration's tag check nested inside the
|
||||
// first migration's guarded body. Neither tag can be checked,
|
||||
// skipped, or fixed independently of the other - a failure or a
|
||||
// deliberate skip of the discount migration silently takes the
|
||||
// shipping-agent migration down with it, and nothing in the
|
||||
// Upgrade Tags table records that the second step ran on its own.
|
||||
if not UpgradeTag.HasUpgradeTag(GetCustomerShippingAgentFieldTag()) then begin
|
||||
Customer.SetLoadFields("Shipping Agent Code");
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
if Customer."Shipping Agent Code" = '' then begin
|
||||
Customer."Shipping Agent Code" := DefaultShippingAgentCode();
|
||||
Customer.Modify();
|
||||
end;
|
||||
until Customer.Next() = 0;
|
||||
UpgradeTag.SetUpgradeTag(GetCustomerShippingAgentFieldTag());
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -6,23 +6,35 @@ begin
|
|||
Customer.SetLoadFields("Discount %", "Customer Posting Group");
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
SetDefaultDiscountIfEligible(Customer);
|
||||
// A business-data safety condition inside this one migration's
|
||||
// loop is not a second migration hiding inside the first -
|
||||
// Microsoft's own upgrade-tag example nests exactly this shape
|
||||
// (a corruption guard, then a redundant-write guard) inside a
|
||||
// single tagged procedure.
|
||||
if (Customer."Discount %" = 0) and (Customer."Customer Posting Group" <> '') then begin
|
||||
Customer."Discount %" := 5;
|
||||
Customer.Modify();
|
||||
end;
|
||||
until Customer.Next() = 0;
|
||||
|
||||
UpgradeTag.SetUpgradeTag(GetCustomerDiscountFieldTag());
|
||||
end;
|
||||
|
||||
local procedure SetDefaultDiscountIfEligible(var Customer: Record Customer)
|
||||
// A second, genuinely unrelated migration gets its own tag and its own
|
||||
// top-level procedure - not nested inside the first one's guarded body.
|
||||
local procedure UpgradeCustomerShippingAgentField()
|
||||
begin
|
||||
// Both safety conditions from the original logic are preserved, just
|
||||
// flattened into early exits instead of nested ifs: don't overwrite an
|
||||
// already-set discount, and don't touch a customer with no posting
|
||||
// group configured yet.
|
||||
if Customer."Discount %" <> 0 then
|
||||
exit;
|
||||
if Customer."Customer Posting Group" = '' then
|
||||
if UpgradeTag.HasUpgradeTag(GetCustomerShippingAgentFieldTag()) then
|
||||
exit;
|
||||
|
||||
Customer."Discount %" := 5;
|
||||
Customer.Modify();
|
||||
Customer.SetLoadFields("Shipping Agent Code");
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
if Customer."Shipping Agent Code" = '' then begin
|
||||
Customer."Shipping Agent Code" := DefaultShippingAgentCode();
|
||||
Customer.Modify();
|
||||
end;
|
||||
until Customer.Next() = 0;
|
||||
|
||||
UpgradeTag.SetUpgradeTag(GetCustomerShippingAgentFieldTag());
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -7,23 +7,25 @@ countries: [w1]
|
|||
application-area: [all]
|
||||
---
|
||||
|
||||
# Keep upgrade tag conditional logic to at most two levels of nesting
|
||||
# Never nest upgrade tag checks or blend two migrations under one tag
|
||||
|
||||
## Description
|
||||
|
||||
The conditional logic that gates upgrade code behind an upgrade tag should be at most two levels deep: check the tag, exit if already applied, otherwise run the upgrade step. Deeper nesting — tag checks inside tag checks, or a tag check combined with multi-branch business-data conditions — signals that the upgrade step is trying to do more than one thing, or that it is reconstructing decision logic that belongs in the tag structure itself (one tag per distinct upgrade step), not in nested `if` statements inside a single step.
|
||||
Upgrade tag *checks* should stay flat: never nest one tag's existence check inside another tag's guarded body, and never let one tagged procedure quietly perform a second, functionally distinct migration — that turns two upgrade steps into one that can't be tracked, skipped, or fixed independently, which is exactly what separate tags exist to prevent. That is the specific nesting Microsoft's own guidance warns against ("Keep tags simple by limiting nesting tags to two levels").
|
||||
|
||||
Upgrade code runs unattended, once, against production data with no chance to interactively debug a wrong branch. The cost of a nesting-driven mistake here is much higher than in ordinary application code, which is why the ceiling is lower than general AL style would otherwise allow.
|
||||
That is not a limit on how much conditional logic a single migration's own loop body may contain. Microsoft's own worked example for upgrade tags nests a record loop with two business-data safety conditions — a corruption guard, then a redundant-write guard — inside one `if UpgradeTagMgt.HasUpgradeTag(...) then exit;`-guarded procedure, and its own design guidance separately *requires* this: "Implement extra safety checks to avoid data corruption, even though you're using upgrade tags." A business-data guard that protects the single migration a tag represents is not a second migration hiding inside the first, however many `if` levels it takes.
|
||||
|
||||
Upgrade code runs unattended, once, against production data with no chance to interactively debug a wrong branch — which is why mixing two migrations under one tag, or losing track of which tag guards which step, is a genuinely higher-cost mistake here than the equivalent would be in ordinary application code.
|
||||
|
||||
## Best Practice
|
||||
|
||||
One tag check, one exit, one upgrade action — two levels deep at most.
|
||||
One tag, one migration: exit early if the tag is already set, then run the one upgrade step that tag represents — including as many business-data safety conditions as that single step's own correctness requires, nested however deep the logic actually needs. Reach for a second, separately tagged migration only when the nested logic is doing genuinely unrelated work (a different table, a different field, a different concern) that could legitimately be skipped, retried, or fixed on its own.
|
||||
|
||||
See sample: [`upgrade-tag-logic-must-not-nest-deeply.good.al`](upgrade-tag-logic-must-not-nest-deeply.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Nesting the tag check, a record loop, and a multi-branch business condition inside one procedure. Split the buried business condition into its own, separately tagged upgrade step instead.
|
||||
Checking one upgrade tag inside the guarded body of another, or writing two functionally unrelated migrations — different tables, different concerns — under a single tag so neither can be tracked, skipped, or fixed independently of the other. A record loop with business-data safety conditions inside one tagged migration's own body is not this anti-pattern, even several `if` levels deep, as long as every condition serves that one migration.
|
||||
|
||||
See sample: [`upgrade-tag-logic-must-not-nest-deeply.bad.al`](upgrade-tag-logic-must-not-nest-deeply.bad.al).
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue