|
| 1 | +# Proposal: RENAME with Reference Refactoring |
| 2 | + |
| 3 | +**Status:** Draft |
| 4 | +**Date:** 2026-04-08 |
| 5 | + |
| 6 | +## Motivation |
| 7 | + |
| 8 | +Renaming entities, microflows, pages, and modules is one of the most common refactoring operations. Currently mxcli has `RENAME ENTITY` and `RENAME MODULE` in the grammar but neither is implemented. Renaming in Mendix is dangerous because cross-references are stored as qualified name strings (BY_NAME_REFERENCE) — renaming without updating references breaks the project. |
| 9 | + |
| 10 | +A safe RENAME command that automatically updates all references would be a significant productivity gain, especially for AI-assisted refactoring workflows (e.g., monolith-to-multi-app decomposition). |
| 11 | + |
| 12 | +## Current State |
| 13 | + |
| 14 | +### What exists |
| 15 | + |
| 16 | +| Operation | Status | |
| 17 | +|-----------|--------| |
| 18 | +| `ALTER ENTITY ... RENAME ATTRIBUTE Old TO New` | Works | |
| 19 | +| `ALTER ENUMERATION ... RENAME VALUE Old TO New` | Works | |
| 20 | +| `RENAME ENTITY Module.Old TO New` | Grammar only, not implemented | |
| 21 | +| `RENAME MODULE Old TO New` | Grammar only, not implemented | |
| 22 | +| `RENAME MICROFLOW/PAGE/NANOFLOW ...` | Not in grammar | |
| 23 | + |
| 24 | +### Existing reference-update code |
| 25 | + |
| 26 | +The codebase already has precedent for scanning and rewriting BY_NAME references: |
| 27 | + |
| 28 | +- `UpdateEnumerationRefsInAllDomainModels()` in `writer_domainmodel.go` — scans all entities to update enumeration qualified names when an enum moves between modules |
| 29 | +- `MoveEntity()` in `writer_domainmodel.go` — updates validation rule attribute references when an entity is moved |
| 30 | + |
| 31 | +### How references are stored |
| 32 | + |
| 33 | +Mendix MPR files use two reference types: |
| 34 | + |
| 35 | +| Type | Storage | Example | Rename Impact | |
| 36 | +|------|---------|---------|---------------| |
| 37 | +| BY_ID_REFERENCE | Binary UUID | Association parent/child pointers, index attribute pointers | **Safe** — IDs survive rename | |
| 38 | +| BY_NAME_REFERENCE | Qualified name string | `"Module.EntityName"`, `"Module.MicroflowName"` | **Breaks** — string must be updated | |
| 39 | + |
| 40 | +All cross-document references use BY_NAME_REFERENCE. This is the fundamental challenge. |
| 41 | + |
| 42 | +## Design |
| 43 | + |
| 44 | +### Syntax |
| 45 | + |
| 46 | +```sql |
| 47 | +-- Rename entity (updates all references) |
| 48 | +RENAME ENTITY Module.OldName TO NewName; |
| 49 | + |
| 50 | +-- Rename microflow |
| 51 | +RENAME MICROFLOW Module.OldName TO NewName; |
| 52 | + |
| 53 | +-- Rename nanoflow |
| 54 | +RENAME NANOFLOW Module.OldName TO NewName; |
| 55 | + |
| 56 | +-- Rename page |
| 57 | +RENAME PAGE Module.OldName TO NewName; |
| 58 | + |
| 59 | +-- Rename module (updates ALL qualified names) |
| 60 | +RENAME MODULE OldName TO NewName; |
| 61 | + |
| 62 | +-- Dry run: show what would change without modifying anything |
| 63 | +RENAME ENTITY Module.OldName TO NewName DRY RUN; |
| 64 | +``` |
| 65 | + |
| 66 | +The `TO` target is always just a name (not qualified) — the document stays in the same module. Cross-module moves are handled by the existing `MOVE` command. |
| 67 | + |
| 68 | +### Reference Update Matrix |
| 69 | + |
| 70 | +When renaming `Module.OldEntity` to `Module.NewEntity`, these BY_NAME references must be updated: |
| 71 | + |
| 72 | +| Location | Reference Field | Example | |
| 73 | +|----------|----------------|---------| |
| 74 | +| **Microflow actions** | Entity in CREATE/RETRIEVE/DELETE/CHANGE/AGGREGATE/LIST | `"Module.OldEntity"` → `"Module.NewEntity"` | |
| 75 | +| **Microflow parameters** | Entity type reference | `$Param: Module.OldEntity` | |
| 76 | +| **Microflow return types** | Entity type reference | `RETURNS Module.OldEntity` | |
| 77 | +| **Associations** | Cross-module child/parent refs | `"Module.OldEntity"` in `Child` field | |
| 78 | +| **Generalization** | Parent entity reference | `EXTENDS Module.OldEntity` | |
| 79 | +| **Enumeration attributes** | EnumerationRef on attribute types | `"Module.OldEnum"` | |
| 80 | +| **View entity OQL** | Entity names in SELECT/FROM/JOIN | `FROM Module.OldEntity AS e` | |
| 81 | +| **Page datasources** | Entity in DATABASE/XPATH sources | `DataSource: DATABASE Module.OldEntity` | |
| 82 | +| **Page parameter types** | Entity type in page params | `Params: { $p: Module.OldEntity }` | |
| 83 | +| **Navigation** | Home page, login page refs | `"Module.OldPage"` | |
| 84 | +| **Settings** | After-startup, before-shutdown microflow | `"Module.OldMicroflow"` | |
| 85 | +| **Security** | Allowed module roles on microflows | (BY_NAME refs) | |
| 86 | +| **Member access rules** | Attribute/association names | Column names in access rules | |
| 87 | +| **Import/Export mappings** | Entity references in element mappings | `"Module.OldEntity"` | |
| 88 | +| **Business events** | Entity references in messages | `"Module.OldEntity"` | |
| 89 | +| **Java action parameters** | Entity type references | `"Module.OldEntity"` | |
| 90 | +| **Scheduled events** | Microflow references | `"Module.OldMicroflow"` | |
| 91 | + |
| 92 | +### Architecture: Unified Reference Scanner |
| 93 | + |
| 94 | +Instead of writing one-off update functions per document type, implement a **generic reference scanner** that: |
| 95 | + |
| 96 | +1. Reads every document in the project (all units from the MPR) |
| 97 | +2. Walks the BSON tree looking for string values matching the old qualified name |
| 98 | +3. Replaces matching strings with the new qualified name |
| 99 | +4. Rewrites the modified documents |
| 100 | + |
| 101 | +This brute-force approach is safe because: |
| 102 | +- BY_NAME references are always stored as plain strings in BSON |
| 103 | +- The qualified name format (`Module.Name`) is unambiguous — no false positives from substring matches |
| 104 | +- It catches references we haven't explicitly listed (future-proof) |
| 105 | + |
| 106 | +```go |
| 107 | +// RenameReferences scans all documents and replaces qualified name strings. |
| 108 | +func (w *Writer) RenameReferences(oldQualifiedName, newQualifiedName string) (int, error) { |
| 109 | + units, err := w.reader.ListRawUnits() |
| 110 | + if err != nil { |
| 111 | + return 0, err |
| 112 | + } |
| 113 | + |
| 114 | + count := 0 |
| 115 | + for _, unit := range units { |
| 116 | + raw, err := bson.Unmarshal(unit.Contents) |
| 117 | + if err != nil { |
| 118 | + continue |
| 119 | + } |
| 120 | + if updated, changed := replaceStringValues(raw, oldQualifiedName, newQualifiedName); changed { |
| 121 | + contents, _ := bson.Marshal(updated) |
| 122 | + w.updateUnitContents(unit.ID, contents) |
| 123 | + count++ |
| 124 | + } |
| 125 | + } |
| 126 | + return count, nil |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +The `replaceStringValues` function recursively walks the BSON document tree and replaces exact string matches. It also handles partial matches for attribute-qualified names (e.g., `Module.Entity.Attribute` when renaming `Module.Entity`). |
| 131 | + |
| 132 | +### Matching Rules |
| 133 | + |
| 134 | +For a rename of `Module.OldName` to `Module.NewName`: |
| 135 | + |
| 136 | +| Pattern | Match? | Example | |
| 137 | +|---------|--------|---------| |
| 138 | +| Exact: `"Module.OldName"` | Yes | Entity ref in microflow action | |
| 139 | +| Prefix: `"Module.OldName.Attribute"` | Yes → `"Module.NewName.Attribute"` | Validation rule attribute ref | |
| 140 | +| Substring: `"SomeModule.OldName"` | No | Different module, different entity | |
| 141 | +| Substring: `"Module.OldNameExtra"` | No | Longer name, not a match | |
| 142 | + |
| 143 | +The match is: string equals `oldName` OR string starts with `oldName + "."`. |
| 144 | + |
| 145 | +### Dry Run Mode |
| 146 | + |
| 147 | +`DRY RUN` scans without modifying, outputting what would change: |
| 148 | + |
| 149 | +``` |
| 150 | +RENAME ENTITY MyModule.Customer TO Client DRY RUN; |
| 151 | +
|
| 152 | +Would rename: MyModule.Customer → MyModule.Client |
| 153 | +References found: 23 |
| 154 | + MyModule.ACT_Customer_Save (Microflow) — 3 references |
| 155 | + MyModule.ACT_Customer_Delete (Microflow) — 2 references |
| 156 | + MyModule.Customer_Overview (Page) — 4 references |
| 157 | + MyModule.Customer_Edit (Page) — 5 references |
| 158 | + MyModule.Order (Entity) — 1 reference (association) |
| 159 | + MyModule.IMM_CustomerResponse (Import Mapping) — 2 references |
| 160 | + ... |
| 161 | +``` |
| 162 | + |
| 163 | +This lets users preview the blast radius before committing. |
| 164 | + |
| 165 | +### Module Rename |
| 166 | + |
| 167 | +Module rename is the most impactful — every qualified name starting with `OldModule.` must change. The same scanner works, but matches on the prefix `OldModule.` and replaces with `NewModule.`. |
| 168 | + |
| 169 | +Additionally, module rename must update: |
| 170 | +- The module document's `Name` field |
| 171 | +- The `themesource/oldmodule/` directory (rename to `themesource/newmodule/`) |
| 172 | +- The `javasource/oldmodule/` directory |
| 173 | +- Folder container names in the hierarchy |
| 174 | + |
| 175 | +## Implementation Plan |
| 176 | + |
| 177 | +### Phase 1: Reference scanner + dry run |
| 178 | + |
| 179 | +1. Implement `RenameReferences(old, new string) ([]RenameHit, error)` in `sdk/mpr/writer_rename.go` |
| 180 | +2. Implement `replaceStringValues()` BSON tree walker |
| 181 | +3. Add `DRY RUN` support to show affected documents without modifying |
| 182 | +4. Wire `RENAME ENTITY` and `RENAME MODULE` grammar rules to AST + visitor + executor |
| 183 | +5. Executor calls dry-run scanner and reports results |
| 184 | + |
| 185 | +**Deliverable**: `RENAME ENTITY Module.Old TO New DRY RUN;` works and lists all references. |
| 186 | + |
| 187 | +### Phase 2: Entity and enumeration rename |
| 188 | + |
| 189 | +6. Implement actual rename: update entity Name + run reference scanner |
| 190 | +7. Handle attribute-qualified names (`Module.Entity.Attribute` patterns) |
| 191 | +8. Handle OQL queries in ViewEntitySourceDocuments (string replacement in OQL text) |
| 192 | +9. Test with roundtrip: rename → `mx check` → 0 errors |
| 193 | +10. Add `RENAME ENUMERATION` support |
| 194 | + |
| 195 | +### Phase 3: Microflow, nanoflow, page rename |
| 196 | + |
| 197 | +11. Implement `RENAME MICROFLOW/NANOFLOW/PAGE` in grammar + executor |
| 198 | +12. Update navigation references (home pages, login pages) |
| 199 | +13. Update settings references (after-startup, before-shutdown) |
| 200 | +14. Update scheduled event microflow references |
| 201 | + |
| 202 | +### Phase 4: Module rename |
| 203 | + |
| 204 | +15. Implement `RENAME MODULE` — prefix replacement on all qualified names |
| 205 | +16. Handle filesystem directories (themesource, javasource) |
| 206 | +17. Handle the module security document name |
| 207 | +18. Handle folder container hierarchy updates |
| 208 | + |
| 209 | +### Phase 5: Association and constant rename |
| 210 | + |
| 211 | +19. `RENAME ASSOCIATION Module.Old TO New` |
| 212 | +20. `RENAME CONSTANT Module.Old TO New` |
| 213 | + |
| 214 | +## Risks |
| 215 | + |
| 216 | +### False positives in string replacement |
| 217 | + |
| 218 | +The brute-force scanner replaces any BSON string matching the qualified name. In theory, a string literal in a microflow expression could contain `"Module.EntityName"` as user-visible text. This is extremely unlikely (qualified names are an internal format) but possible. |
| 219 | + |
| 220 | +**Mitigation**: The dry-run mode lets users review before applying. We could also skip known text-only fields (Documentation, Caption) if false positives become an issue. |
| 221 | + |
| 222 | +### OQL query rewriting |
| 223 | + |
| 224 | +OQL queries reference entities as `Module.Entity` in FROM/JOIN clauses. Simple string replacement works for entity renames, but module renames need care — the OQL alias (`AS e`) stays the same, only the qualified name changes. |
| 225 | + |
| 226 | +### Java source files |
| 227 | + |
| 228 | +Renaming an entity changes the proxy class name in `javasource/<module>/proxies/`. Java source files that reference the old class name will break. This is out of scope for Phase 1-3 but should be documented as a limitation. |
| 229 | + |
| 230 | +## Scope Exclusions |
| 231 | + |
| 232 | +- **Java source file updates**: Out of scope — rename produces correct MPR but Java files need manual update |
| 233 | +- **Widget property string references**: Pluggable widget properties may contain entity/attribute names as strings — these are not updatable without widget-specific knowledge |
| 234 | +- **Git history**: Rename doesn't create a git rename operation — it modifies files in place |
| 235 | +- **Cross-project references**: Only single-project rename (multi-project is deferred to multi-app support) |
| 236 | + |
| 237 | +## Effort Estimate |
| 238 | + |
| 239 | +- Phase 1 (scanner + dry run): Small — ~150 lines Go |
| 240 | +- Phase 2 (entity rename): Medium — ~100 lines Go + tests |
| 241 | +- Phase 3 (microflow/page rename): Small — reuses Phase 2 scanner |
| 242 | +- Phase 4 (module rename): Medium — filesystem + prefix replacement |
| 243 | +- Phase 5 (association/constant): Small — reuses scanner |
0 commit comments