intent: two values of one row, compared - checks: compare (#7095) - #7121
Merged
Conversation
`checks:` knew `exactlyOne`, `itemsSumEqual` and `itemsMin`. No kind related two
values of the SAME record, so the most ordinary rule a business document has could
not be declared at all: an invoice was saved (200), issued, and overdue the moment
it existed, because its date moved three months forward and its due date stayed
where it was. The only expressible workaround was a hand-written calculated action
per document type, which CORRECTS the value instead of refusing it - so the clerk
is never told their date was overruled.
checks:
- { kind: compare, field: due, op: ge, than: date, message: "Due cannot be before the invoice date" }
- { kind: compare, field: paid, op: le, than: total, message: "Paid cannot exceed the total" }
Row-level like `exactlyOne`: enforced in every generated controller's `validate()`
(the entity, personal and partner surfaces) as a 400 carrying the authored message,
and therefore taking no `status` gate - a rule about two values of one row holds
from the first save, not from a transition. `op:` is spelled out because an omitted
operator has no defensible default ("not before" and "strictly after" are different
rules and the wrong guess is silent).
Both operands are the entity's own FIELDS - a comparison of two foreign keys means
nothing - and must sit in ONE comparison family, which is what the generated code
needs: two temporals compare through their own `compareTo` (a `LocalDate` does not
compare to an `Instant`), two numbers by value through `BigDecimal` so a `decimal`
against a `long` stays exact. Only dates, timestamps and numbers compare; a string,
a boolean or a `month`/`week` label is refused rather than silently ordered
lexicographically, as is a field compared with itself. An ABSENT operand is not a
violation - a comparison is about two values that exist, and requiredness is its
own declaration.
The generated app test learns the rule with it: its sample values are per-type
constants, so two dates come out EQUAL and a strict comparison (`gt`/`lt`/`ne`)
would have every generated suite refused with 400 by the check the module just
declared. The `.test` manifest carries the comparison and the runner derives the
left operand from the right by the operator's own smallest step.
Fixes #7095
delchev
force-pushed
the
issue-7095-check-compare
branch
from
September 8, 2026 06:31
bd40dd6 to
faf1299
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
checks:knewexactlyOne,itemsSumEqualanditemsMin. No kind related two values of the SAME record, so the most ordinary rule a business document has could not be declared at all: an invoice was saved (200), issued, and overdue the moment it existed, because its date moved three months forward and its due date stayed where it was. The only expressible workaround was a hand-written calculated action per document type, which corrects the value instead of refusing it - so the clerk is never told their date was overruled, and every document type needs its own class for one comparison.Row-level like
exactlyOne: enforced in every generated controller'svalidate()- the entity, personal and partner surfaces - as a 400 carrying the authored message, and therefore taking nostatusgate: a rule about two values of one row holds from the first save, not from a transition.op:(ge/gt/le/lt/eq/ne) is spelled out because an omitted operator has no defensible default - "not before" and "strictly after" are different rules and the wrong guess is silent.Both operands are the entity's own fields - a comparison of two foreign keys means nothing - and must sit in ONE comparison family, which is what the generated code needs: two temporals compare through their own
compareTo(aLocalDatedoes not compare to anInstant), two numbers by value throughBigDecimalso adecimalagainst alongstays exact. Only dates, timestamps and numbers compare; a string, a boolean or amonth/weeklabel is refused rather than silently ordered lexicographically, as is a field compared with itself. An absent operand is not a violation - a comparison is about two values that exist, and requiredness is its own declaration.The generated app test learns the rule with it: its sample values are per-type constants, so two dates come out EQUAL and a strict comparison (
gt/lt/ne) would have every generated suite refused with 400 by the check the module just declared. The.testmanifest now carries the comparison and the runner derives the left operand from the right by the operator's own smallest step.Verification
IntentParserTest.compareChecksParseAndValidate- parse plus every refusal (unknown op, mixed families, a non-comparable type, a dangling operand, a self-comparison, astatusgate, missing operands).EdmIntentGeneratorTest.compareChecksEmitOperatorAndFamily- the two PascalCased properties, the Java operator and the family flag reach the.model.AppTestIntentGeneratorTest- the comparison reaches the.testmanifest.IntentEmissionCoverageIT(green, the only local check that COMPILES the generated client Java): both emitted branches are asserted inEntryController, and at runtime a due date behind the entry date is refused with the authored message (400), the same date is accepted, and a record carrying no due date passes.mvn formatter:validateand the-P releasejavadoc pass on the touched modules.Docs: dirigible-io/dirigible-io.github.io#234 (merged), IntentFile/intent-specification#68, IntentFile/intentfile.github.io#51.
Fixes #7095