Skip to content

Commit 4be34bb

Browse files
akoclaude
andcommitted
feat: add ALTER PAGE ADD/DROP VARIABLE support (Phase 3)
Implement ADD/DROP variable operations for ALTER PAGE/SNIPPET, completing Phase 3 of the page variables proposal. This enables adding and removing page variables on existing pages without requiring a full CREATE OR REPLACE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b0028d1 commit 4be34bb

12 files changed

Lines changed: 8105 additions & 7536 deletions

File tree

.claude/skills/mendix/alter-page.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ REPLACE footer1 WITH {
123123

124124
Replaces the target widget with one or more new widgets. The new widgets use the same syntax as `CREATE PAGE`.
125125

126+
### ADD Variables - Add a Page Variable
127+
128+
```sql
129+
ADD Variables $showStockColumn: Boolean = 'true'
130+
```
131+
132+
Adds a new page variable (`Forms$LocalVariable`) to the page/snippet. DataType can be `Boolean`, `String`, `Integer`, `Decimal`, `DateTime`, or an entity type. Default value is a Mendix expression in single quotes.
133+
134+
### DROP Variables - Remove a Page Variable
135+
136+
```sql
137+
DROP Variables $showStockColumn
138+
```
139+
140+
Removes a page variable by name.
141+
126142
## Examples
127143

128144
### Change button text and style
@@ -143,6 +159,14 @@ ALTER PAGE MyModule.Customer_Edit {
143159
};
144160
```
145161

162+
### Add a page variable for column visibility
163+
164+
```sql
165+
ALTER PAGE MyModule.ProductOverview {
166+
ADD Variables $showStockColumn: Boolean = 'if (3 < 4) then true else false'
167+
};
168+
```
169+
146170
### Remove unused fields and update title
147171

148172
```sql

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ Modify an existing page or snippet's widget tree in-place without full `CREATE O
415415
| Drop widgets | `DROP WIDGET name1, name2` | Remove widgets by name |
416416
| Replace widget | `REPLACE widgetName WITH { widgets }` | Replace widget subtree |
417417
| Pluggable prop | `SET 'showLabel' = false ON cbStatus` | Quoted name for pluggable widgets |
418+
| Add variable | `ADD Variables $name: Type = 'expr'` | Add a page variable |
419+
| Drop variable | `DROP Variables $name` | Remove a page variable |
418420

419421
**Supported SET properties:** Caption, Label, ButtonStyle, Class, Style, Editable, Visible, Name, Title (page-level), and quoted pluggable widget properties.
420422

mdl-examples/doctype-tests/03-page-examples.mdl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2178,6 +2178,37 @@ CREATE PAGE PgTest.P036_ComboBox_Association
21782178

21792179
-- ============================================================================================ --
21802180

2181+
-- MARK: ALTER PAGE Variable Examples
2182+
2183+
-- =============================================================================
2184+
-- ALTER PAGE - ADD / DROP Page Variables
2185+
-- =============================================================================
2186+
--
2187+
-- Page variables (Forms$LocalVariable) can be added to or removed from existing
2188+
-- pages and snippets using ALTER PAGE / ALTER SNIPPET with ADD/DROP Variables.
2189+
--
2190+
2191+
-- Add a boolean page variable for controlling column visibility
2192+
ALTER PAGE PgTest.P033b_DataGrid_ColumnProperties {
2193+
ADD Variables $showPriceColumn: Boolean = 'true';
2194+
};
2195+
2196+
-- Add a string variable with an escaped string default
2197+
ALTER PAGE PgTest.P033b_DataGrid_ColumnProperties {
2198+
ADD Variables $filterMode: String = '''active''';
2199+
};
2200+
2201+
-- Drop a page variable by name
2202+
ALTER PAGE PgTest.P033b_DataGrid_ColumnProperties {
2203+
DROP Variables $filterMode;
2204+
};
2205+
2206+
-- Multiple variable operations in a single ALTER statement
2207+
ALTER PAGE PgTest.P033b_DataGrid_ColumnProperties {
2208+
ADD Variables $pageSize: Integer = '20';
2209+
DROP Variables $showPriceColumn;
2210+
};
2211+
21812212
-- MARK: Move Examples
21822213

21832214
-- =============================================================================

mdl/ast/ast_alter_page.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,17 @@ type ReplaceWidgetOp struct {
5252
}
5353

5454
func (s *ReplaceWidgetOp) isAlterPageOperation() {}
55+
56+
// AddVariableOp represents: ADD Variables $name: Type = 'default'
57+
type AddVariableOp struct {
58+
Variable PageVariable
59+
}
60+
61+
func (s *AddVariableOp) isAlterPageOperation() {}
62+
63+
// DropVariableOp represents: DROP Variables $name
64+
type DropVariableOp struct {
65+
VariableName string // without $ prefix
66+
}
67+
68+
func (s *DropVariableOp) isAlterPageOperation() {}

mdl/executor/cmd_alter_page.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ func (e *Executor) execAlterPage(s *ast.AlterPageStmt) error {
8989
if err := e.applyReplaceWidgetWith(rawData, o, modName, containerID, findWidget); err != nil {
9090
return fmt.Errorf("REPLACE failed: %w", err)
9191
}
92+
case *ast.AddVariableOp:
93+
if err := applyAddVariable(&rawData, o); err != nil {
94+
return fmt.Errorf("ADD VARIABLE failed: %w", err)
95+
}
96+
case *ast.DropVariableOp:
97+
if err := applyDropVariable(rawData, o); err != nil {
98+
return fmt.Errorf("DROP VARIABLE failed: %w", err)
99+
}
92100
default:
93101
return fmt.Errorf("unknown ALTER %s operation type: %T", containerType, op)
94102
}
@@ -936,6 +944,85 @@ func extractEntityFromDataSource(wDoc bson.D) string {
936944
return ""
937945
}
938946

947+
// ============================================================================
948+
// ADD / DROP variable
949+
// ============================================================================
950+
951+
// applyAddVariable adds a new LocalVariable to the raw BSON page/snippet.
952+
func applyAddVariable(rawData *bson.D, op *ast.AddVariableOp) error {
953+
// Check for duplicate variable name
954+
existingVars := dGetArrayElements(dGet(*rawData, "Variables"))
955+
for _, ev := range existingVars {
956+
if evDoc, ok := ev.(bson.D); ok {
957+
if dGetString(evDoc, "Name") == op.Variable.Name {
958+
return fmt.Errorf("variable $%s already exists", op.Variable.Name)
959+
}
960+
}
961+
}
962+
963+
// Build VariableType BSON
964+
varTypeID := mpr.GenerateID()
965+
bsonTypeName := mdlTypeToBsonType(op.Variable.DataType)
966+
varType := bson.D{
967+
{Key: "$ID", Value: mpr.IDToBsonBinary(varTypeID)},
968+
{Key: "$Type", Value: bsonTypeName},
969+
}
970+
if bsonTypeName == "DataTypes$ObjectType" {
971+
varType = append(varType, bson.E{Key: "Entity", Value: op.Variable.DataType})
972+
}
973+
974+
// Build LocalVariable BSON document
975+
varID := mpr.GenerateID()
976+
varDoc := bson.D{
977+
{Key: "$ID", Value: mpr.IDToBsonBinary(varID)},
978+
{Key: "$Type", Value: "Forms$LocalVariable"},
979+
{Key: "DefaultValue", Value: op.Variable.DefaultValue},
980+
{Key: "Name", Value: op.Variable.Name},
981+
{Key: "VariableType", Value: varType},
982+
}
983+
984+
// Append to existing Variables array, or create new field
985+
existing := toBsonA(dGet(*rawData, "Variables"))
986+
if existing != nil {
987+
elements := dGetArrayElements(dGet(*rawData, "Variables"))
988+
elements = append(elements, varDoc)
989+
dSetArray(*rawData, "Variables", elements)
990+
} else {
991+
// Field doesn't exist — append to the document
992+
*rawData = append(*rawData, bson.E{Key: "Variables", Value: bson.A{int32(3), varDoc}})
993+
}
994+
995+
return nil
996+
}
997+
998+
// applyDropVariable removes a LocalVariable from the raw BSON page/snippet.
999+
func applyDropVariable(rawData bson.D, op *ast.DropVariableOp) error {
1000+
elements := dGetArrayElements(dGet(rawData, "Variables"))
1001+
if elements == nil {
1002+
return fmt.Errorf("variable $%s not found", op.VariableName)
1003+
}
1004+
1005+
// Find and remove the variable
1006+
found := false
1007+
var kept []any
1008+
for _, elem := range elements {
1009+
if doc, ok := elem.(bson.D); ok {
1010+
if dGetString(doc, "Name") == op.VariableName {
1011+
found = true
1012+
continue
1013+
}
1014+
}
1015+
kept = append(kept, elem)
1016+
}
1017+
1018+
if !found {
1019+
return fmt.Errorf("variable $%s not found", op.VariableName)
1020+
}
1021+
1022+
dSetArray(rawData, "Variables", kept)
1023+
return nil
1024+
}
1025+
9391026
// ============================================================================
9401027
// Widget BSON building
9411028
// ============================================================================

mdl/grammar/MDLParser.g4

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ alterPageOperation
184184
| alterPageInsert SEMICOLON?
185185
| alterPageDrop SEMICOLON?
186186
| alterPageReplace SEMICOLON?
187+
| alterPageAddVariable SEMICOLON?
188+
| alterPageDropVariable SEMICOLON?
187189
;
188190

189191
alterPageSet
@@ -210,6 +212,14 @@ alterPageReplace
210212
: REPLACE identifierOrKeyword WITH LBRACE pageBodyV3 RBRACE
211213
;
212214

215+
alterPageAddVariable
216+
: ADD VARIABLES_KW variableDeclaration // ADD Variables $show: Boolean = 'true'
217+
;
218+
219+
alterPageDropVariable
220+
: DROP VARIABLES_KW VARIABLE // DROP Variables $show
221+
;
222+
213223
navigationClause
214224
: HOME (PAGE | MICROFLOW) qualifiedName (FOR qualifiedName)?
215225
| LOGIN PAGE qualifiedName

mdl/grammar/parser/MDLParser.interp

Lines changed: 3 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)