mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 09:26:52 +01:00
Add 15 community knowledge articles from BC Code Intel ingest (#66)
* Add 15 community knowledge articles from BC Code Intel ingest Ingests net-new /community knowledge from BC Code Intelligence, surviving the admission test, gray-zone salvage, and dedup against the full corpus. Domains: ui (6), error-handling (3), performance (2), upgrade (1), appsource (1), security (1), telemetry (1). The two BC24 No. Series migration drafts are merged into one article. Adds good/bad AL samples for the clean-fit articles (error-handling, performance, security, telemetry). UI and appsource remain knowledge-only. Validator and knowledge-index checks pass (207 articles). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Correct SetLoadFields JIT-load article to match MS docs The draft claimed accessing an unlisted field "reloads the entire row" per record. Microsoft's partial-records docs say otherwise: the platform does an implicit Get that loads the missing field(s), and in a direct var loop the first JIT updates the enumerator so later iterations do not re-load. The genuine per-row penalty is the pass-by-value case, where the copy's enumerator is not updated. Rewrite the article around JIT loading and the by-value footgun, rename the slug from ...full-reload to ...jit-load, and fix the good/bad samples to demonstrate the by-value repetition accurately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jeremy Vyska <jeremy@sparebrained.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6281e7e39a
commit
4119417ce4
29 changed files with 613 additions and 0 deletions
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50130 "Purge Orders Bad Sample"
|
||||
{
|
||||
procedure PurgeCancelledLines(var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
// Assumes DeleteAll fires OnDelete and cascades to reservation entries
|
||||
// and item applications. It does not: parameterless DeleteAll() is
|
||||
// DeleteAll(false) and skips OnDelete, so the rows vanish but their
|
||||
// dependent records are orphaned.
|
||||
SalesLine.DeleteAll();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
codeunit 50130 "Purge Orders Good Sample"
|
||||
{
|
||||
procedure PurgeCancelledLines(var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
// These lines have OnDelete cleanup (reservation entries, item
|
||||
// application). Pass true so DeleteAll runs OnDelete per record and the
|
||||
// cleanup actually happens — the row-by-row cost is accepted on purpose.
|
||||
SalesLine.DeleteAll(true);
|
||||
end;
|
||||
|
||||
procedure PurgeStagingBuffer(var TempBuffer: Record "Name/Value Buffer" temporary)
|
||||
begin
|
||||
// No OnDelete logic to run: the fast, set-based form is correct here.
|
||||
TempBuffer.DeleteAll();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: performance
|
||||
keywords: [deleteall, ondelete, run-trigger, set-based-delete, bulk-delete, triggers, validation]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# DeleteAll skips OnDelete unless you pass RunTrigger
|
||||
|
||||
> Contributions welcome — open a PR to refine or extend this article.
|
||||
|
||||
## Description
|
||||
|
||||
`Record.DeleteAll()` — equivalently `DeleteAll(false)` — translates to a single set-based SQL `DELETE` and **does not** run AL `OnDelete` triggers or field/table validations. Only database-level referential constraints still apply. To run `OnDelete` logic you must call `DeleteAll(true)`, which then deletes record-by-record and forfeits the set-based performance, making it equivalent to a `FindSet` loop calling `Delete(true)`. The common misconception, which training data reproduces, is that `DeleteAll` iterates and fires `OnDelete` per record; it does not. (Parameterless `Delete()` likewise defaults to `Delete(false)` and skips `OnDelete`.)
|
||||
|
||||
## Best Practice
|
||||
|
||||
Use `DeleteAll()` / `DeleteAll(false)` for bulk deletion only when no AL `OnDelete` cleanup is required — it is the fast, set-based form. When `OnDelete` logic must run (cascading deletes, ledger cleanup, integration events), pass `DeleteAll(true)` and accept the row-by-row cost, or refactor the cleanup to run explicitly before the bulk delete.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling `DeleteAll()` and assuming dependent records, integration events, or validation side effects are handled by `OnDelete`. The deletion succeeds but the AL-side cleanup never runs, leaving orphaned data — and adding a manual `FindSet`/`Delete` loop "for safety" reintroduces the per-record cost the set-based form was chosen to avoid.
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
codeunit 50132 "LoadFields Bad Sample"
|
||||
{
|
||||
procedure TotalReleasedAmount(): Decimal
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
Total: Decimal;
|
||||
begin
|
||||
// "Currency Code" is not listed. The helper takes SalesHeader BY VALUE,
|
||||
// so the copy neither shares the load set nor updates the enumerator:
|
||||
// reading the unlisted field triggers a fresh JIT load (an extra Get)
|
||||
// on EVERY iteration, quietly reversing the saving.
|
||||
SalesHeader.SetLoadFields("Amount Including VAT", Status);
|
||||
if SalesHeader.FindSet() then
|
||||
repeat
|
||||
if IsLocalReleased(SalesHeader) then
|
||||
Total += SalesHeader."Amount Including VAT";
|
||||
until SalesHeader.Next() = 0;
|
||||
exit(Total);
|
||||
end;
|
||||
|
||||
local procedure IsLocalReleased(SalesHeader: Record "Sales Header"): Boolean
|
||||
begin
|
||||
exit((SalesHeader.Status = SalesHeader.Status::Released) and
|
||||
(SalesHeader."Currency Code" = ''));
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
codeunit 50132 "LoadFields Good Sample"
|
||||
{
|
||||
procedure TotalReleasedAmount(): Decimal
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
Total: Decimal;
|
||||
begin
|
||||
// Every field read anywhere downstream is listed — including the one
|
||||
// the by-var helper reads — so no JIT load is ever triggered.
|
||||
SalesHeader.SetLoadFields("Amount Including VAT", Status, "Currency Code");
|
||||
if SalesHeader.FindSet() then
|
||||
repeat
|
||||
if IsLocalReleased(SalesHeader) then
|
||||
Total += SalesHeader."Amount Including VAT";
|
||||
until SalesHeader.Next() = 0;
|
||||
exit(Total);
|
||||
end;
|
||||
|
||||
local procedure IsLocalReleased(var SalesHeader: Record "Sales Header"): Boolean
|
||||
begin
|
||||
exit((SalesHeader.Status = SalesHeader.Status::Released) and
|
||||
(SalesHeader."Currency Code" = ''));
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: performance
|
||||
keywords: [setloadfields, partial-records, just-in-time-load, jit-load, round-trip, pass-by-value, enumerator]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Reading an unlisted field after SetLoadFields triggers a JIT load
|
||||
|
||||
> Contributions welcome — open a PR to refine or extend this article.
|
||||
|
||||
## Description
|
||||
|
||||
`SetLoadFields` loads only the named fields, but the trap is what happens when code later reads a field that was *not* listed: the platform silently issues a **just-in-time (JIT) load** — an implicit `Get` that fetches the missing field(s) in a second database round-trip. A single JIT load can erase the saving; the real danger is a JIT that repeats per record. The optimization is only a win if the listed set covers every field touched anywhere downstream, not just in the immediate code block.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Before adding `SetLoadFields`, audit the *whole* access lifecycle of the record variable — every field read in the loop body, in called procedures, in `OnValidate`/`OnAfterGetRecord`, and in anything that receives the record — and list all of them via `SetLoadFields`/`AddLoadFields`. Be especially careful when passing a partial record **by value**: the copy does not share the load set and its enumerator is not updated, so a helper that reads an unlisted field re-triggers the JIT on *every* iteration. Pass by `var` where you can (a JIT then updates the enumerator, so later iterations don't re-load), or call `AddLoadFields` before passing by value. If you cannot enumerate the fields confidently, prefer not to call `SetLoadFields` at all. See the existing guidance on when partial records pay off (`use-setloadfields-for-partial-records`).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Adding `SetLoadFields(Field1, Field2)` at the top of a loop, then reading `Field3` deeper in the body or inside a by-value helper. The code compiles and returns correct data, but pays a hidden JIT round-trip — and in the by-value case it repeats once per row, quietly reversing the gain. JIT loads also introduce `Inconsistent read` / record-modified race errors that a full non-partial load avoids. Reviewer signal: a `SetLoadFields` list that omits a field later read through that record variable, especially a record passed by value to a procedure that reads a field the caller never listed.
|
||||
Loading…
Add table
Add a link
Reference in a new issue