Skip to content

retrieve … first is the object; limit 1 is a list of one under mdl 1 (#734) - #740

Merged
ako merged 14 commits into
mainfrom
feature/734-retrieve-first-vs-limit
Sep 27, 2026
Merged

ako merged 14 commits into
mainfrom
feature/734-retrieve-first-vs-limit

Conversation

@ako

@ako ako commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Closes #734 (plan item 2.5 of PROPOSAL_mdl_beta_syntax_freeze.md, §5 item 2; ADR-0011).

What changes

  • retrieve … first is Mendix's "First object" range (ConstantRange, SingleObject) in every language version.
  • Under mdl 1;, a bare retrieve … limit 1 is a Custom range: a list of one.
  • Without the header (mdl 0), limit 1 keeps its alpha meaning, the object. check/exec warn MDL-V1-LIMIT1, a langver.Change declared in mdl/visitor/visitor_retrieve_range.go.
  • describe prints the object range as first. That text means the same under any header.
  • The visitor resolves the meaning into the new RetrieveStmt.First. The writer, the flow builder's variable typing, the validator's typing and MDL-RETRIEVE01 all read that field and never read limit text.
  • MDL-RETRIEVE01 is adjusted, not retired. It keys on the object range, whichever spelling produced it. It stays because treating an object as a list is still a build error that check --references does not otherwise catch. The message now says CE0100 for a loop: mx check 11.13 reports a loop over an object as CE0100, not CE0097.
  • Docs: CLAUDE.md idiom 5, the quick reference, the syntax topic, the skills and the docs-site pages now teach first for an object. There is also a CHANGELOG entry.

Design choices the ADRs did not settle

  • first does not combine with limit or offset. The grammar is first | [limit n] [offset n], because Mendix's object range has no offset.
  • first on an association retrieve is refused (a new construct, so this is no new rejection of an existing script). Mendix gives that source no range, so the keyword would be dropped.
  • Which texts count as "bare limit 1" is the exact condition the alpha writer used: limit text 1 and no offset. limit (1), limit $One and limit 1 offset n were already lists, and they neither change nor warn.
  • Variable typing fix. The builder typed limit 1 offset n as an object while the writer stored a list. It now follows the stored range (a list), because it reads First.
  • Remaining gap under mdl 0: a Studio Pro Custom range with limit 1 and no offset still describes as limit 1. Re-running that output without a header therefore turns it into an object, as it did before. The difference now is that the run warns MDL-V1-LIMIT1. mdl 0 has no spelling for that range. It round-trips once describe emits the mdl 1; header at beta (langver.Frozen).
  • Version lookup: the statement builders are free functions, so languageVersionOf(tree) reads the header from the parse-tree root. This is the same approach as MDL list operations as one statement per Studio Pro activity; set mandatory under mdl 1 (#733) #737's scriptLanguageVersion. Whichever PR merges second should fold one into the other.

Test plan (what I ran)

  • make build; make lint (Go + TS) passed.
  • go test -count=1 ./mdl/visitor/ ./mdl/executor/ ./mdl/ast/ ./mdl/grammar/... ./mdl/formatter/ ./mdl/linter/... ./cmd/mxcli/... passed.
  • go test -tags integration ./mdl/roundtrip/ passed. The allowlist is unchanged: PopulateUserAttributes stays getput-allowlisted for Round-trip harness: untracked describe → exec losses on the Studio Pro fixture (microflows, entities, pages, snippets, menus, roles, JS actions) #721.
  • go test -tags integration ./mdl/executor/ -run TestRoundtripMicroflow_RetrieveWith passed. Its expectation was updated to first.
  • New tests:
    • mdl/visitor/retrieve_first_test.go: each spelling under both versions, plus the refusals.
    • mdl/executor/cmd_microflows_retrieve_first_test.go: the stored range and variable type per version, the describe spelling, and check (MDL-V1-LIMIT1 / MDL-RETRIEVE01) per version.
    • mdl/roundtrip/retrieve_range_test.go on the Studio Pro-authored PedApp (FeedbackModule.PopulateUserAttributes):
      • The object form: describe prints first, and exec under both headers keeps the ConstantRange and PutGet.
      • The list form: exec under mdl 1 stores a CustomRange with limit 1, and its describe → exec under mdl 1 keeps it (PutGet).
      • Control: the same text run without the header stores the object range and warns once.
  • Revert checks (each revert made the named tests fail):
    1. Ignoring the version in the visitor (limit 1 is always the object) failed TestRetrieveLimitOne_Mdl1IsAListOfOne.
    2. Making describe print limit 1 for the object range failed TestFormatAction_Retrieve_RangeSpelling and TestPedAppRoundTrip_RetrieveRange.
    3. Restoring the builder's old limit-text logic failed four cases of TestRetrieveRange_WrittenPerLanguageVersion.
  • Mendix validation (mxcli docker check, mxbuild 11.13.0) on a copy of the Studio Pro project TestApp (baseline: 0 errors):
    • An mdl 1; script with a loop over limit 1 and a first object gave 0 errors.
    • The same script without the header is refused by check/exec (MDL-RETRIEVE01). Forced with --no-check, mx reports CE0100 'Users' is of type System.User, but should be of type List.

🤖 Generated with Claude Code

ako and others added 14 commits September 27, 2026 08:33
…t mandatory under mdl 1

Each Studio Pro List operation / Aggregate list activity is one statement
whose operand is a variable, so nesting cannot be written:

  $Open = filter $Orders by Status = M.Status.Open;
  $Big  = filter $Orders where $currentObject/Total > 1000;
  $N    = count $Open;
  $Csv  = reduce $Orders from '' as String using $currentResult + $currentObject/Name;

The statement forms parse under every version. The call forms keep
parsing: a respelling everywhere except find/contains, registered as
MDL-DEPR003 (list operations) and MDL-DEPR004 (aggregates), both building
the same AST. find(...)/contains(...) clash with the string functions, so
they, a list call after `set`, and a nested call are version-gated
(MDL-V1-LIST): kept and warned under mdl 0, refused under mdl 1, where
`set $x = find(...)` is always the string function. A reassignment without
`set` is refused under mdl 1 and warned under mdl 0 (MDL-V1-SET).

`where` is always Find/Filter by expression; `by` and the call form keep
the by-member reading when the condition is `Member = value`
(ast.IsMemberEquality, shared by visitor and flow builder).

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
describe writes the language of the newest frozen version, or of the
script it runs in when that is newer. While mdl 1 is a preview a plain
describe keeps the call form; inside an `mdl 1;` script it prints the
statement form, which executes back to the same microflow (PedApp test).

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…forms

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lementIDs, Writer.UpdateRawUnitPatch)

TransplantIDs pairs elements by type and position, which re-pairs the
surviving flows of a patched microflow onto their neighbours' $IDs after a
drop. A write that started from the stored bytes skips it; elision and the
storage-GUID guard still apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e, replace, drop)

