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

This commit is contained in:
Stefano Demiliani 2026-09-15 12:51:15 +02:00 • committed by GitHub
parent b7617fb48a
commit 861f53dd97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 225 additions and 3 deletions

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -32,22 +32,24 @@ Match relevant entries against changed `query` objects, variables typed as `Quer
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`.
- 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`.
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
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.
- 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`.
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`.