templates: apply a field's defaultValue before the create-time calculations (#7104) - #7115
Merged
Merged
Conversation
…ations (#7104) `defaultValue:` was the column's DB DEFAULT and nothing else on the server, so the database supplied it at INSERT - after the generated repository's create-time calculations had already run in Java and read a null. A purchase-order line posted without VatRate stored VatRate 20 from the default and Vat 0 from the calculation that reads it, and only a later no-op update (whose recalculation sees the stored default) put the two in agreement. The generated form hid it, because the item dialog seeds the same default and posts it explicitly - so it was every REST caller, import and server-side create that silently got a wrong total. The generated repository now assigns each authored default itself, as the first statement of save(): a column the write left empty gets the default in a literal of the property's own type, BEFORE the label, the guards, the checks and the calculatedOnCreate expressions read it. Only save() defaults - an existing row is never re-defaulted, so a value the user deliberately cleared stays cleared - and the DB DEFAULT remains for a row that reaches the table by any other route. A numeric default is parsed from its authored text rather than inlined as a numeric literal, so an author's "8.0" on an integer column fails that one create instead of failing the whole generated build. A string default is accepted in either authoring shape, bare or SQL-quoted, since the value reaches the DDL verbatim. A date/time or binary column is excluded: its DEFAULT is typically a SQL expression (CURRENT_DATE) with no Java stand-in, so there the DB DEFAULT stays the only leg. Fixes #7104 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 8, 2026
delchev
added a commit
that referenced
this pull request
Sep 8, 2026
…7131) (#7163) The amend detection (#7071) compared the stored item rows with the freshly derived ones as they stand. Since #7104/#7115 the derived ones do not stand that way for long: `#applyDefaults()` runs first in every generated `save()`, so a compared column carrying an authored `defaultValue` is FILLED on the stored row and still null on the not-yet-saved derived one. In the canonical journal shape - a debit row assigning `Debit`, a credit row assigning `Credit`, both `default: 0` - the stored credit row reads `Debit = 0` back while the derived one holds null, `unchanged` could never be true, and every redelivery of the posting event was classified as an amendment: all item rows deleted and re-inserted with new ids, firing `-deleted`/`-created` on each and churning every roll-up over them, silently and indefinitely. The comparison is now over the values as they will be stored. Each compared property (and each `map:` header assignment) carries the default its own derived side will end up with, and a stored row reading that default back is not a change. A default only the DATABASE can apply - a `date`/`timestamp` column whose DEFAULT reaches the DDL verbatim as a SQL expression, which `#applyDefaults()` skips for exactly the same reason - has no Java stand-in, so what the stored row holds cannot be derived at all: that column is compared only for the rows that do assign it. Two adjacent weaknesses in the same comparison: - every header assignment expression was evaluated TWICE, once inside `same(target.X, <expr>)` and again when assigning, so a non-deterministic expression made any later redelivery read as an amendment. Each is now evaluated once, into a numbered local both the comparison and the assignment read; - `amendableGuard` fell back to `target.<Status> == null` for an `init:` that is not a digit string - false for every document the posting creates, so it refused every legitimate amendment with a log saying someone had acted on a document nobody had touched. That fallback is unreachable through the parser (`StatusSymbolResolver` rewrites a status named by its seeded name to the seed id and refuses one that resolves to none), but it is now a warned fail-open rather than a silent refusal: no guard is emitted, the target stays rewritable as one with no lifecycle is, and the model is named at generation time. The authored `type:` to SQL type mapping the branch needs is the EDM's own, so it moves to `IntentEntities.sqlType` and `EdmIntentGenerator.mapDataType` delegates to it - two generators deciding independently what a default means is how this defect arrived. A model whose posting columns carry no defaults generates the same handler as before. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 8, 2026
delchev
added a commit
that referenced
this pull request
Sep 9, 2026
…#7200) #7101's create-path allocator resolved a null `per:` FK to its own copy of the relation's `init:` (`numberPerDefault`), on the premise that "the database applies that on insert". Since #7104/#7115 the applyDefaults macro is the FIRST statement of `save()` and assigns exactly that FK from exactly that value, so by the time the numbering block runs the FK is already set and the `== null` arm is unreachable. Both arms resolved the same partition, so nothing was wrong at run time - but the comment documented a premise that no longer holds, and a reader would take a dead fallback for the load-bearing guarantee. The arm and the marker are gone: the allocator reads the FK the write has already defaulted, and a null FK now means what it says - a relation that declares no `init:` at all, i.e. the tenant-wide base row. `numberPerDefault` had exactly one consumer, so `EdmIntentGenerator.putNumberPartitionDefaults` goes with it (which also reunites the `ProcessIds` javadoc it had been inserted in front of with `processIdsProperty`). The issue's second half asked for the partition default to be unquoted like `#defaultLiteral`, so an `init: 'ACME'` could not allocate in partition `'ACME'` beside `ACME`. It cannot: unlike a field `default:`, a relation's `init:` is resolved by `StatusSymbolResolver` against the target's own seeds and anything that is neither a seeded name nor a numeric id is refused at parse - so the quoted shape never reaches a generator. Adding the unquoting would have been a second piece of dead code beside the one being removed, so the invariant is stated where a reader would otherwise wonder and pinned by a test instead. The `stampOn: issue` stamp keeps its own `perDefault` fallback: it reads a row loaded from the database, where the column can genuinely hold null (an explicit null write into a nullable column with a DEFAULT stores null). Verified: the rendered repository defaults the FK before the allocation and carries no second copy of the init value (IntentEngineIT's numbering case, rewritten to assert that ORDER rather than the old ternary constant, green end to end); engine-intent's 1182 unit tests green. Fixes #7147 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
defaultValue:was the column's DB DEFAULT and nothing else on the server, so the database supplied it at INSERT — after the generated repository's create-time calculations had already run in Java and read anull.On BusinessIntents STA, a purchase-order line posted without
VatRate(vatRate: { defaultValue: 20 },vat: calculatedOnCreate: "round(Net * VatRate / 100, 2)") storedVatRate 20from the default andVat 0from the calculation that reads it; only a later no-opPUT— whose recalculation sees the stored default — put the two in agreement. The generated form hid it, because the item dialog seeds the same default and posts it explicitly. So it was every REST caller, import and server-side create that silently got a wrong total.The fix
Repository.java.templategains an#applyDefaults()macro, emitted as the first statement ofsave(): a column the write left empty gets its authored default, in a literal of the property's own type, before the label, the roll-up/aggregate guards, the document checks and thecalculatedOnCreateexpressions read it.save()only. An existing row is never re-defaulted, so a value the user deliberately cleared stays cleared throughupdate().new java.math.BigDecimal("20")) rather than inlined as a numeric literal, so an author's8.0on an integer column fails that one create instead of failing the whole generated build.DRAFT, what the item dialog seeds) or SQL-quoted ('DRAFT', what a working DB DEFAULT needs, since the value reaches the DDL verbatim) — and both yield the string the column would hold.date/time/timestampor binary column is excluded: its DEFAULT reaches the DDL verbatim and is typically a SQL expression (CURRENT_DATE,now()), which has no Java stand-in. There the DB DEFAULT remains the only leg.A to-one relation's
init:is the same mechanism (it lands asdataDefaultValueon the FK), so an initial status is now assigned by the repository too — the value the create-time postings and process triggers see, not the one the database was about to fill.Verification
IntentEngineIT#an_authored_default_is_applied_before_the_create_time_calculations— a decimal, a boolean and a string default; each guarded onnull; the decimal assigned before theCalc.evalthat reads it; and exactly once in the file, soupdate()does not re-default.IntentEngineITnew test +a_required_value_is_refused_by_name_instead_of_by_the_database+calculated_field_action_emits_an_imports_backed_callout_in_the_repository— 3/3.IntentEmissionCoverageIT— green. It exercises this at runtime through a real HTTP create: a document posted without itsinit:-defaulted status still answers 200 and echoes the applied default.ModelGenerationIT— green (every template still renders its model).mvn install— BUILD SUCCESS.Fixes #7104
🤖 Generated with Claude Code