Splices a fragment into the raw stored unit: appends only the fragment's
objects and flows, rewires the flow around the target by its pointers and
the rewired end, moves nodes past the insertion point to make room, and
never rewrites an $ID. Save refuses a unit in which anything still points at
a removed element or two elements share an $ID. Refuses what it cannot do
safely: after a decision, before a join, inside a loop body, drop/replace of
a decision or of an activity with an error handler, and a placement that
would overlap an object. The modelsdk backend writes through
UpdateRawUnitPatch (canon.Reconcile).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#736)

alter microflow|nanoflow M.F { insert after|before <target> { … }
replace <target> with { … } drop <target>; }. Targets are the #713 content
addresses, resolved against the stored flow before any operation runs.
Fragments are built with the create-microflow builder, seeded with the
flow's variables, and cut out of their start and end events. A fragment
variable that clashes with one the flow has, or one it reads that is not
declared upstream of the insertion point, is an error; so is dropping an
activity whose output is still read. Acceptance on PedApp VAL_Feedback:
only the new log, the two flows around it and the shifted positions differ;
an empty alter writes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… handle

A handle has to parse as an alter target, and a target ends at the { of a
fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs the shared splice on the stored flow and sends the difference as
ped_update_document path operations in one update, after checking the live
document still matches the .mpr (PED addresses entries by index). Drop and
replace are refused: PED does not roll back a removal when an update fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loop body flows are stored in the unit's Flows list, not in the loop, so
removing a Studio Pro loop with two or more body activities left them
pointing at removed objects and Save refused the unit
(TestApp ACT_ConflictedWorkflowHelper_ApplyJumpTo). mx check 11.14 on the
dropped and replaced copies gives the baseline error list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each operation was checked against the flow as stored only. Measured
with mx check 11.14 on TestApp: two fragments declaring the same
variable gave CE0111, and a fragment reading a variable a drop in the
same statement removed gave CE0109, after "Altered microflow". The
context now tracks what earlier operations declared, read and removed.
Also count a fragment loop's iterator as the fragment's own, so loop
fragments are no longer refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dl 1

`first` is Mendix's "First object" range in every language version. Under
the mdl 1 header a bare `limit 1` is a Custom range, a list of one; without
it the alpha meaning (the object) is kept and warns MDL-V1-LIMIT1. The
visitor resolves the meaning into RetrieveStmt.First, so the writer, the
variable typing and MDL-RETRIEVE01 no longer read limit text. describe
prints the object range as `first`. `first` on an association retrieve
is refused (that source has no range).

MDL-RETRIEVE01 keys on the object range and names CE0100 for a loop, as
measured with mx check 11.13.

Tests: visitor per version, builder range per version, check per version,
describe spelling, and a PedApp round trip of both forms with a headerless
control.

Part of #734.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nder mdl 1

CLAUDE.md idiom 5, the quick reference, the syntax topic, the skills and
docs-site pages that taught `limit 1` for an object now write `first`
and say what `limit 1` means per language version. CHANGELOG entry.

Part of #734.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ako
ako merged commit bf89a77 into main Sep 27, 2026
17 checks passed
ako added a commit that referenced this pull request Sep 27, 2026
…forms

On top of #740/#745/#741/#738, `fmt --upgrade --header` refused almost every
example script: those PRs gated new constructs on the header without a
rewrite. Each construct with a mechanical, meaning-preserving rewrite now has
one, computed from the parse tree by the visitor that records it (ast.Fix,
rune-offset edits, mdl/visitor/visitor_upgrade_fixes.go):

- MDL-V1-SEMI: add the missing `;`. MDL-V1-SLASH: delete the `/` line.
- MDL-V1-ESCAPE: write the literal's mdl 0 value with '' as the only escape;
  no edit inside an expression stored as written (mdl 0 already passed the
  backslash through); an escaped line break in a re-rendered expression is
  reported, since writing it into the literal changes what the builder
  stores (measured: a log message becomes a `{1}` template parameter).
- MDL-V1-LIMIT1: `limit 1` -> `first`. MDL-V1-SET: add `set`.
- MDL-DEPR003/004 and MDL-V1-LIST: call form -> statement form; find and
  contains on a declared String keep the call and gain `set`; a nested call,
  or an operand whose type the script does not state, is reported.
- MDL-V1-REPLACE02: `create or replace user role|demo user` -> `create`.

MDL-V1-PROP, MDL-V1-PROPVALUE and MDL-V1-REPLACE01 have no mechanical
rewrite; they are listed in `unrewritable` (may only shrink) and block the
header with HeaderBlockedError, which names each construct and why.
TestGatedRegistryIsComplete holds every visitor.LanguageChanges() code to
exactly one of the two lists.

The examples test lists the two corpus scripts that keep mdl 0
(keepsItsVersion) and the two whose AST differs but whose model is the same
(buildsTheSameModelNotTheSameAST), both may only shrink. The execute-both
property test upgrades a blocked script without the header, and by default
skips scripts whose only edits are the header and terminators, which never
reach the AST: 249 scripts execute (6 min); MXCLI_UPGRADE_ALL=1 runs all 701,
587 of them executing to the same model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

retrieve … first vs limit 1: limit 1 is a list under mdl 1

1 participant