mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 06:36:55 +01:00
Add query filter semantics guidance (#186)
Some checks are pending
Validate knowledge index / validate-index (push) Waiting to run
Validate AL review fixtures / validate-review-fixtures (push) Waiting to run
Validate skill index and report schemas / validate-contract (push) Waiting to run
Validate frontmatter and structure / validate (push) Waiting to run
Some checks are pending
Validate knowledge index / validate-index (push) Waiting to run
Validate AL review fixtures / validate-review-fixtures (push) Waiting to run
Validate skill index and report schemas / validate-contract (push) Waiting to run
Validate frontmatter and structure / validate (push) Waiting to run
This commit is contained in:
parent
b7617fb48a
commit
861f53dd97
8 changed files with 225 additions and 3 deletions
|
|
@ -30,6 +30,14 @@
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"article": "no-pii-in-telemetry-message-string"
|
"article": "no-pii-in-telemetry-message-string"
|
||||||
},
|
},
|
||||||
|
"query": {
|
||||||
|
"articles": [
|
||||||
|
"dataitemtablefilter-cannot-be-overwritten-at-runtime",
|
||||||
|
"reopening-query-resets-cursor-but-keeps-filters",
|
||||||
|
"set-query-filters-before-open",
|
||||||
|
"setfilter-overwrites-query-columnfilter"
|
||||||
|
]
|
||||||
|
},
|
||||||
"reporting": {
|
"reporting": {
|
||||||
"articles": [
|
"articles": [
|
||||||
"clear-report-variable-before-independent-runmodal",
|
"clear-report-variable-before-independent-runmodal",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
query 50428 "Static Query Filter Bad"
|
||||||
|
{
|
||||||
|
QueryType = Normal;
|
||||||
|
|
||||||
|
elements
|
||||||
|
{
|
||||||
|
dataitem(SalesHeader; "Sales Header")
|
||||||
|
{
|
||||||
|
DataItemTableFilter = Status = const(Open);
|
||||||
|
|
||||||
|
column(DocumentNo; "No.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
filter(StatusFilter; Status)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50429 "Static Query Filter Bad"
|
||||||
|
{
|
||||||
|
procedure ReadReleasedOrders()
|
||||||
|
var
|
||||||
|
SalesHeader: Record "Sales Header";
|
||||||
|
SalesHeaderQuery: Query "Static Query Filter Bad";
|
||||||
|
begin
|
||||||
|
// This is combined with Status = Open and returns no rows.
|
||||||
|
SalesHeaderQuery.SetRange(StatusFilter, SalesHeader.Status::Released);
|
||||||
|
SalesHeaderQuery.Open();
|
||||||
|
while SalesHeaderQuery.Read() do
|
||||||
|
ProcessOrder(SalesHeaderQuery.DocumentNo);
|
||||||
|
SalesHeaderQuery.Close();
|
||||||
|
end;
|
||||||
|
|
||||||
|
local procedure ProcessOrder(DocumentNo: Code[20])
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
query 50430 "Static Query Filter Good"
|
||||||
|
{
|
||||||
|
QueryType = Normal;
|
||||||
|
|
||||||
|
elements
|
||||||
|
{
|
||||||
|
dataitem(SalesHeader; "Sales Header")
|
||||||
|
{
|
||||||
|
DataItemTableFilter = "Document Type" = const(Order);
|
||||||
|
|
||||||
|
column(DocumentNo; "No.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
filter(StatusFilter; Status)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50431 "Static Query Filter Good"
|
||||||
|
{
|
||||||
|
procedure ReadReleasedOrders()
|
||||||
|
var
|
||||||
|
SalesHeader: Record "Sales Header";
|
||||||
|
SalesHeaderQuery: Query "Static Query Filter Good";
|
||||||
|
begin
|
||||||
|
SalesHeaderQuery.SetRange(StatusFilter, SalesHeader.Status::Released);
|
||||||
|
SalesHeaderQuery.Open();
|
||||||
|
while SalesHeaderQuery.Read() do
|
||||||
|
ProcessOrder(SalesHeaderQuery.DocumentNo);
|
||||||
|
SalesHeaderQuery.Close();
|
||||||
|
end;
|
||||||
|
|
||||||
|
local procedure ProcessOrder(DocumentNo: Code[20])
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: query
|
||||||
|
keywords: [query, dataitemtablefilter, setfilter, setrange, static-filter, filter-precedence]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# DataItemTableFilter cannot be overwritten at runtime
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
`DataItemTableFilter` defines a static filter on a Query dataitem. A runtime `SetFilter` or `SetRange` on the same source field does not replace that filter. The static and runtime filters are combined with AND, so contradictory values produce an empty dataset instead of broadening or replacing the query definition.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
Keep only invariant restrictions in `DataItemTableFilter`. Expose caller-selectable fields through a column or filter row and apply their values with `SetFilter` or `SetRange` before `Open()`. When both filter types intentionally target the same field, ensure their intersection represents the required dataset.
|
||||||
|
|
||||||
|
See sample: [`dataitemtablefilter-cannot-be-overwritten-at-runtime.good.al`](dataitemtablefilter-cannot-be-overwritten-at-runtime.good.al).
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Define a static filter in `DataItemTableFilter`, then apply a contradictory runtime filter to the same source field while expecting the runtime filter to replace the static one. Both filters remain effective and the query returns no rows.
|
||||||
|
|
||||||
|
See sample: [`dataitemtablefilter-cannot-be-overwritten-at-runtime.bad.al`](dataitemtablefilter-cannot-be-overwritten-at-runtime.bad.al).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Filtering in Query objects — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-query-filters
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
query 50432 "Column Query Filter Bad"
|
||||||
|
{
|
||||||
|
QueryType = Normal;
|
||||||
|
|
||||||
|
elements
|
||||||
|
{
|
||||||
|
dataitem(SalesLine; "Sales Line")
|
||||||
|
{
|
||||||
|
column(DocumentNo; "Document No.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
column(LineQuantity; Quantity)
|
||||||
|
{
|
||||||
|
ColumnFilter = LineQuantity = filter(> 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50433 "Column Query Filter Bad"
|
||||||
|
{
|
||||||
|
procedure ReadSmallPositiveLines()
|
||||||
|
var
|
||||||
|
SalesLineQuery: Query "Column Query Filter Bad";
|
||||||
|
begin
|
||||||
|
// This replaces > 0, so negative quantities are also returned.
|
||||||
|
SalesLineQuery.SetFilter(LineQuantity, '<100');
|
||||||
|
SalesLineQuery.Open();
|
||||||
|
while SalesLineQuery.Read() do
|
||||||
|
ProcessLine(SalesLineQuery.DocumentNo, SalesLineQuery.LineQuantity);
|
||||||
|
SalesLineQuery.Close();
|
||||||
|
end;
|
||||||
|
|
||||||
|
local procedure ProcessLine(DocumentNo: Code[20]; Quantity: Decimal)
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
query 50434 "Column Query Filter Good"
|
||||||
|
{
|
||||||
|
QueryType = Normal;
|
||||||
|
|
||||||
|
elements
|
||||||
|
{
|
||||||
|
dataitem(SalesLine; "Sales Line")
|
||||||
|
{
|
||||||
|
DataItemTableFilter = Quantity = filter(> 0);
|
||||||
|
|
||||||
|
column(DocumentNo; "Document No.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
column(LineQuantity; Quantity)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50435 "Column Query Filter Good"
|
||||||
|
{
|
||||||
|
procedure ReadSmallPositiveLines()
|
||||||
|
var
|
||||||
|
SalesLineQuery: Query "Column Query Filter Good";
|
||||||
|
begin
|
||||||
|
// This combines with the invariant Quantity > 0 dataitem filter.
|
||||||
|
SalesLineQuery.SetFilter(LineQuantity, '<100');
|
||||||
|
SalesLineQuery.Open();
|
||||||
|
while SalesLineQuery.Read() do
|
||||||
|
ProcessLine(SalesLineQuery.DocumentNo, SalesLineQuery.LineQuantity);
|
||||||
|
SalesLineQuery.Close();
|
||||||
|
end;
|
||||||
|
|
||||||
|
local procedure ProcessLine(DocumentNo: Code[20]; Quantity: Decimal)
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: query
|
||||||
|
keywords: [query, columnfilter, setfilter, setrange, filter-precedence, runtime-filter]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# SetFilter and SetRange overwrite Query ColumnFilter
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
`ColumnFilter` on a Query column or filter row defines a dynamic filter. A runtime `SetFilter` or `SetRange` on that same column or filter row replaces the `ColumnFilter`; it does not combine the two conditions. Rows excluded by the declarative filter can therefore reappear when the runtime filter omits that restriction.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
Place invariant restrictions in `DataItemTableFilter`, which runtime filters cannot overwrite. When a `ColumnFilter` is intentionally replaceable, make each runtime `SetFilter` or `SetRange` express the complete required condition before `Open()`.
|
||||||
|
|
||||||
|
See sample: [`setfilter-overwrites-query-columnfilter.good.al`](setfilter-overwrites-query-columnfilter.good.al).
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Apply `SetFilter` or `SetRange` to a column or filter row and rely on its existing `ColumnFilter` to remain effective. The runtime call replaces that filter and can admit rows that the query definition appeared to exclude.
|
||||||
|
|
||||||
|
See sample: [`setfilter-overwrites-query-columnfilter.bad.al`](setfilter-overwrites-query-columnfilter.bad.al).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Filtering in Query objects — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-query-filters
|
||||||
|
|
@ -32,22 +32,24 @@ Match relevant entries against changed `query` objects, variables typed as `Quer
|
||||||
|
|
||||||
The following targeted checks cover every current `query` article:
|
The following targeted checks cover every current `query` article:
|
||||||
|
|
||||||
|
- A `DataItemTableFilter` and a runtime `SetFilter` or `SetRange` constrain the same source field incompatibly, while the runtime call is intended to replace or broaden the static filter — `dataitemtablefilter-cannot-be-overwritten-at-runtime`.
|
||||||
- `SetFilter` or `SetRange` occurs after `Open()` without a new `Open()` before the next `Read()` — `set-query-filters-before-open`.
|
- `SetFilter` or `SetRange` occurs after `Open()` without a new `Open()` before the next `Read()` — `set-query-filters-before-open`.
|
||||||
|
- A runtime `SetFilter` or `SetRange` replaces a `ColumnFilter` on the same column or filter row, while later code relies on the declarative restriction remaining effective — `setfilter-overwrites-query-columnfilter`.
|
||||||
- An already-open query is opened again as if that advanced the cursor, or a query variable is reused for an independent operation without `Clear` even though old filters must not carry over — `reopening-query-resets-cursor-but-keeps-filters`.
|
- An already-open query is opened again as if that advanced the cursor, or a query variable is reused for an independent operation without `Clear` even though old filters must not carry over — `reopening-query-resets-cursor-but-keeps-filters`.
|
||||||
|
|
||||||
Resolve layer conflicts per READ. When no query knowledge exists, emit `no-knowledge`; when knowledge exists but no article matches the changed Query usage, emit `completed` with no findings.
|
Resolve layer conflicts per READ. When no query knowledge exists, emit `no-knowledge`; when knowledge exists but no article matches the changed Query usage, emit `completed` with no findings.
|
||||||
|
|
||||||
## Action
|
## Action
|
||||||
|
|
||||||
Evaluate every worklist article against the diff's Query call order and surrounding control flow.
|
Evaluate every worklist article against the Query definition, the diff's call order, and surrounding control flow. For filter-precedence findings, require both the declarative filter and the runtime call to be visible, and require local evidence that replacement, broadening, or retention of the original filter is intended.
|
||||||
|
|
||||||
- Emit `major` for an unambiguous Anti Pattern that can close the dataset, restart processing, or retain an unintended filter.
|
- Emit `major` for an unambiguous Anti Pattern that can close the dataset, restart processing, retain an unintended filter, produce an empty intersection, or admit rows excluded by an overwritten filter.
|
||||||
- Emit `minor` when code contradicts a Best Practice but the resulting behavior depends on unseen control flow.
|
- Emit `minor` when code contradicts a Best Practice but the resulting behavior depends on unseen control flow.
|
||||||
- Do not emit applicability-only information. A Query article produces a finding only when the changed code violates its normative guidance.
|
- Do not emit applicability-only information. A Query article produces a finding only when the changed code violates its normative guidance.
|
||||||
|
|
||||||
Set confidence to `high` for a locally visible call sequence and `medium` when aliases, helper calls, or missing context obscure the sequence. Domain-scoped agent findings follow DO's precision bar and remain capped at `minor`/`medium`.
|
Set confidence to `high` for a locally visible call sequence and `medium` when aliases, helper calls, or missing context obscure the sequence. Domain-scoped agent findings follow DO's precision bar and remain capped at `minor`/`medium`.
|
||||||
|
|
||||||
Provide `suggested-code` only when moving a filter before `Open()` or adding `Clear` is a complete, local, unambiguous replacement. Otherwise set `suggested-code-omission-reason`.
|
Provide `suggested-code` only when moving a filter before `Open()`, adding `Clear`, moving an invariant restriction to `DataItemTableFilter`, or composing the complete runtime filter is a complete, local, unambiguous replacement. Otherwise set `suggested-code-omission-reason`.
|
||||||
|
|
||||||
Outcome selection follows DO: `completed`, `no-knowledge`, `not-applicable`, `partial`, or `failed`.
|
Outcome selection follows DO: `completed`, `no-knowledge`, `not-applicable`, `partial`, or `failed`.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue