Skip to content

Commit 2e92d7c

Browse files
akoclaude
andcommitted
feat: MDL support for entity system attributes (#182)
Mendix persistent entities support four built-in system attributes for auditing: createdDate, changedDate, owner, changedBy. The Go SDK already read/wrote them, but MDL only exposed SET STORE OWNER. The other three flags could not be set or unset, and DESCRIBE ENTITY emitted none of them. Add full MDL support for all four flags: - CREATE ENTITY accepts STORE OWNER / STORE CHANGED BY / STORE CREATED DATE / STORE CHANGED DATE as entityOptions - ALTER ENTITY supports SET STORE ... and DROP STORE ... for all four - DESCRIBE ENTITY emits the STORE clauses for round-trip support - New tokens CHANGED, CREATED added to lexer + keyword rule - Doctype test 26-system-attribute-examples.mdl covers CREATE/ALTER/DROP Verified end-to-end against a real Mendix project: round-trip works, mx check returns 0 errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2e667cc commit 2e92d7c

File tree

14 files changed

+11074
-10371
lines changed

14 files changed

+11074
-10371
lines changed

cmd/mxcli/lsp_completions_gen.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
-- ============================================================================
2+
-- System Attribute Examples (Issue #182)
3+
-- ============================================================================
4+
--
5+
-- Mendix persistent entities support four optional system attributes that
6+
-- provide built-in auditing:
7+
--
8+
-- STORE OWNER -> System.owner association
9+
-- STORE CHANGED BY -> System.changedBy association
10+
-- STORE CREATED DATE -> CreatedDate attribute (DateTime)
11+
-- STORE CHANGED DATE -> ChangedDate attribute (DateTime)
12+
--
13+
-- This file demonstrates enabling these flags both during CREATE ENTITY
14+
-- (as entity options) and via ALTER ENTITY (SET / DROP).
15+
--
16+
-- Usage:
17+
-- 1. Connect to your project: CONNECT LOCAL 'app.mpr';
18+
-- 2. Execute: EXECUTE SCRIPT 'mdl-examples/doctype-tests/26-system-attribute-examples.mdl';
19+
-- 3. Roundtrip: DESCRIBE ENTITY SysAttrTest.<EntityName>
20+
--
21+
-- ============================================================================
22+
23+
CREATE MODULE SysAttrTest;
24+
25+
-- ============================================================================
26+
-- Example 1: All four system attributes set during CREATE
27+
-- ============================================================================
28+
29+
/**
30+
* Audited entity that uses all four built-in system attributes.
31+
*/
32+
CREATE OR MODIFY PERSISTENT ENTITY SysAttrTest.AuditedRecord (
33+
Title: String(200) NOT NULL,
34+
Amount: Decimal
35+
)
36+
STORE OWNER
37+
STORE CHANGED BY
38+
STORE CREATED DATE
39+
STORE CHANGED DATE;
40+
41+
-- ============================================================================
42+
-- Example 2: Subset of system attributes
43+
-- ============================================================================
44+
45+
CREATE OR MODIFY PERSISTENT ENTITY SysAttrTest.LogEntry (
46+
Message: String(500),
47+
Level: String(20)
48+
)
49+
STORE CREATED DATE;
50+
51+
-- ============================================================================
52+
-- Example 3: ALTER ENTITY to enable flags after creation
53+
-- ============================================================================
54+
55+
CREATE OR MODIFY PERSISTENT ENTITY SysAttrTest.Customer (
56+
Name: String(100) NOT NULL,
57+
Email: String(200)
58+
);
59+
60+
ALTER ENTITY SysAttrTest.Customer SET STORE OWNER;
61+
ALTER ENTITY SysAttrTest.Customer SET STORE CHANGED BY;
62+
ALTER ENTITY SysAttrTest.Customer SET STORE CREATED DATE;
63+
ALTER ENTITY SysAttrTest.Customer SET STORE CHANGED DATE;
64+
65+
-- ============================================================================
66+
-- Example 4: ALTER ENTITY to disable previously enabled flags
67+
-- ============================================================================
68+
69+
CREATE OR MODIFY PERSISTENT ENTITY SysAttrTest.TempRecord (
70+
Payload: String(100)
71+
)
72+
STORE OWNER
73+
STORE CREATED DATE;
74+
75+
-- Disable both flags
76+
ALTER ENTITY SysAttrTest.TempRecord DROP STORE OWNER;
77+
ALTER ENTITY SysAttrTest.TempRecord DROP STORE CREATED DATE;

mdl/ast/ast_entity.go

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,19 @@ func (k EntityKind) String() string {
3333

3434
// CreateEntityStmt represents: CREATE [OR MODIFY] PERSISTENT|NON-PERSISTENT ENTITY Module.Name [EXTENDS Parent] (attributes) ...
3535
type CreateEntityStmt struct {
36-
Name QualifiedName
37-
Kind EntityKind
38-
Generalization *QualifiedName // Parent entity for inheritance (e.g., System.Image)
39-
Attributes []Attribute
40-
Indexes []Index
41-
Position *Position
42-
Documentation string
43-
Comment string
44-
CreateOrModify bool // true for CREATE OR MODIFY
36+
Name QualifiedName
37+
Kind EntityKind
38+
Generalization *QualifiedName // Parent entity for inheritance (e.g., System.Image)
39+
Attributes []Attribute
40+
Indexes []Index
41+
Position *Position
42+
Documentation string
43+
Comment string
44+
CreateOrModify bool // true for CREATE OR MODIFY
45+
StoreOwner bool // STORE OWNER (system attribute)
46+
StoreChangedBy bool // STORE CHANGED BY (system attribute)
47+
StoreCreatedDate bool // STORE CREATED DATE (system attribute)
48+
StoreChangedDate bool // STORE CHANGED DATE (system attribute)
4549
}
4650

4751
func (s *CreateEntityStmt) isStatement() {}
@@ -57,16 +61,23 @@ func (s *DropEntityStmt) isStatement() {}
5761
type AlterEntityOp int
5862

5963
const (
60-
AlterEntityAddAttribute AlterEntityOp = iota // ADD ATTRIBUTE / ADD COLUMN
61-
AlterEntityRenameAttribute // RENAME ATTRIBUTE / RENAME COLUMN
62-
AlterEntityModifyAttribute // MODIFY ATTRIBUTE / MODIFY COLUMN
63-
AlterEntityDropAttribute // DROP ATTRIBUTE / DROP COLUMN
64-
AlterEntitySetDocumentation // SET DOCUMENTATION
65-
AlterEntitySetComment // SET COMMENT
66-
AlterEntityAddIndex // ADD INDEX
67-
AlterEntityDropIndex // DROP INDEX
68-
AlterEntitySetStoreOwner // SET STORE OWNER
69-
AlterEntitySetPosition // SET POSITION (x, y)
64+
AlterEntityAddAttribute AlterEntityOp = iota // ADD ATTRIBUTE / ADD COLUMN
65+
AlterEntityRenameAttribute // RENAME ATTRIBUTE / RENAME COLUMN
66+
AlterEntityModifyAttribute // MODIFY ATTRIBUTE / MODIFY COLUMN
67+
AlterEntityDropAttribute // DROP ATTRIBUTE / DROP COLUMN
68+
AlterEntitySetDocumentation // SET DOCUMENTATION
69+
AlterEntitySetComment // SET COMMENT
70+
AlterEntityAddIndex // ADD INDEX
71+
AlterEntityDropIndex // DROP INDEX
72+
AlterEntitySetStoreOwner // SET STORE OWNER
73+
AlterEntitySetStoreChangedBy // SET STORE CHANGED BY
74+
AlterEntitySetStoreCreatedDate // SET STORE CREATED DATE
75+
AlterEntitySetStoreChangedDate // SET STORE CHANGED DATE
76+
AlterEntityDropStoreOwner // DROP STORE OWNER
77+
AlterEntityDropStoreChangedBy // DROP STORE CHANGED BY
78+
AlterEntityDropStoreCreatedDate // DROP STORE CREATED DATE
79+
AlterEntityDropStoreChangedDate // DROP STORE CHANGED DATE
80+
AlterEntitySetPosition // SET POSITION (x, y)
7081
)
7182

7283
// AlterEntityStmt represents: ALTER ENTITY Module.Name ADD/DROP/RENAME/MODIFY ATTRIBUTE ...

mdl/executor/cmd_entities.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ func (e *Executor) execCreateEntity(s *ast.CreateEntityStmt) error {
198198
Attributes: attrs,
199199
ValidationRules: validationRules,
200200
Indexes: indexes,
201+
HasOwner: s.StoreOwner,
202+
HasChangedBy: s.StoreChangedBy,
203+
HasCreatedDate: s.StoreCreatedDate,
204+
HasChangedDate: s.StoreChangedDate,
201205
}
202206

203207
// Set generalization (inheritance) if specified
@@ -694,6 +698,62 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error {
694698
e.invalidateDomainModelsCache()
695699
fmt.Fprintf(e.output, "Enabled store owner on entity %s\n", s.Name)
696700

701+
case ast.AlterEntitySetStoreChangedBy:
702+
entity.HasChangedBy = true
703+
if err := e.writer.UpdateEntity(dm.ID, entity); err != nil {
704+
return fmt.Errorf("failed to set store changed by: %w", err)
705+
}
706+
e.invalidateDomainModelsCache()
707+
fmt.Fprintf(e.output, "Enabled store changed by on entity %s\n", s.Name)
708+
709+
case ast.AlterEntitySetStoreCreatedDate:
710+
entity.HasCreatedDate = true
711+
if err := e.writer.UpdateEntity(dm.ID, entity); err != nil {
712+
return fmt.Errorf("failed to set store created date: %w", err)
713+
}
714+
e.invalidateDomainModelsCache()
715+
fmt.Fprintf(e.output, "Enabled store created date on entity %s\n", s.Name)
716+
717+
case ast.AlterEntitySetStoreChangedDate:
718+
entity.HasChangedDate = true
719+
if err := e.writer.UpdateEntity(dm.ID, entity); err != nil {
720+
return fmt.Errorf("failed to set store changed date: %w", err)
721+
}
722+
e.invalidateDomainModelsCache()
723+
fmt.Fprintf(e.output, "Enabled store changed date on entity %s\n", s.Name)
724+
725+
case ast.AlterEntityDropStoreOwner:
726+
entity.HasOwner = false
727+
if err := e.writer.UpdateEntity(dm.ID, entity); err != nil {
728+
return fmt.Errorf("failed to drop store owner: %w", err)
729+
}
730+
e.invalidateDomainModelsCache()
731+
fmt.Fprintf(e.output, "Disabled store owner on entity %s\n", s.Name)
732+
733+
case ast.AlterEntityDropStoreChangedBy:
734+
entity.HasChangedBy = false
735+
if err := e.writer.UpdateEntity(dm.ID, entity); err != nil {
736+
return fmt.Errorf("failed to drop store changed by: %w", err)
737+
}
738+
e.invalidateDomainModelsCache()
739+
fmt.Fprintf(e.output, "Disabled store changed by on entity %s\n", s.Name)
740+
741+
case ast.AlterEntityDropStoreCreatedDate:
742+
entity.HasCreatedDate = false
743+
if err := e.writer.UpdateEntity(dm.ID, entity); err != nil {
744+
return fmt.Errorf("failed to drop store created date: %w", err)
745+
}
746+
e.invalidateDomainModelsCache()
747+
fmt.Fprintf(e.output, "Disabled store created date on entity %s\n", s.Name)
748+
749+
case ast.AlterEntityDropStoreChangedDate:
750+
entity.HasChangedDate = false
751+
if err := e.writer.UpdateEntity(dm.ID, entity); err != nil {
752+
return fmt.Errorf("failed to drop store changed date: %w", err)
753+
}
754+
e.invalidateDomainModelsCache()
755+
fmt.Fprintf(e.output, "Disabled store changed date on entity %s\n", s.Name)
756+
697757
case ast.AlterEntitySetPosition:
698758
if s.Position == nil {
699759
return fmt.Errorf("no position provided")

mdl/executor/cmd_entities_describe.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,20 @@ func (e *Executor) describeEntity(name ast.QualifiedName) error {
364364
}
365365
}
366366

367+
// Output system attribute flags as entity options (round-trippable)
368+
if entity.HasOwner {
369+
fmt.Fprint(e.output, "\nSTORE OWNER")
370+
}
371+
if entity.HasChangedBy {
372+
fmt.Fprint(e.output, "\nSTORE CHANGED BY")
373+
}
374+
if entity.HasCreatedDate {
375+
fmt.Fprint(e.output, "\nSTORE CREATED DATE")
376+
}
377+
if entity.HasChangedDate {
378+
fmt.Fprint(e.output, "\nSTORE CHANGED DATE")
379+
}
380+
367381
fmt.Fprintln(e.output, ";")
368382

369383
// Output access rule GRANT statements

mdl/grammar/MDLLexer.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,8 @@ JUMP: J U M P;
627627
DUE: D U E;
628628
OVERVIEW: O V E R V I E W;
629629
DATE: D A T E;
630+
CHANGED: C H A N G E D;
631+
CREATED: C R E A T E D;
630632
PARALLEL: P A R A L L E L;
631633
WAIT: W A I T;
632634
ANNOTATION: A N N O T A T I O N;

mdl/grammar/MDLParser.g4

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,10 @@ entityOptions
547547
entityOption
548548
: COMMENT STRING_LITERAL
549549
| INDEX indexDefinition
550+
| STORE OWNER
551+
| STORE CHANGED BY
552+
| STORE CREATED DATE_TYPE
553+
| STORE CHANGED DATE_TYPE
550554
;
551555

552556
attributeDefinitionList
@@ -751,6 +755,13 @@ alterEntityAction
751755
| SET DOCUMENTATION STRING_LITERAL
752756
| SET COMMENT STRING_LITERAL
753757
| SET STORE OWNER
758+
| SET STORE CHANGED BY
759+
| SET STORE CREATED DATE_TYPE
760+
| SET STORE CHANGED DATE_TYPE
761+
| DROP STORE OWNER
762+
| DROP STORE CHANGED BY
763+
| DROP STORE CREATED DATE_TYPE
764+
| DROP STORE CHANGED DATE_TYPE
754765
| SET POSITION LPAREN NUMBER_LITERAL COMMA NUMBER_LITERAL RPAREN
755766
| ADD INDEX indexDefinition
756767
| DROP INDEX IDENTIFIER
@@ -3611,6 +3622,7 @@ keyword
36113622
| JAVA | EVENTS | OVER | MEMBERS // Miscellaneous keywords
36123623
| WORKFLOW | WORKFLOWS | REFERENCES | CALLERS | CALLEES // Code search keywords
36133624
| TASK | DECISION | SPLIT | OUTCOMES | TARGETING | NOTIFICATION | TIMER | JUMP | DUE | OVERVIEW | DATE | PARALLEL | WAIT | BY // Workflow keywords
3625+
| CHANGED | CREATED // Entity system attribute keywords
36143626
| TRANSITIVE | IMPACT | SEARCH // Additional search keywords
36153627
| BUSINESS | EVENT | SUBSCRIBE | SETTINGS | CONFIGURATION // Business events / settings keywords
36163628
| DEFINE | FRAGMENT | FRAGMENTS // Fragment keywords

mdl/grammar/parser/MDLLexer.interp

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

0 commit comments

Comments
 (0)