From 16e550afbb87d6a757bd4e19e4779b20be7b60ab Mon Sep 17 00:00:00 2001 From: Andrej Koelewijn Date: Fri, 22 May 2026 20:52:14 +0000 Subject: [PATCH] fix #585: parse remaining numeric BSON fields across all numeric widths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six more numeric facets read with the same narrow `.(int32)` assertion fixed in #583 for StringAttributeType.Length. Studio Pro writes them as BSON int64; mxcli was silently returning 0 for all of them: - JsonElement.MinOccurs / MaxOccurs / MaxLength / FractionDigits / TotalDigits (parser_misc.go) — XSD facets in JSON Structures - ScheduledEvent.Interval (parser_enumeration.go) — scheduled-event cadence; misreport showed real events as "interval 0" Also collapsed parseClosePageAction's int32/int64 if-else chain (NumberOfPagesToClose) to the same extractInt helper for consistency. The five existing array-marker probes in parser.go and parser_microflow_actions.go are left as-is; those probe for `int32` intentionally because Mendix BSON-array prefix markers really are stored as int32 type discriminators, not as values to read. Added two test files mirroring the #583 template: - parser_misc_test.go — five JsonElement facets × four numeric widths - parser_scheduledevent_test.go — Interval across numeric widths Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/fix-issue.md | 2 +- sdk/mpr/parser_enumeration.go | 6 ++- sdk/mpr/parser_microflow_actions.go | 8 ++-- sdk/mpr/parser_misc.go | 24 ++++++----- sdk/mpr/parser_misc_test.go | 59 +++++++++++++++++++++++++++ sdk/mpr/parser_scheduledevent_test.go | 54 ++++++++++++++++++++++++ 6 files changed, 136 insertions(+), 17 deletions(-) create mode 100644 sdk/mpr/parser_misc_test.go create mode 100644 sdk/mpr/parser_scheduledevent_test.go diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 13795afee..47933e7a7 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -38,7 +38,7 @@ to the symptom table below, so the next similar issue costs fewer reads. | Pluggable widget `Selection` BSON value is lowercase (`single` / `multi` / `none`) but Studio Pro stores PascalCase — contributes to CE0463 drift on stricter widgets and looks wrong in diffs | MDL passes the user's typed value verbatim to `SetSelection`; the builder didn't normalise | `mdl/backend/mpr/widget_builder.go` `SetSelection` | Canonicalise via `canonicalSelectionValue` helper (lowercase-keyed switch → `Single` / `Multi` / `None`); unknown values pass through | | Nightly `mx check` reports `CE0117 "Error(s) in expression." at Log message activity 'Log message (warning)'` on Mendix 10.24.19+ but not 10.24.16 or 11.x | Mendix 10.24.19 tightened expression validation: `toString()` is now a type error (toString expects a non-string input). An example called `toString($OrderNumber)` where `$OrderNumber` was already a string parameter | The offending `log warning ... with ({1} = toString($stringVar))` — find via `~/.mxcli/mxbuild/{ver}/modeler/mx check`, then bisect with `drop microflow ...` until CE0117 disappears | Remove the redundant `toString()` wrapper around already-string values. Only wrap non-string values (integers, decimals, dates, enums) in `toString()`. The Mendix 11.x parser is more lenient and lets this slide, but 10.24.19+ rejects it | | Mendix can't resolve the microflow named in `CREATE ODATA CLIENT (ConfigurationMicroflow: microflow X.Y)` / `ErrorHandlingMicroflow:` — error names the literal string `"MICROFLOW X.Y"` as the missing microflow | Case-mismatched prefix strip: visitor emits uppercase `"MICROFLOW "` from `odataValueText`, but `extractMicroflowRef` only trimmed lowercase `"microflow "`, so the keyword survived into BSON | `mdl/executor/cmd_odata.go` → `extractMicroflowRef` | Use a case-insensitive strip: `if strings.EqualFold(ref[:10], "microflow ") { return ref[10:] }`. Whenever a value goes from a visitor that emits a keyword-prefixed form to an executor that strips it, the strip must match the case the visitor produces — grep visitor files for `"MICROFLOW " +`/`"ENTITY " +`/etc. when adding a new property. Issue #573 | -| `describe entity` shows every String attribute as `String(unlimited)` and `CATALOG.ATTRIBUTES.Length` reports `0` for all of them, even though Studio Pro shows a finite max length and runtime rejects overlong values | BSON numeric width mismatch — Mendix Studio Pro stores `Length` as int64, but `parseAttributeType` used `raw["Length"].(int32)` so the assertion always failed and Length defaulted to 0 | `sdk/mpr/parser_domainmodel.go` → `parseAttributeType` `case "DomainModels$StringAttributeType"` | Replace narrow type assertions on BSON numeric fields with the existing `extractInt(raw["X"])` helper (`sdk/mpr/parser.go`), which handles int32/int64/int/float64. Sweep other `parser_*.go` files for the same `.(int32)` / `.(int64)` pattern on size/length/count fields. Issue #583 | +| `describe`/catalog reports a numeric BSON field (Length, MinOccurs, MaxOccurs, MaxLength, FractionDigits, TotalDigits, Interval, NumberOfPagesToClose, …) as `0` / `unlimited` even though Studio Pro shows a real value | BSON numeric width mismatch — Studio Pro writes the field as `int64`, but the parser asserted `raw["X"].(int32)` so the type assertion failed silently and the field defaulted to its zero value | `sdk/mpr/parser_*.go` — grep for the field name; the fix point is the narrow assertion | Replace narrow type assertions on BSON numeric fields with the existing `extractInt(raw["X"])` helper (`sdk/mpr/parser.go`). It handles int32/int64/int/float64. When a non-zero default must survive a missing field, gate with `if _, ok := raw["X"]; ok { … = extractInt(...) }`. Sweep `grep -n '\.(int32)' sdk/mpr/parser_*.go` and ignore matches whose comment says "marker" (BSON-array-prefix probes are intentional). Issues #583, #585 | --- diff --git a/sdk/mpr/parser_enumeration.go b/sdk/mpr/parser_enumeration.go index 0defdfe5c..dfaac3bc9 100644 --- a/sdk/mpr/parser_enumeration.go +++ b/sdk/mpr/parser_enumeration.go @@ -197,8 +197,10 @@ func (r *Reader) parseScheduledEvent(unitID, containerID string, contents []byte if enabled, ok := raw["Enabled"].(bool); ok { event.Enabled = enabled } - if interval, ok := raw["Interval"].(int32); ok { - event.Interval = int(interval) + // Issue #585: Studio Pro stores Interval as BSON int64; extractInt + // also accepts int32/int/float64 emitted by other writers. + if _, ok := raw["Interval"]; ok { + event.Interval = extractInt(raw["Interval"]) } if intervalType, ok := raw["IntervalType"].(string); ok { event.IntervalType = intervalType diff --git a/sdk/mpr/parser_microflow_actions.go b/sdk/mpr/parser_microflow_actions.go index 74cd70857..2fcb7cd55 100644 --- a/sdk/mpr/parser_microflow_actions.go +++ b/sdk/mpr/parser_microflow_actions.go @@ -292,10 +292,10 @@ func parseShowHomePageAction(raw map[string]any) *microflows.ShowHomePageAction func parseClosePageAction(raw map[string]any) *microflows.ClosePageAction { action := µflows.ClosePageAction{} action.ID = model.ID(extractBsonID(raw["$ID"])) - if numPages, ok := raw["NumberOfPagesToClose"].(int32); ok { - action.NumberOfPages = int(numPages) - } else if numPages, ok := raw["NumberOfPagesToClose"].(int64); ok { - action.NumberOfPages = int(numPages) + // Issue #585: collapse the int32/int64 dispatch to the shared extractInt + // helper. Default of 1 is preserved when the field is absent. + if _, ok := raw["NumberOfPagesToClose"]; ok { + action.NumberOfPages = extractInt(raw["NumberOfPagesToClose"]) } else { action.NumberOfPages = 1 } diff --git a/sdk/mpr/parser_misc.go b/sdk/mpr/parser_misc.go index 2e6d403f5..bc83784ea 100644 --- a/sdk/mpr/parser_misc.go +++ b/sdk/mpr/parser_misc.go @@ -759,11 +759,15 @@ func parseJsonElement(raw map[string]any) *JsonElement { if v, ok := raw["PrimitiveType"].(string); ok { elem.PrimitiveType = v } - if v, ok := raw["MinOccurs"].(int32); ok { - elem.MinOccurs = int(v) + // Issue #585: Studio Pro writes these numeric facets as BSON int64; + // mxcli's writer emits int32. extractInt accepts both (plus int and + // float64). Default values for MaxLength/FractionDigits/TotalDigits + // stay at -1 (set in the literal above) when the field is absent. + if _, ok := raw["MinOccurs"]; ok { + elem.MinOccurs = extractInt(raw["MinOccurs"]) } - if v, ok := raw["MaxOccurs"].(int32); ok { - elem.MaxOccurs = int(v) + if _, ok := raw["MaxOccurs"]; ok { + elem.MaxOccurs = extractInt(raw["MaxOccurs"]) } if v, ok := raw["Nillable"].(bool); ok { elem.Nillable = v @@ -771,14 +775,14 @@ func parseJsonElement(raw map[string]any) *JsonElement { if v, ok := raw["IsDefaultType"].(bool); ok { elem.IsDefaultType = v } - if v, ok := raw["MaxLength"].(int32); ok { - elem.MaxLength = int(v) + if _, ok := raw["MaxLength"]; ok { + elem.MaxLength = extractInt(raw["MaxLength"]) } - if v, ok := raw["FractionDigits"].(int32); ok { - elem.FractionDigits = int(v) + if _, ok := raw["FractionDigits"]; ok { + elem.FractionDigits = extractInt(raw["FractionDigits"]) } - if v, ok := raw["TotalDigits"].(int32); ok { - elem.TotalDigits = int(v) + if _, ok := raw["TotalDigits"]; ok { + elem.TotalDigits = extractInt(raw["TotalDigits"]) } if v, ok := raw["OriginalValue"].(string); ok { elem.OriginalValue = v diff --git a/sdk/mpr/parser_misc_test.go b/sdk/mpr/parser_misc_test.go new file mode 100644 index 000000000..a12d01b0c --- /dev/null +++ b/sdk/mpr/parser_misc_test.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mpr + +import ( + "testing" +) + +// Issue #585: parseJsonElement asserted `raw[field].(int32)` for every numeric +// facet on a JSON-structure element. Mendix Studio Pro stores these fields as +// BSON int64, so the assertion failed silently and the parsed value defaulted +// to 0 — the same class of bug fixed for StringAttributeType.Length in #583. +// +// Each numeric facet must round-trip across every BSON numeric width that the +// mongo-driver may produce (int32, int64, int, float64) and preserve the +// default when the field is missing. +func TestParseJsonElement_NumericFields_BsonNumericWidths(t *testing.T) { + type fieldCase struct { + name string + bsonKey string + read func(*JsonElement) int + missing int // expected zero value when field is absent + } + fields := []fieldCase{ + {"MinOccurs", "MinOccurs", func(e *JsonElement) int { return e.MinOccurs }, 0}, + {"MaxOccurs", "MaxOccurs", func(e *JsonElement) int { return e.MaxOccurs }, 0}, + {"MaxLength", "MaxLength", func(e *JsonElement) int { return e.MaxLength }, -1}, + {"FractionDigits", "FractionDigits", func(e *JsonElement) int { return e.FractionDigits }, -1}, + {"TotalDigits", "TotalDigits", func(e *JsonElement) int { return e.TotalDigits }, -1}, + } + + widths := []struct { + name string + value any + }{ + {"int32 (mxcli writer)", int32(42)}, + {"int64 (Studio Pro writer)", int64(42)}, + {"int", int(42)}, + {"float64 (extended JSON)", float64(42)}, + } + + for _, f := range fields { + for _, w := range widths { + t.Run(f.name+"/"+w.name, func(t *testing.T) { + raw := map[string]any{f.bsonKey: w.value} + elem := parseJsonElement(raw) + if got := f.read(elem); got != 42 { + t.Errorf("%s = %d, want 42 (input %T(%v))", f.name, got, w.value, w.value) + } + }) + } + t.Run(f.name+"/missing", func(t *testing.T) { + elem := parseJsonElement(map[string]any{}) + if got := f.read(elem); got != f.missing { + t.Errorf("%s = %d, want default %d when field absent", f.name, got, f.missing) + } + }) + } +} diff --git a/sdk/mpr/parser_scheduledevent_test.go b/sdk/mpr/parser_scheduledevent_test.go new file mode 100644 index 000000000..6e9c6a529 --- /dev/null +++ b/sdk/mpr/parser_scheduledevent_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mpr + +import ( + "testing" + + "go.mongodb.org/mongo-driver/bson" +) + +// Issue #585: parseScheduledEvent asserted `raw["Interval"].(int32)`. Studio +// Pro writes Interval as BSON int64, so the assertion failed silently and +// every scheduled event read from a Studio Pro-written MPR appeared with +// Interval=0 — the same misreport pattern fixed in #583 for +// StringAttributeType.Length. +func TestParseScheduledEvent_Interval_BsonNumericWidths(t *testing.T) { + cases := []struct { + name string + interval any + want int + }{ + {"int32 (mxcli writer)", int32(15), 15}, + {"int64 (Studio Pro writer)", int64(15), 15}, + {"int", int(15), 15}, + {"float64 (extended JSON)", float64(15), 15}, + {"missing field", nil, 0}, + } + + r := &Reader{version: MPRVersionV1} + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + doc := bson.M{ + "$Type": "ScheduledEvents$ScheduledEvent", + "Name": "MyEvent", + "Enabled": true, + "IntervalType": "Hour", + } + if tc.interval != nil { + doc["Interval"] = tc.interval + } + data, err := bson.Marshal(doc) + if err != nil { + t.Fatalf("bson.Marshal: %v", err) + } + event, err := r.parseScheduledEvent("unit-id", "container-id", data) + if err != nil { + t.Fatalf("parseScheduledEvent: %v", err) + } + if event.Interval != tc.want { + t.Errorf("Interval = %d, want %d (input %T(%v))", event.Interval, tc.want, tc.interval, tc.interval) + } + }) + } +}