Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions mdl/catalog/builder_audit_members_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0

package catalog

import (
"testing"

"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/domainmodel"
)

// Mendix stores Owner / ChangedBy / CreatedDate / ChangedDate as BOOLEANS on
// the entity's generalization node, not as attributes, so they never appear in
// CATALOG.ATTRIBUTES. Before these columns existed, the obvious query for
// "which entities lack an audit trail" --
//
// SELECT e.QualifiedName FROM CATALOG.ENTITIES e
// LEFT JOIN CATALOG.ATTRIBUTES a
// ON a.EntityQualifiedName = e.QualifiedName AND a.Name = 'CreatedDate'
// WHERE a.Name IS NULL
//
// returned every entity forever, including ones that already had the field.
// DESCRIBE ENTITY renders them in the attribute list, which is what makes the
// omission surprising rather than obviously by-design.
func TestEntitiesCarryTheAuditMembers(t *testing.T) {
cat, err := New()
if err != nil {
t.Fatal(err)
}
defer cat.Close()

const modID = model.ID("mod-sales")
b := &Builder{
catalog: cat,
snapshot: &Snapshot{ID: "snap"},
hierarchy: &hierarchy{moduleIDs: map[model.ID]bool{modID: true}, moduleNames: map[model.ID]string{modID: "Sales"}},
domainModelCache: []*domainmodel.DomainModel{{
ContainerID: modID,
Entities: []*domainmodel.Entity{
{
BaseElement: model.BaseElement{ID: "e-audited"},
Name: "Order", Persistable: true,
HasOwner: true, HasChangedBy: true,
HasCreatedDate: true, HasChangedDate: true,
},
{
BaseElement: model.BaseElement{ID: "e-bare"},
Name: "Lookup", Persistable: true,
},
},
}},
}

tx, err := cat.CatalogDB().Begin()
if err != nil {
t.Fatal(err)
}
b.tx = tx
if err := b.buildEntities(); err != nil {
t.Fatalf("buildEntities: %v", err)
}
if err := tx.Commit(); err != nil {
t.Fatal(err)
}

res, err := cat.Query(`SELECT QualifiedName, HasCreatedDate, HasChangedDate,
HasOwner, HasChangedBy FROM entities_data ORDER BY QualifiedName`)
if err != nil {
t.Fatalf("query: %v -- the columns must exist, or the catalog cannot answer this at all", err)
}
if res.Count != 2 {
t.Fatalf("got %d rows, want 2", res.Count)
}

got := map[string][4]int64{}
for _, row := range res.Rows {
qn, _ := row[0].(string)
var flags [4]int64
for i := 0; i < 4; i++ {
flags[i], _ = row[i+1].(int64)
}
got[qn] = flags
}

if want := [4]int64{1, 1, 1, 1}; got["Sales.Order"] != want {
t.Errorf("Sales.Order audit flags = %v, want %v -- the entity's booleans "+
"are not reaching the row", got["Sales.Order"], want)
}
if want := [4]int64{0, 0, 0, 0}; got["Sales.Lookup"] != want {
t.Errorf("Sales.Lookup audit flags = %v, want %v -- an entity without "+
"audit members must not claim them", got["Sales.Lookup"], want)
}
}
7 changes: 6 additions & 1 deletion mdl/catalog/builder_modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ func (b *Builder) buildEntities() error {
INSERT INTO entities_data (Id, Name, QualifiedName, ModuleName, Folder, EntityType,
Description, Generalization, AttributeCount, AssociationCount,
AccessRuleCount, ValidationRuleCount, HasEventHandlers,
HasCreatedDate, HasChangedDate, HasOwner, HasChangedBy,
IsExternal, ExternalService,
ProjectId, SnapshotId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
return err
Expand Down Expand Up @@ -140,6 +141,10 @@ func (b *Builder) buildEntities() error {
len(entity.AccessRules),
len(entity.ValidationRules),
hasEventHandlers,
boolToInt(entity.HasCreatedDate),
boolToInt(entity.HasChangedDate),
boolToInt(entity.HasOwner),
boolToInt(entity.HasChangedBy),
isExternal,
externalService,
projectID, snapshotID,
Expand Down
19 changes: 18 additions & 1 deletion mdl/catalog/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ package catalog
//
// History:
//
// 12 — HasCreatedDate / HasChangedDate / HasOwner / HasChangedBy on entities.
// A CREATE TABLE IF NOT EXISTS does not add a column to a cached
// catalog, so without the bump every query naming one fails with "no
// such column" on exactly the projects that have a catalog already --
// a hard error rather than a wrong answer, but on the wrong machines.
// 11 — the `widget` edge in refs (page/snippet -> widget definition) and the
// graph_god_nodes change that keeps widget targets off the asset side.
// Both need the bump for the same reason: refs are only written by
Expand All @@ -29,7 +34,7 @@ package catalog
// SnapshotSource / SourceId / SourceBranch / SourceRevision columns
// from every row (issue #576).
// 1 — initial flat schema with denormalized snapshot columns on every row.
const CatalogSchemaVersion = "11"
const CatalogSchemaVersion = "12"

// MetaSchemaVersion is the catalog_meta key that records the schema version
// the cache was built against.
Expand Down Expand Up @@ -118,6 +123,18 @@ func (c *Catalog) createTables() error {
AccessRuleCount INTEGER DEFAULT 0,
ValidationRuleCount INTEGER DEFAULT 0,
HasEventHandlers INTEGER DEFAULT 0,
-- Mendix stores the four audit members as BOOLEANS on the entity's
-- generalization node, not as attributes, so they are absent from
-- CATALOG.ATTRIBUTES by construction: "does this entity have a
-- CreatedDate" was unanswerable from SQL, and a LEFT JOIN against
-- attributes reported every entity as missing it forever, even
-- after one was added. DESCRIBE ENTITY renders them in the
-- attribute list (CreatedDate: AutoCreatedDate), which is what
-- makes their absence here surprising.
HasCreatedDate INTEGER DEFAULT 0,
HasChangedDate INTEGER DEFAULT 0,
HasOwner INTEGER DEFAULT 0,
HasChangedBy INTEGER DEFAULT 0,
IsExternal INTEGER DEFAULT 0,
ExternalService TEXT,
ProjectId TEXT,
Expand Down
23 changes: 21 additions & 2 deletions mdl/linter/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ type Entity struct {
ValidationRuleCount int
HasEventHandlers bool
IsExternal bool
// The four audit members. Mendix stores these as booleans on the entity's
// generalization node rather than as attributes, so they are not in
// AttributeCount and a rule cannot find them by walking Attributes().
HasCreatedDate bool
HasChangedDate bool
HasOwner bool
HasChangedBy bool
}

// Entities returns an iterator over all entities (excluding system modules).
Expand All @@ -183,7 +190,8 @@ func (ctx *LintContext) Entities() iter.Seq[Entity] {
END,
e.Description, e.Generalization, e.AttributeCount,
e.AccessRuleCount, e.ValidationRuleCount,
e.HasEventHandlers, e.IsExternal
e.HasEventHandlers, e.IsExternal,
e.HasCreatedDate, e.HasChangedDate, e.HasOwner, e.HasChangedBy
FROM entities e
LEFT JOIN modules m ON e.ModuleName = m.Name
WHERE %s
Expand All @@ -199,10 +207,17 @@ func (ctx *LintContext) Entities() iter.Seq[Entity] {
var e Entity
var desc, gen, folder sql.NullString
var hasEventHandlers, isExternal int
// Nullable on purpose: a row written before these columns existed,
// or any fixture that does not name them, scans as NULL, and NULL
// into a plain int fails the whole query -- which the iterator
// reports as "no entities" and every rule then reads as "nothing
// to flag". A silent green run, not an error.
var hasCreatedDate, hasChangedDate, hasOwner, hasChangedBy sql.NullInt64
err := rows.Scan(&e.ID, &e.Name, &e.QualifiedName, &e.ModuleName, &folder,
&e.EntityType, &desc, &gen, &e.AttributeCount,
&e.AccessRuleCount, &e.ValidationRuleCount,
&hasEventHandlers, &isExternal)
&hasEventHandlers, &isExternal,
&hasCreatedDate, &hasChangedDate, &hasOwner, &hasChangedBy)
if err != nil {
ctx.recordQueryError("Entities (row scan)", err)
continue
Expand All @@ -212,6 +227,10 @@ func (ctx *LintContext) Entities() iter.Seq[Entity] {
e.Generalization = gen.String
e.HasEventHandlers = hasEventHandlers == 1
e.IsExternal = isExternal == 1
e.HasCreatedDate = hasCreatedDate.Int64 == 1
e.HasChangedDate = hasChangedDate.Int64 == 1
e.HasOwner = hasOwner.Int64 == 1
e.HasChangedBy = hasChangedBy.Int64 == 1

if ctx.IsExcluded(e.ModuleName) {
continue
Expand Down
9 changes: 7 additions & 2 deletions mdl/linter/context_queryerror_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ func brokenCatalogDB(t *testing.T) catalog.CatalogDB {
`CREATE TABLE entities (Id TEXT, Name TEXT, QualifiedName TEXT, ModuleName TEXT,
Folder TEXT, EntityType TEXT, Description TEXT, Generalization TEXT,
AttributeCount INTEGER, AccessRuleCount INTEGER, ValidationRuleCount INTEGER,
HasEventHandlers INTEGER, IsExternal INTEGER)`,
HasEventHandlers INTEGER, IsExternal INTEGER,
HasCreatedDate INTEGER, HasChangedDate INTEGER,
HasOwner INTEGER, HasChangedBy INTEGER)`,
`INSERT INTO modules VALUES ('ModA', '')`,
`INSERT INTO entities VALUES ('e1','E','ModA.E','ModA','','PERSISTENT','','',0,0,0,0,0)`,
`INSERT INTO entities (Id, Name, QualifiedName, ModuleName, Folder, EntityType,
Description, Generalization, AttributeCount, AccessRuleCount,
ValidationRuleCount, HasEventHandlers, IsExternal)
VALUES ('e1','E','ModA.E','ModA','','PERSISTENT','','',0,0,0,0,0)`,
} {
if _, err := db.Exec(q); err != nil {
t.Fatalf("exec %s: %v", q, err)
Expand Down
10 changes: 8 additions & 2 deletions mdl/linter/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ func setupModuleFilterDB(t *testing.T) catalog.CatalogDB {
Id TEXT, Name TEXT, QualifiedName TEXT, ModuleName TEXT, Folder TEXT,
EntityType TEXT, Description TEXT, Generalization TEXT,
AttributeCount INTEGER, AccessRuleCount INTEGER, ValidationRuleCount INTEGER,
HasEventHandlers INTEGER, IsExternal INTEGER
HasEventHandlers INTEGER, IsExternal INTEGER,
HasCreatedDate INTEGER, HasChangedDate INTEGER,
HasOwner INTEGER, HasChangedBy INTEGER
)`)
if err != nil {
t.Fatalf("create entities table: %v", err)
Expand All @@ -53,7 +55,11 @@ func setupModuleFilterDB(t *testing.T) catalog.CatalogDB {
if _, err := db.Exec(`INSERT INTO modules VALUES (?, ?, '')`, mod+"-id", mod); err != nil {
t.Fatalf("insert module %s: %v", mod, err)
}
if _, err := db.Exec(`INSERT INTO entities VALUES (?, ?, ?, ?, '', 'PERSISTENT', '', '', 0, 0, 0, 0, 0)`,
if _, err := db.Exec(`INSERT INTO entities
(Id, Name, QualifiedName, ModuleName, Folder, EntityType, Description,
Generalization, AttributeCount, AccessRuleCount, ValidationRuleCount,
HasEventHandlers, IsExternal)
VALUES (?, ?, ?, ?, '', 'PERSISTENT', '', '', 0, 0, 0, 0, 0)`,
mod+"_e", mod+"_Entity", mod+".Entity", mod); err != nil {
t.Fatalf("insert entity for %s: %v", mod, err)
}
Expand Down
13 changes: 11 additions & 2 deletions mdl/linter/rules/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,23 @@ func setupEntitiesDB(t *testing.T, entities [][]any) catalog.CatalogDB {
Id TEXT, Name TEXT, QualifiedName TEXT, ModuleName TEXT, Folder TEXT,
EntityType TEXT, Description TEXT, Generalization TEXT,
AttributeCount INTEGER, AccessRuleCount INTEGER, ValidationRuleCount INTEGER,
HasEventHandlers INTEGER, IsExternal INTEGER
HasEventHandlers INTEGER, IsExternal INTEGER,
HasCreatedDate INTEGER, HasChangedDate INTEGER,
HasOwner INTEGER, HasChangedBy INTEGER
)`)
if err != nil {
t.Fatalf("failed to create entities table: %v", err)
}

for _, row := range entities {
_, err := db.Exec(`INSERT INTO entities VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
// Named columns, not positional: the fixture supplies the 13 a rule
// test cares about, and a column added to the real schema (the four
// audit members were) must not break every caller.
_, err := db.Exec(`INSERT INTO entities
(Id, Name, QualifiedName, ModuleName, Folder, EntityType, Description,
Generalization, AttributeCount, AccessRuleCount, ValidationRuleCount,
HasEventHandlers, IsExternal)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
row...)
if err != nil {
t.Fatalf("failed to insert entity: %v", err)
Expand Down
4 changes: 4 additions & 0 deletions mdl/linter/starlark.go
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,10 @@ func entityToStarlark(e Entity) starlark.Value {
"validation_rule_count": starlark.MakeInt(e.ValidationRuleCount),
"has_event_handlers": starlark.Bool(e.HasEventHandlers),
"is_external": starlark.Bool(e.IsExternal),
"has_created_date": starlark.Bool(e.HasCreatedDate),
"has_changed_date": starlark.Bool(e.HasChangedDate),
"has_owner": starlark.Bool(e.HasOwner),
"has_changed_by": starlark.Bool(e.HasChangedBy),
})
}

Expand Down
Loading
Loading