From 6100e4c376f1a6be30b70e04b6088b689cb2ce4c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 20:08:21 +0000 Subject: [PATCH 1/2] feat: add Mendix 11.13 to the nightly matrix, and fix the drift it exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding '11.13.0' to the matrix on its own would have shipped a red nightly: 11.13 replaced the integer QueryType on DatabaseConnector$DatabaseQuery with a `Type` string enum ("Select" / "NonSelect" / "Unknown"), and ships a one-time conversion (ExternalDatabaseConnectionQueryTypeConversion) for older documents. mxcli wrote the legacy integer unconditionally, so on 11.13 the new property was simply absent — and an absent `Type` reads as Unknown: [error] [CE5277] "Please re-run and save the query to fix the error" at Execute database query action activity 'Query external database' once per activity, on both engines. The queries themselves report nothing, so it reads like a microflow defect. mdl/dbconnector decides the spelling from the project's Mendix version and writes exactly one of them. Writing both is not a safe hedge: a property the target's metamodel does not define is the shape Studio Pro fails to resolve on open. The read side accepts either, or the next ALTER of an 11.13 project would write Unknown straight back. mxcli never connects to the database, so it cannot derive the type the way Studio Pro does (running the query and inspecting the result set). It reads the leading SQL keyword instead — still better than Mendix's own converter, which marks every migrated query Select regardless of statement. A value already stored outranks the heuristic, so a round-trip preserves what Studio Pro derived. Verified with mx check on real projects: 11.13.0 given the doctype corpus is clean on both engines (a pre-fix binary reproduces CE5277 exactly); 11.12.2, 11.6.6 and 10.24 still store the legacy integer and still pass. Full integration suite green against 11.13. Found but NOT fixed, and documented as such: 11.13 also added CE5278 ("The JDBC driver is missing from the module settings"), a check on the module's Java dependencies rather than on anything mxcli writes. mxcli has no way to author module settings. It does not affect the nightly, whose harness imports the connector mpk. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA --- .claude/skills/fix-issue.md | 1 + .github/workflows/nightly.yml | 2 +- CLAUDE.md | 9 + .../WIDGET_BSON_VERSION_COMPATIBILITY.md | 40 +++++ .../1113-database-query-type-enum.mdl | 68 ++++++++ mdl/backend/modelsdk/db_write.go | 29 +++- .../modelsdk/db_write_querytype_test.go | 154 ++++++++++++++++++ mdl/backend/modelsdk/integration_read.go | 23 ++- mdl/dbconnector/querytype.go | 128 +++++++++++++++ mdl/dbconnector/querytype_test.go | 82 ++++++++++ model/types.go | 9 +- sdk/mpr/parser_dbconnection.go | 10 +- sdk/mpr/writer_dbconnection.go | 36 +++- sdk/mpr/writer_dbconnection_querytype_test.go | 122 ++++++++++++++ sdk/mpr/writer_id_order_test.go | 2 +- 15 files changed, 695 insertions(+), 20 deletions(-) create mode 100644 mdl-examples/bug-tests/1113-database-query-type-enum.mdl create mode 100644 mdl/backend/modelsdk/db_write_querytype_test.go create mode 100644 mdl/dbconnector/querytype.go create mode 100644 mdl/dbconnector/querytype_test.go create mode 100644 sdk/mpr/writer_dbconnection_querytype_test.go diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 0536a7267..2cfedb935 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -320,6 +320,7 @@ cases for these three BSON types — they fell to `default: return nil`. | `describe` (and `context` / `diff-local`) renders a Retrieve's XPath with only its **first** predicate group — `where A/B[EndDate = $X];` when the BSON holds `[A/B[EndDate = $X]][Status != 'Completed'][CompletionDate = empty]`. No warning; the output reads as a complete but materially *less restrictive* query, so correct defensive code looks buggy | The grammar's `xpathConstraint` rule matches ONE bracket group, and Mendix concatenates siblings. `ParseXPathConstraint` removes the error listeners, so ANTLR parsed group 1, left the rest on the token stream, and **still returned ok=true**; `enrichXPathConstraintForDescribe` treated that as a full parse and re-rendered only what came back. The `if !ok { return original }` fallback never fired | `mdl/visitor/visitor_xpath_public.go` (`ParseXPathConstraint`), `mdl/visitor/xpath_groups.go` (`SplitXPathPredicateGroups`), `mdl/executor/cmd_microflows_format_action.go` (`enrichXPathGroups`, and the render-path split) | Two layers. (1) Reject a partial parse — after the rule, require `stream.LA(1) == antlr.TokenEOF`; that alone stops the loss, since the caller then falls back to the stored string. (2) Split into top-level groups and enrich each, so enrichment still reaches groups after the first. The splitter must track **nesting depth and quoting**: a naive `][` split mangles a nested `[A/B[x = 1]]` and a literal containing `]`. **Generalisable**: a parser that silently accepts a prefix is worse than one that fails — any `ok` returned by a rule that can match less than its input must be checked against EOF before callers treat it as lossless. Repro `mdl-examples/bug-tests/772-xpath-constraint-groups.mdl`; A/B against a pre-fix binary on the same project shows the two dropped groups. Issue #772 | | An import/export mapping over an entity created with `EXTENDS` maps only its **own** attributes; every inherited field shows unmapped in Studio Pro, and `mx check` reports CE1613 "The selected attribute 'Mod.Child.Attr' no longer exists". An inherited Boolean/DateTime element also gets `DataType=String` | The mapping builder prefixed the entity being mapped unconditionally (`attr = parentEntity + "." + attr`), but a member reference is qualified against the entity that **declares** it — the same rule as entity access rules (#758) and the change-object writer (#451). Separately `resolveAttributeType` scanned only the entity's own attributes and fell through to its `"String"` default | `mdl/executor/cmd_import_mappings.go` and `cmd_export_mappings.go` (both carry the same two lines), `mdl/executor/entity_hierarchy.go` (`ResolveMemberRef`, `ResolveMemberType`) | Route both sites through the generalization walk added for #758: `ResolveMemberRef` returns the declaring-entity reference and `ResolveMemberType` finds the type up the chain, each falling back to the old behaviour when the member cannot be resolved. **Watch for the sibling defect**: the old `resolveAttributeType` matched entities **by name across every domain model**, so a same-named entity in another module could win — resolve the module by name instead. **Generalisable**: when one rule has several call sites, a fix at one of them proves nothing about the others; grep for the *pattern* (`range entity.Attributes`, `parentEntity + "."`) rather than the reported symptom. Repro `mdl-examples/bug-tests/703-mapping-inherited-attributes.mdl`; A/B on the same project shows `Map703.Contract.DocName` (CE1613) become `Map703.DocumentBase.DocName`. Issue #703, umbrella #765 | | `alter settings model JavaVersion = 'Java21'` on Mendix 11.12+ produces a project mxbuild refuses to **load**: `mx check` reports `System.ArgumentOutOfRangeException ... (Parameter 'majorVersion is an unsupported value: Java21')` at `JavaVersionExtensions.fromString`. Every check downstream of the settings unit is lost with it | Mendix renamed the property between 11.6 (`JavaVersion` = `"Java21"`) and 11.12 (`JavaMajorVersion` = `"21"`) — and the rename changed the **value format** as well as the key. The #759 fix followed only the key, writing the caller's value through verbatim, so the 11.6 spelling landed in the 11.12 key | `mdl/settingsoverlay/settingsoverlay.go` (`JavaVersionValue`, `SetJavaVersion`) — shared by both engines; the dead third copy in `modelsdk/mpr/serialize_services.go` carried it too | Render the value in the dialect the stored key expects: strip/add the `Java` prefix per key, and pass an unrecognisable value through untouched so a typo surfaces as a Mendix error instead of a mangled setting. **Generalisable**: a renamed property is not only a renamed key — check whether the value encoding moved with it, and cover *both* directions (either spelling in, document's dialect out). Note the sharper failure mode: the original #759 shape was an unknown property, which mxbuild **tolerates**, so only Studio Pro broke; a wrong *value* for a known enum is a hard build failure, which is why this one surfaced as a red nightly rather than a user report. Repro `mdl-examples/bug-tests/759-java-version-value-dialect.mdl`. Issue #759 (follow-up) | +| On **Mendix 11.13 only**, every microflow using `EXECUTE DATABASE QUERY` fails `mx check` with **CE5277** "Please re-run and save the query to fix the error", once per activity. The queries themselves report nothing — the error lands on the *activities* pointing at them, so it reads like a microflow defect. Both engines. 11.12 and below are clean | 11.13 replaced the integer `QueryType` (1 = custom SQL) on `DatabaseConnector$DatabaseQuery` with a `Type` **string enum** (`Select` / `NonSelect` / `Unknown`), shipping a one-time conversion (`ExternalDatabaseConnectionQueryTypeConversion`) for old documents. mxcli wrote the legacy integer unconditionally, so on 11.13 the new property was simply **absent** — and an absent `Type` reads as Unknown, which is exactly what CE5277 reports | `mdl/dbconnector/querytype.go` (new, shared by both engines), `sdk/mpr/writer_dbconnection.go` + `parser_dbconnection.go`, `mdl/backend/modelsdk/db_write.go` + `integration_read.go`, `model/types.go` (`DatabaseQuery.QueryTypeName`) | Branch on the project's Mendix version (`ProjectVersion().IsAtLeast(11, 13)`) and write **exactly one** spelling. Writing both is not a safe hedge — a property the target's metamodel does not define is the #759 Studio-Pro-won't-open shape. Read side must accept either, or the next ALTER of an 11.13 project writes Unknown straight back. mxcli can't derive the type the way Studio Pro does (running the query and inspecting the result set), so it reads the leading SQL keyword — still better than Mendix's own converter, which marks every migrated query `Select` regardless of statement. **Diagnosis method**: `mx convert -p -s ` with the NEW mxbuild runs the version's own migration, then diff the BSON — that is what showed `QueryType: 1` → `Type: "Select"` without guessing. **Generalisable**: onboarding a new Mendix minor is not just adding it to the nightly matrix — run the doctype corpus against it first (`MX_BINARY=~/.mxcli/mxbuild//modeler/mx go test -tags integration -run TestMxCheck_DoctypeScripts`), because a renamed property surfaces as a red matrix job, not a compile error. Repro `mdl-examples/bug-tests/1113-database-query-type-enum.mdl`. Sibling drift found in the same sweep and deliberately NOT fixed: **CE5278** ("The JDBC driver is missing from the module settings"), a new 11.13 check about the module's Java dependencies, which mxcli has no way to author | **Key insight:** `microflows$ListRange` stores offset/limit inside a nested `CustomRange` map — must cast `raw["CustomRange"].(map[string]any)` before diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index feff6cb88..f1987fb35 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - mendix-version: ['10.24.19.104498', '11.6.6', '11.12.0'] + mendix-version: ['10.24.19.104498', '11.6.6', '11.12.0', '11.13.0'] fail-fast: false name: test (Mendix ${{ matrix.mendix-version }}) steps: diff --git a/CLAUDE.md b/CLAUDE.md index ac29c771a..e4a512c89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -235,6 +235,15 @@ Enum-valued properties are the sibling trap: validate against `generated/metamodel` (e.g. `SettingsDatabaseType` is `Hsqldb`, never `HSQLDB`) rather than passing a user string through. +**On a CREATE there is no stored document to read the key off.** Rule 1 then +becomes: branch on the project's Mendix version and write exactly one spelling — +never both as a hedge. `mdl/dbconnector` does this for the 11.13 rename of +`DatabaseQuery.QueryType` (int) to `Type` (string enum), which mxbuild *does* +catch, as CE5277 on every activity using the query. To learn the target shape +without guessing, run the new mxbuild's own migration over an old project +(`mx convert -p -s `) and diff the BSON: Mendix ships a one-time +conversion per renamed property, so the converted document is authoritative. + ### Association Parent/Child Pointer Semantics (Counter-Intuitive) **CRITICAL**: Mendix BSON uses inverted naming for association pointers: diff --git a/docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md b/docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md index fcb9526fa..025e7b98c 100644 --- a/docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md +++ b/docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md @@ -276,6 +276,46 @@ manual patching after CE0463 reports come in. In the meantime, this doc + the `.claude/skills/debug-bson.md` methodology keep the patch cadence manageable. +## 11.13 onboarding: the drift was outside the widget layer + +Adding a Mendix minor to the nightly matrix is not a one-line change to +`.github/workflows/nightly.yml` — run the doctype corpus against it first: + +```bash +mxcli setup mxbuild --version 11.13.0 +MX_BINARY=~/.mxcli/mxbuild/11.13.0/modeler/mx \ + go test -tags integration -count=1 -run TestMxCheck_DoctypeScripts ./mdl/executor/ +``` + +11.13 produced **no** widget-envelope drift. The one failure came from a +different layer entirely — the Database Connector — and shows the shape of +problem to expect from any new minor: a renamed, re-encoded property. + +| Construct | ≤ 11.12 | 11.13 | Cause | +|---|---|---|---| +| any `EXECUTE DATABASE QUERY` activity | accepted | **CE5277** "Please re-run and save the query to fix the error" | `DatabaseConnector$DatabaseQuery.QueryType` (int, 1 = custom SQL) became `Type` (string enum `Select`/`NonSelect`/`Unknown`). mxcli wrote only the legacy key, so `Type` was absent — and absent reads as Unknown. Fixed in `mdl/dbconnector` | +| a `PostgreSQL`/`MSSQL` connection in a bare project | accepted | **CE5278** "The … JDBC driver is missing from the module settings" | A new check on the module's Java dependencies, not on anything mxcli writes. Not fixed: mxcli has no way to author module settings. Invisible in the doctype harness, which imports the connector mpk | + +**The method that settled it without guessing**: run the *new* mxbuild's own +migration over an old project and diff the result. + +```bash +mx convert -p -s /path/to/11.12-project # with the 11.13 mx binary +``` + +Mendix ships a one-time conversion per renamed property +(`ExternalDatabaseConnectionQueryTypeConversion` here), so the converted +document *is* the authoritative target shape. This is the non-widget counterpart +of the "Studio Pro Update Widget" diff above, and it needs no Studio Pro. + +Two rules carried over from the settings rename (#759) apply verbatim: + +1. **Write exactly one spelling.** Writing both is not a safe hedge — a property + the target version's metamodel does not define is what Studio Pro fails to + resolve on open. mxbuild tolerates it, so `mx check` is not the gate here. +2. **The read side must accept either**, or the next ALTER of a new-version + project writes the old key's default straight back over the new one. + ## References - [`.claude/skills/debug-bson.md`](../../.claude/skills/debug-bson.md) — investigation procedure for CE0463 and related widget BSON errors diff --git a/mdl-examples/bug-tests/1113-database-query-type-enum.mdl b/mdl-examples/bug-tests/1113-database-query-type-enum.mdl new file mode 100644 index 000000000..fe12b06be --- /dev/null +++ b/mdl-examples/bug-tests/1113-database-query-type-enum.mdl @@ -0,0 +1,68 @@ +-- Mendix 11.13: DatabaseQuery.QueryType became the `Type` string enum +-- +-- Symptom (on an 11.13 project only): every microflow using EXECUTE DATABASE +-- QUERY fails `mx check`, once per activity: +-- +-- [error] [CE5277] "Please re-run and save the query to fix the error" +-- at Execute database query action activity 'Query external database' +-- +-- The queries themselves report nothing — the error lands on the activities that +-- point at them, which is why it reads like a microflow problem. +-- +-- Cause: Mendix 11.13 replaced the integer `QueryType` (1 = custom SQL) on +-- DatabaseConnector$DatabaseQuery with a `Type` string enum ("Select" / +-- "NonSelect" / "Unknown"), and ships a one-time conversion +-- (ExternalDatabaseConnectionQueryTypeConversion) that rewrites old documents. +-- mxcli wrote the legacy integer unconditionally, so on 11.13 the new property +-- was simply absent — and an absent `Type` reads as Unknown, which is what +-- CE5277 reports. +-- +-- Fix: mdl/dbconnector decides the spelling from the project's Mendix version +-- and writes exactly one of them. Writing both is not a safe hedge: a property +-- the target version's metamodel does not define is what Studio Pro fails to +-- resolve on open (the #759 failure shape). +-- +-- Because mxcli never connects to the database, it cannot derive the type the +-- way Studio Pro does (by running the query and inspecting the result set). It +-- reads the leading SQL keyword instead — better than Mendix's own converter, +-- which marks every migrated query "Select" regardless of statement. +-- +-- Verify: run against an 11.13+ project, then `mx check` — no CE5277. On 11.12 +-- and below the same script stores the legacy integer and also checks clean. +-- +-- On a bare 11.13 project `mx check` still reports CE5278 ("The PostgreSQL JDBC +-- driver ... is missing from the module settings") — a SEPARATE check 11.13 added, +-- about the module's Java dependencies rather than anything mxcli writes. Add the +-- driver in Studio Pro; the doctype harness does not hit it because it imports the +-- connector mpk, which brings the dependency with it. + +create module DbType; + +create constant DbType.Dsn type string + default 'jdbc:postgresql://localhost:5432/demo'; +create constant DbType.User type string default 'demo'; +create constant DbType.Pass type string default ''; + +create database connection DbType.Warehouse +type 'PostgreSQL' +connection string @DbType.Dsn +username @DbType.User +password @DbType.Pass +begin + -- Reads: stored as Type = 'Select' on 11.13+. + query ListOrders + sql 'SELECT orderId, customer, total FROM orders ORDER BY orderId'; + + -- Writes: stored as Type = 'NonSelect'. The legacy integer could not express + -- this distinction at all. + query ArchiveOrders + sql 'UPDATE orders SET archived = true WHERE total = 0'; +end; + +-- The activity is where CE5277 surfaced, so the repro needs one. +create microflow DbType.MF_ListOrders() +begin + $Orders = execute database query DbType.Warehouse.ListOrders; +end; + +describe database connection DbType.Warehouse; diff --git a/mdl/backend/modelsdk/db_write.go b/mdl/backend/modelsdk/db_write.go index 46dbb1c65..8f16cf79a 100644 --- a/mdl/backend/modelsdk/db_write.go +++ b/mdl/backend/modelsdk/db_write.go @@ -5,6 +5,7 @@ package modelsdkbackend import ( "fmt" + "github.com/mendixlabs/mxcli/mdl/dbconnector" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/modelsdk/codec" "github.com/mendixlabs/mxcli/modelsdk/element" @@ -49,7 +50,7 @@ func (b *Backend) CreateDatabaseConnection(conn *model.DatabaseConnection) error if conn.ID == "" { conn.ID = model.ID(mmpr.GenerateID()) } - g := databaseConnectionToGen(conn) + g := databaseConnectionToGen(conn, b.storesQueryTypeEnum()) contents, err := (&codec.Encoder{}).Encode(g) if err != nil { return fmt.Errorf("CreateDatabaseConnection: encode: %w", err) @@ -67,7 +68,7 @@ func (b *Backend) UpdateDatabaseConnection(conn *model.DatabaseConnection) error if b.writer == nil { return fmt.Errorf("UpdateDatabaseConnection: not connected for writing") } - g := databaseConnectionToGen(conn) + g := databaseConnectionToGen(conn, b.storesQueryTypeEnum()) contents, err := (&codec.Encoder{}).Encode(g) if err != nil { return fmt.Errorf("UpdateDatabaseConnection: encode: %w", err) @@ -86,10 +87,20 @@ func (b *Backend) DeleteDatabaseConnection(id model.ID) error { return b.writer.DeleteUnit(string(id)) } +// storesQueryTypeEnum reports whether this project stores a query's type under the +// Mendix 11.13+ `Type` key. An unreadable version falls back to the legacy key. +func (b *Backend) storesQueryTypeEnum() bool { + pv := b.ProjectVersion() + if pv == nil { + return false + } + return dbconnector.StoresTypeEnum(pv.MajorVersion, pv.MinorVersion) +} + // databaseConnectionToGen builds the DatabaseConnection element tree directly with // the verified storage keys. The gen/databaseconnector setters bind different // property keys, so this mirrors sdk/mpr.serializeDatabaseConnection field-for-field. -func databaseConnectionToGen(conn *model.DatabaseConnection) element.Element { +func databaseConnectionToGen(conn *model.DatabaseConnection, typeEnum bool) element.Element { e := newElem("DatabaseConnector$DatabaseConnection", string(conn.ID)) addStr(e, "Name", conn.Name) addStr(e, "DatabaseType", conn.DatabaseType) @@ -107,7 +118,7 @@ func databaseConnectionToGen(conn *model.DatabaseConnection) element.Element { queries := make([]element.Element, 0, len(conn.Queries)) for _, q := range conn.Queries { - queries = append(queries, databaseQueryToGen(q)) + queries = append(queries, databaseQueryToGen(q, typeEnum)) } addPartList(e, "Queries", queries) @@ -118,11 +129,17 @@ func databaseConnectionToGen(conn *model.DatabaseConnection) element.Element { } // databaseQueryToGen builds a DatabaseConnector$DatabaseQuery sub-element. -func databaseQueryToGen(q *model.DatabaseQuery) element.Element { +func databaseQueryToGen(q *model.DatabaseQuery, typeEnum bool) element.Element { e := newElem("DatabaseConnector$DatabaseQuery", string(q.ID)) addStr(e, "Name", q.Name) addStr(e, "Query", q.SQL) - addInt64(e, "QueryType", int64(q.QueryType)) + // Exactly one of the two spellings — writing the other invents a property the + // target version's metamodel does not define. See mdl/dbconnector. + if typeEnum { + addStr(e, dbconnector.TypeKey, dbconnector.TypeToWrite(q.QueryTypeName, q.SQL)) + } else { + addInt64(e, dbconnector.QueryTypeKey, int64(q.QueryType)) + } mappings := make([]element.Element, 0, len(q.TableMappings)) for _, m := range q.TableMappings { diff --git a/mdl/backend/modelsdk/db_write_querytype_test.go b/mdl/backend/modelsdk/db_write_querytype_test.go new file mode 100644 index 000000000..cc3d1f0c0 --- /dev/null +++ b/mdl/backend/modelsdk/db_write_querytype_test.go @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 + +package modelsdkbackend + +import ( + "database/sql" + "os" + "path/filepath" + "testing" + + "github.com/mendixlabs/mxcli/mdl/dbconnector" + "github.com/mendixlabs/mxcli/model" + "go.mongodb.org/mongo-driver/bson" + + _ "modernc.org/sqlite" +) + +// TestCreateDatabaseConnection_QueryTypeSpellingFollowsVersion is the wiring proof +// for the Mendix 11.13 query-type rename. The encoding itself is covered by the +// unit tests in mdl/dbconnector; what this asserts is that the project's version +// actually reaches the writer — a correct mapping nothing consults would still +// have produced the CE5277 that made the 11.13 nightly red. +func TestCreateDatabaseConnection_QueryTypeSpellingFollowsVersion(t *testing.T) { + tests := []struct { + name string + productVer string + wantKey string + wantValue any + absentKey string + }{ + { + name: "mendix_11_6_writes_legacy_int", + productVer: "11.6.6", + wantKey: dbconnector.QueryTypeKey, + wantValue: int64(dbconnector.CustomSQLQueryType), + absentKey: dbconnector.TypeKey, + }, + { + name: "mendix_11_13_writes_type_enum", + productVer: "11.13.0", + wantKey: dbconnector.TypeKey, + wantValue: dbconnector.TypeSelect, + absentKey: dbconnector.QueryTypeKey, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + proj := copyFixture(t) + setProductVersion(t, proj, tc.productVer) + + b := New() + if err := b.Connect(proj); err != nil { + t.Fatalf("connect: %v", err) + } + mod, err := b.GetModuleByName("MyFirstModule") + if err != nil || mod == nil { + t.Fatalf("GetModuleByName: %v", err) + } + conn := &model.DatabaseConnection{ + ContainerID: mod.ID, + Name: "TestDB", + DatabaseType: "PostgreSQL", + Queries: []*model.DatabaseQuery{{ + Name: "GetAll", + SQL: "SELECT id FROM t", + QueryType: dbconnector.CustomSQLQueryType, + }}, + } + if err := b.CreateDatabaseConnection(conn); err != nil { + t.Fatalf("CreateDatabaseConnection: %v", err) + } + if err := b.Disconnect(); err != nil { + t.Fatalf("disconnect: %v", err) + } + + q := readFirstQuery(t, proj, string(conn.ID)) + if got, ok := q[tc.wantKey]; !ok || got != tc.wantValue { + t.Errorf("%s = %#v (present=%v), want %#v", tc.wantKey, got, ok, tc.wantValue) + } + if v, ok := q[tc.absentKey]; ok { + t.Errorf("wrote %s = %#v; Mendix %s stores %s", + tc.absentKey, v, tc.productVer, tc.wantKey) + } + }) + } +} + +// setProductVersion rewrites the fixture's recorded Mendix version, standing in +// for a project created by that release. +func setProductVersion(t *testing.T, proj, ver string) { + t.Helper() + db, err := sql.Open("sqlite", proj) + if err != nil { + t.Fatalf("open mpr: %v", err) + } + defer db.Close() + if _, err := db.Exec("UPDATE _MetaData SET _ProductVersion = ?", ver); err != nil { + t.Fatalf("set _ProductVersion: %v", err) + } +} + +// readFirstQuery returns the first Queries entry of a stored DatabaseConnection. +func readFirstQuery(t *testing.T, proj, unitID string) map[string]any { + t.Helper() + raw := readUnitBytes(t, proj, unitID) + var doc bson.M + if err := bson.Unmarshal(raw, &doc); err != nil { + t.Fatalf("unmarshal unit: %v", err) + } + arr, ok := doc["Queries"].(bson.A) + if !ok || len(arr) < 2 { + t.Fatalf("Queries = %#v, want a marker plus at least one query", doc["Queries"]) + } + q, ok := arr[1].(bson.D) + if !ok { + m, ok2 := arr[1].(bson.M) + if !ok2 { + t.Fatalf("query entry has type %T", arr[1]) + } + return m + } + out := make(map[string]any, len(q)) + for _, e := range q { + out[e.Key] = e.Value + } + return out +} + +// readUnitBytes returns a unit's stored contents. MPR v2 keeps each unit in its +// own file under mprcontents/; v1 keeps them in the Unit table. +func readUnitBytes(t *testing.T, proj, unitID string) []byte { + t.Helper() + dir := filepath.Dir(proj) + matches, _ := filepath.Glob(filepath.Join(dir, "mprcontents", "*", "*", unitID+".mxunit")) + if len(matches) == 1 { + b, err := os.ReadFile(matches[0]) + if err != nil { + t.Fatalf("read unit file: %v", err) + } + return b + } + + db, err := sql.Open("sqlite", proj) + if err != nil { + t.Fatalf("open mpr: %v", err) + } + defer db.Close() + var contents []byte + row := db.QueryRow("SELECT Contents FROM Unit WHERE hex(UnitID) = hex(?)", unitID) + if err := row.Scan(&contents); err != nil { + t.Fatalf("read unit %s: %v", unitID, err) + } + return contents +} diff --git a/mdl/backend/modelsdk/integration_read.go b/mdl/backend/modelsdk/integration_read.go index 3f5c9e835..fe000731d 100644 --- a/mdl/backend/modelsdk/integration_read.go +++ b/mdl/backend/modelsdk/integration_read.go @@ -3,6 +3,7 @@ package modelsdkbackend import ( + "github.com/mendixlabs/mxcli/mdl/dbconnector" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/modelsdk/element" genBe "github.com/mendixlabs/mxcli/modelsdk/gen/businessevents" @@ -443,10 +444,18 @@ func (b *Backend) ListDatabaseConnections() ([]*model.DatabaseConnection, error) if !ok { continue } + // Mendix 11.13 replaced the integer QueryType with the `Type` string + // enum, which the gen accessor does not know; read it off the raw + // document so a round-trip preserves the stored value. dq := &model.DatabaseQuery{ - Name: q.Name(), - SQL: q.Query(), - QueryType: int(q.QueryType()), + Name: q.Name(), + SQL: q.Query(), + QueryTypeName: rawQueryTypeName(q), + } + if dq.QueryTypeName != "" { + dq.QueryType = dbconnector.LegacyQueryTypeFor(dq.QueryTypeName) + } else { + dq.QueryType = int(q.QueryType()) } dq.ID = model.ID(q.ID()) dq.TypeName = "DatabaseConnector$DatabaseQuery" @@ -466,3 +475,11 @@ func firstNonEmpty(vals ...string) string { } return "" } + +// rawQueryTypeName reads a query's Mendix 11.13+ `Type` enum member off the raw +// document. The gen accessor binds only the legacy integer QueryType, so a +// project written by 11.13 has no typed accessor for what it actually stores. +func rawQueryTypeName(q *genDb.DatabaseQuery) string { + v, _ := q.Raw().Lookup(dbconnector.TypeKey).StringValueOK() + return v +} diff --git a/mdl/dbconnector/querytype.go b/mdl/dbconnector/querytype.go new file mode 100644 index 000000000..47c47b9dc --- /dev/null +++ b/mdl/dbconnector/querytype.go @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package dbconnector holds the version-dependent storage rules for the Mendix +// Database Connector documents, shared by both write engines so the two cannot +// drift apart: the codec engine (mdl/backend/modelsdk) and the legacy engine +// (sdk/mpr). +// +// Today that is one rule — how a query's type is stored — but it is the same +// shape of problem as mdl/settingsoverlay: a property Mendix renamed and +// re-encoded between minor versions, where writing the wrong spelling produces a +// document the target version rejects. +package dbconnector + +import "strings" + +// The two spellings of a DatabaseQuery's type. They differ in value encoding as +// well as in name, so the key alone does not determine what to write. +// +// Mendix 11.13 replaced the integer QueryType with a string enum under a new +// `Type` key. Its own one-time conversion (ExternalDatabaseConnectionQueryTypeConversion) +// rewrites `QueryType: 1` to `Type: "Select"` and drops the old key entirely. +const ( + TypeKey = "Type" // Mendix 11.13+, string enum + QueryTypeKey = "QueryType" // Mendix <= 11.12, int (1 = custom SQL) +) + +// Members of the 11.13 query-type enumeration. An absent `Type` reads as Unknown, +// which is what CE5277 ("Please re-run and save the query to fix the error") +// reports on every Execute-database-query activity pointing at such a query. +const ( + TypeSelect = "Select" + TypeNonSelect = "NonSelect" + TypeUnknown = "Unknown" +) + +// CustomSQLQueryType is the legacy integer mxcli writes for a hand-written query +// (the only kind MDL can author); Mendix's converter maps it to TypeSelect. +const CustomSQLQueryType = 1 + +// StoresTypeEnum reports whether a project of the given Mendix major.minor stores +// the query type under the 11.13+ `Type` key rather than the legacy integer +// `QueryType`. Callers with no version information pass 0, 0. +// +// An unknown version is therefore treated as pre-11.13. That is the safe default: +// the legacy key is what every version through 11.12 expects, and 11.13 repairs a +// document carrying it via its one-time conversion — whereas writing `Type` onto +// an older project invents a property that version's metamodel does not define, +// which is the shape Studio Pro refuses to open. +// +// The two engines share this one predicate rather than each spelling out the +// version test, so a future rename cannot be fixed in one engine and missed in +// the other. +func StoresTypeEnum(major, minor int) bool { + return major > 11 || (major == 11 && minor >= 13) +} + +// TypeForSQL classifies a query by its statement so a freshly authored query gets +// a usable type instead of Unknown. +// +// Studio Pro derives the real value by executing the query and inspecting the +// result set, which mxcli cannot do — it never connects to the database. Reading +// the leading keyword is the closest honest approximation, and it is strictly +// better than what Mendix's own converter does to a pre-11.13 project (it marks +// every migrated query Select regardless of statement). A query whose type the +// heuristic gets wrong is corrected the same way CE5277 asks for: re-run and save +// it in Studio Pro. +func TypeForSQL(sql string) string { + switch firstKeyword(sql) { + case "": + return TypeUnknown + case "select", "with", "show", "describe", "explain", "values", "table": + return TypeSelect + default: + return TypeNonSelect + } +} + +// firstKeyword returns the lower-cased first word of a SQL statement, skipping +// leading whitespace, line comments and block comments. +func firstKeyword(sql string) string { + s := sql + for { + // A parenthesised statement — `(SELECT ...)` — leads with the paren. + s = strings.TrimLeft(s, " \t\r\n(") + switch { + case strings.HasPrefix(s, "--"): + if i := strings.IndexByte(s, '\n'); i >= 0 { + s = s[i+1:] + continue + } + return "" + case strings.HasPrefix(s, "/*"): + if i := strings.Index(s[2:], "*/"); i >= 0 { + s = s[i+4:] + continue + } + return "" + } + break + } + end := strings.IndexFunc(s, func(r rune) bool { + return r == ' ' || r == '\t' || r == '\r' || r == '\n' || r == '(' || r == ';' + }) + if end < 0 { + end = len(s) + } + return strings.ToLower(s[:end]) +} + +// TypeToWrite returns the `Type` value for a query: the one already stored when +// the query round-trips through mxcli (Studio Pro may have set a value the SQL +// heuristic would not derive), otherwise one derived from the SQL. +func TypeToWrite(stored, sql string) string { + if stored != "" { + return stored + } + return TypeForSQL(sql) +} + +// LegacyQueryTypeFor maps a stored 11.13 `Type` back to the legacy integer, for +// the semantic model's QueryType field. Every authored query is custom SQL; +// only an Unknown type has no legacy counterpart. +func LegacyQueryTypeFor(typeName string) int { + if typeName == TypeUnknown || typeName == "" { + return 0 + } + return CustomSQLQueryType +} diff --git a/mdl/dbconnector/querytype_test.go b/mdl/dbconnector/querytype_test.go new file mode 100644 index 000000000..da7994f40 --- /dev/null +++ b/mdl/dbconnector/querytype_test.go @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 + +package dbconnector + +import "testing" + +// TestStoresTypeEnum pins the version boundary. Mendix 11.13 replaced the integer +// QueryType with the `Type` string enum; writing the wrong one produces either a +// query the target rejects (CE5277 on every activity using it) or a property the +// older metamodel does not define. +func TestStoresTypeEnum(t *testing.T) { + tests := []struct { + major, minor int + want bool + }{ + {0, 0, false}, // unknown version — fall back to the legacy key + {10, 24, false}, + {11, 6, false}, + {11, 12, false}, + {11, 13, true}, + {11, 20, true}, + {12, 0, true}, + } + for _, tc := range tests { + if got := StoresTypeEnum(tc.major, tc.minor); got != tc.want { + t.Errorf("StoresTypeEnum(%d, %d) = %v, want %v", tc.major, tc.minor, got, tc.want) + } + } +} + +func TestTypeForSQL(t *testing.T) { + tests := []struct { + sql string + want string + }{ + {"SELECT driverId FROM drivers", TypeSelect}, + {" select 1", TypeSelect}, + {"WITH d AS (SELECT 1) SELECT * FROM d", TypeSelect}, + {"(SELECT 1)", TypeSelect}, + {"UPDATE drivers SET forename = 'x'", TypeNonSelect}, + {"INSERT INTO drivers (forename) VALUES ('y')", TypeNonSelect}, + {"DELETE FROM drivers WHERE driverId = 2", TypeNonSelect}, + {"EXEC dbo.RefreshCache", TypeNonSelect}, + {"", TypeUnknown}, + {" \n\t ", TypeUnknown}, + // Leading comments must not be mistaken for the statement. + {"-- fetch everyone\nSELECT 1", TypeSelect}, + {"/* header */ UPDATE t SET a = 1", TypeNonSelect}, + {"-- only a comment", TypeUnknown}, + } + for _, tc := range tests { + if got := TypeForSQL(tc.sql); got != tc.want { + t.Errorf("TypeForSQL(%q) = %q, want %q", tc.sql, got, tc.want) + } + } +} + +// TestTypeToWrite_PrefersStored: Studio Pro derives the type by executing the +// query, so it can hold a value the SQL heuristic would not derive. A round-trip +// through mxcli must not overwrite it. +func TestTypeToWrite_PrefersStored(t *testing.T) { + if got := TypeToWrite(TypeSelect, "EXEC dbo.GetRows"); got != TypeSelect { + t.Errorf("TypeToWrite overwrote the stored value: got %q", got) + } + if got := TypeToWrite("", "EXEC dbo.GetRows"); got != TypeNonSelect { + t.Errorf("TypeToWrite(unstored) = %q, want %q", got, TypeNonSelect) + } +} + +func TestLegacyQueryTypeFor(t *testing.T) { + tests := map[string]int{ + TypeSelect: CustomSQLQueryType, + TypeNonSelect: CustomSQLQueryType, + TypeUnknown: 0, + "": 0, + } + for in, want := range tests { + if got := LegacyQueryTypeFor(in); got != want { + t.Errorf("LegacyQueryTypeFor(%q) = %d, want %d", in, got, want) + } + } +} diff --git a/model/types.go b/model/types.go index 23373e324..3b0fc206b 100644 --- a/model/types.go +++ b/model/types.go @@ -526,8 +526,13 @@ type DatabaseConnection struct { // DatabaseQuery represents a DatabaseConnector$DatabaseQuery. type DatabaseQuery struct { BaseElement - Name string `json:"name"` - QueryType int `json:"queryType"` // 1 = custom SQL + Name string `json:"name"` + QueryType int `json:"queryType"` // 1 = custom SQL (Mendix <= 11.12 storage) + // QueryTypeName is the Mendix 11.13+ `Type` enum member ("Select" / + // "NonSelect" / "Unknown"), which replaced the integer QueryType. Empty on a + // project that stores the legacy key, or on a query mxcli has just authored; + // see mdl/dbconnector. + QueryTypeName string `json:"queryTypeName,omitempty"` SQL string `json:"sql,omitempty"` // extracted from TableMappings TableMappings []*DatabaseTableMapping `json:"tableMappings,omitempty"` Parameters []*DatabaseQueryParameter `json:"parameters,omitempty"` diff --git a/sdk/mpr/parser_dbconnection.go b/sdk/mpr/parser_dbconnection.go index eb0be65f7..1b3192e12 100644 --- a/sdk/mpr/parser_dbconnection.go +++ b/sdk/mpr/parser_dbconnection.go @@ -4,6 +4,7 @@ package mpr import ( "fmt" + "github.com/mendixlabs/mxcli/mdl/dbconnector" "github.com/mendixlabs/mxcli/model" "go.mongodb.org/mongo-driver/bson" @@ -57,7 +58,14 @@ func parseDBQuery(raw map[string]any) *model.DatabaseQuery { q.TypeName = extractString(raw["$Type"]) q.Name = extractString(raw["Name"]) q.SQL = extractString(raw["Query"]) - q.QueryType = extractInt(raw["QueryType"]) + // Mendix 11.13 replaced the integer QueryType with the `Type` string enum; + // read whichever key this project stores so a round-trip preserves it. + q.QueryTypeName = extractString(raw[dbconnector.TypeKey]) + if q.QueryTypeName != "" { + q.QueryType = dbconnector.LegacyQueryTypeFor(q.QueryTypeName) + } else { + q.QueryType = extractInt(raw[dbconnector.QueryTypeKey]) + } // Parse TableMappings mappings := extractBsonArray(raw["TableMappings"]) diff --git a/sdk/mpr/writer_dbconnection.go b/sdk/mpr/writer_dbconnection.go index 5ba1a5fe8..1eee2ce91 100644 --- a/sdk/mpr/writer_dbconnection.go +++ b/sdk/mpr/writer_dbconnection.go @@ -5,6 +5,7 @@ package mpr import ( "fmt" + "github.com/mendixlabs/mxcli/mdl/dbconnector" "github.com/mendixlabs/mxcli/model" "go.mongodb.org/mongo-driver/bson" ) @@ -69,8 +70,9 @@ func (w *Writer) serializeDatabaseConnection(conn *model.DatabaseConnection) ([] // Serialize Queries queries := bson.A{int32(2)} // versioned array prefix + typeEnum := w.storesQueryTypeEnum() for _, q := range conn.Queries { - queries = append(queries, serializeDBQuery(q)) + queries = append(queries, serializeDBQuery(q, typeEnum)) } doc = append(doc, bson.E{Key: "Queries", Value: queries}) @@ -83,7 +85,20 @@ func (w *Writer) serializeDatabaseConnection(conn *model.DatabaseConnection) ([] return marshalUnitIDFirst(doc) } -func serializeDBQuery(q *model.DatabaseQuery) bson.D { +// storesQueryTypeEnum reports whether this project stores a query's type under the +// Mendix 11.13+ `Type` key. An unreadable version falls back to the legacy key. +func (w *Writer) storesQueryTypeEnum() bool { + if w.reader == nil { + return false + } + pv := w.reader.ProjectVersion() + if pv == nil { + return false + } + return dbconnector.StoresTypeEnum(pv.MajorVersion, pv.MinorVersion) +} + +func serializeDBQuery(q *model.DatabaseQuery, typeEnum bool) bson.D { id := string(q.ID) if id == "" { id = generateUUID() @@ -101,15 +116,24 @@ func serializeDBQuery(q *model.DatabaseQuery) bson.D { params = append(params, serializeDBQueryParameter(p)) } - return bson.D{ + doc := bson.D{ {Key: "$ID", Value: idToBsonBinary(id)}, {Key: "$Type", Value: "DatabaseConnector$DatabaseQuery"}, {Key: "Name", Value: q.Name}, {Key: "Query", Value: q.SQL}, - {Key: "QueryType", Value: int64(q.QueryType)}, - {Key: "TableMappings", Value: mappings}, - {Key: "Parameters", Value: params}, } + // Exactly one of the two spellings — writing the other invents a property the + // target version's metamodel does not define. See mdl/dbconnector. + if typeEnum { + doc = append(doc, bson.E{Key: dbconnector.TypeKey, + Value: dbconnector.TypeToWrite(q.QueryTypeName, q.SQL)}) + } else { + doc = append(doc, bson.E{Key: dbconnector.QueryTypeKey, Value: int64(q.QueryType)}) + } + return append(doc, + bson.E{Key: "TableMappings", Value: mappings}, + bson.E{Key: "Parameters", Value: params}, + ) } func serializeDBQueryParameter(p *model.DatabaseQueryParameter) bson.D { diff --git a/sdk/mpr/writer_dbconnection_querytype_test.go b/sdk/mpr/writer_dbconnection_querytype_test.go new file mode 100644 index 000000000..c6443218d --- /dev/null +++ b/sdk/mpr/writer_dbconnection_querytype_test.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mpr + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/dbconnector" + "github.com/mendixlabs/mxcli/model" + "go.mongodb.org/mongo-driver/bson" +) + +// TestSerializeDBQuery_QueryTypeSpellingFollowsVersion covers the Mendix 11.13 +// rename of the query-type property. 11.13 replaced the integer `QueryType` with +// the string enum `Type`; a query carrying only the legacy key reads as Unknown, +// which mxbuild reports as CE5277 ("Please re-run and save the query to fix the +// error") on every Execute-database-query activity pointing at it. +// +// Exactly one spelling must be written: the other is a property the target +// version's metamodel does not define, which is the shape Studio Pro refuses to +// open. +func TestSerializeDBQuery_QueryTypeSpellingFollowsVersion(t *testing.T) { + tests := []struct { + name string + typeEnum bool + wantKey string + wantValue any + absentKey string + }{ + { + name: "mendix_11_12_writes_legacy_int", + typeEnum: false, + wantKey: dbconnector.QueryTypeKey, + wantValue: int64(dbconnector.CustomSQLQueryType), + absentKey: dbconnector.TypeKey, + }, + { + name: "mendix_11_13_writes_type_enum", + typeEnum: true, + wantKey: dbconnector.TypeKey, + wantValue: dbconnector.TypeSelect, + absentKey: dbconnector.QueryTypeKey, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + q := &model.DatabaseQuery{ + Name: "GetAll", + SQL: "SELECT driverId FROM drivers", + QueryType: dbconnector.CustomSQLQueryType, + } + doc := docMap(serializeDBQuery(q, tc.typeEnum)) + + if got, ok := doc[tc.wantKey]; !ok || got != tc.wantValue { + t.Errorf("%s = %#v (present=%v), want %#v", tc.wantKey, got, ok, tc.wantValue) + } + if v, ok := doc[tc.absentKey]; ok { + t.Errorf("wrote %s = %#v; this Mendix version stores %s", + tc.absentKey, v, tc.wantKey) + } + }) + } +} + +// TestSerializeDBQuery_TypeFromStatement: mxcli never connects to the database, so +// it derives the 11.13 type from the statement rather than leaving it Unknown. +func TestSerializeDBQuery_TypeFromStatement(t *testing.T) { + tests := []struct { + sql string + stored string + want string + }{ + {sql: "SELECT 1", want: dbconnector.TypeSelect}, + {sql: "UPDATE drivers SET forename = 'x'", want: dbconnector.TypeNonSelect}, + // A value Studio Pro derived by running the query outranks the heuristic. + {sql: "EXEC dbo.GetRows", stored: dbconnector.TypeSelect, want: dbconnector.TypeSelect}, + } + for _, tc := range tests { + q := &model.DatabaseQuery{Name: "Q", SQL: tc.sql, QueryTypeName: tc.stored} + if got := docMap(serializeDBQuery(q, true))[dbconnector.TypeKey]; got != tc.want { + t.Errorf("Type for %q (stored %q) = %#v, want %q", tc.sql, tc.stored, got, tc.want) + } + } +} + +// TestParseDBQuery_ReadsEitherSpelling guards the read half: a project written by +// 11.13 has no QueryType at all, and reading 0 there would write Unknown straight +// back on the next ALTER. +func TestParseDBQuery_ReadsEitherSpelling(t *testing.T) { + legacy := parseDBQuery(map[string]any{ + "Name": "Q", + "Query": "SELECT 1", + dbconnector.QueryTypeKey: int32(dbconnector.CustomSQLQueryType), + "$Type": "DatabaseConnector$DatabaseQuery", + }) + if legacy.QueryType != dbconnector.CustomSQLQueryType || legacy.QueryTypeName != "" { + t.Errorf("legacy parse = %d/%q, want %d/\"\"", + legacy.QueryType, legacy.QueryTypeName, dbconnector.CustomSQLQueryType) + } + + modern := parseDBQuery(map[string]any{ + "Name": "Q", + "Query": "UPDATE t SET a = 1", + dbconnector.TypeKey: dbconnector.TypeNonSelect, + "$Type": "DatabaseConnector$DatabaseQuery", + }) + if modern.QueryTypeName != dbconnector.TypeNonSelect { + t.Errorf("QueryTypeName = %q, want %q", modern.QueryTypeName, dbconnector.TypeNonSelect) + } + if modern.QueryType != dbconnector.CustomSQLQueryType { + t.Errorf("QueryType = %d, want %d", modern.QueryType, dbconnector.CustomSQLQueryType) + } +} + +// docMap flattens a bson.D into a lookup keyed by property name. +func docMap(d bson.D) map[string]any { + out := make(map[string]any, len(d)) + for _, e := range d { + out[e.Key] = e.Value + } + return out +} diff --git a/sdk/mpr/writer_id_order_test.go b/sdk/mpr/writer_id_order_test.go index 32cf1552b..a24967c5d 100644 --- a/sdk/mpr/writer_id_order_test.go +++ b/sdk/mpr/writer_id_order_test.go @@ -216,7 +216,7 @@ func TestStorageObjects_IDIsFirstProperty(t *testing.T) { }}, Parameters: []*model.DatabaseQueryParameter{{ParameterName: "p1"}}, } - marshalAndValidate(t, "DBQuery", serializeDBQuery(q)) + marshalAndValidate(t, "DBQuery", serializeDBQuery(q, false)) }) // Server configuration: $ID added dynamically; nested ConstantValue list. From 88614f431cc2b7c5db2cd28cbd59b2e83e87f38a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 21:30:42 +0000 Subject: [PATCH 2/2] ci: pin every Mendix version in CI to its latest patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nightly matrix and push-test had drifted several patches behind: nightly 10.24.19.104498 -> 10.24.24.119349 11.6.6 -> 11.6.8 11.12.0 -> 11.12.2 push-test 11.9.0 -> 11.12.2 11.9 was two minors behind the nightly's newest 11.x, so the fast single-version gate on every push was validating against a Mendix nobody was targeting. It now matches a version the nightly also covers. Versions confirmed latest by listing the CDN bucket rather than probing names — https://cdn.mendix.com/?list-type=2&prefix=runtime/mxbuild-. That matters for 10.x, whose tarballs carry a build number that cannot be guessed from the release notes (10.24.24.119349). Each bump ran the full executor integration suite (doctype corpus + mx check, both engines) against the new binary before landing. A patch bump is not automatically safe: 11.13 shipped a renamed metamodel property in a minor, and the same class of change can land in a patch. 10.24.24.119349 ok 931s 0 failures 11.6.8 ok 1198s 0 failures 11.12.2 ok 1117s 0 failures 11.13.0 ok 1142s 0 failures (full ./... run) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA --- .github/workflows/nightly.yml | 2 +- .github/workflows/push-test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f1987fb35..4e021ad4e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - mendix-version: ['10.24.19.104498', '11.6.6', '11.12.0', '11.13.0'] + mendix-version: ['10.24.24.119349', '11.6.8', '11.12.2', '11.13.0'] fail-fast: false name: test (Mendix ${{ matrix.mendix-version }}) steps: diff --git a/.github/workflows/push-test.yml b/.github/workflows/push-test.yml index 0104238e0..455d4cdf1 100644 --- a/.github/workflows/push-test.yml +++ b/.github/workflows/push-test.yml @@ -39,7 +39,7 @@ jobs: - name: Check docs-site MDL blocks run: ./scripts/check-skill-mdl.sh ./bin/mxcli docs-site/src - name: Setup mxbuild - run: ./bin/mxcli setup mxbuild --version 11.9.0 + run: ./bin/mxcli setup mxbuild --version 11.12.2 - name: Integration tests run: make test-integration timeout-minutes: 30