From 629137419a5b6ac8aaa8b83007b6e9c2350b5e26 Mon Sep 17 00:00:00 2001 From: Ako Date: Thu, 17 Sep 2026 17:38:06 +0000 Subject: [PATCH 1/2] feat(catalog): index an entity's four audit members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Which entities have no audit trail?" was unanswerable from SQL, and the query anyone would write returned a wrong answer rather than an error: 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 reports every entity as missing CreatedDate, forever, including ones that have it. Measured before this change: adding CreatedDate/ChangedDate to an entity and rebuilding the catalog with `refresh catalog full` left the row count unchanged, and CATALOG.ATTRIBUTES for that entity still listed only its four ordinary attributes. The catalog was not dropping anything. Mendix stores Owner, ChangedBy, CreatedDate and ChangedDate as BOOLEANS on the entity's generalization node (NoGeneralization.HasCreatedDate and friends), not as attributes, so they are absent from CATALOG.ATTRIBUTES by construction and the builder was a faithful projection of a model that has no such rows. What made it a trap is that DESCRIBE ENTITY renders them IN the attribute list, as `CreatedDate: AutoCreatedDate` — so the one view a user checks against says they are attributes, and the catalog says they do not exist. The fix follows the model rather than the rendering: four columns on CATALOG.ENTITIES, mirroring the booleans that are actually stored, so SELECT QualifiedName FROM CATALOG.ENTITIES WHERE HasCreatedDate = 0 is both simpler than the join and correct. Fabricating attribute rows was the alternative and is worse: it would invent structure the model does not have, and disagree with AttributeCount, which counts real attributes. Schema version 11 -> 12. 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 machines that already have a catalog. Verified end to end on a real 11.14 project: AuthToken reads 0/0 before and 1/1 after `add attribute if not exists CreatedDate: AutoCreatedDate`, the other four entities stay 0, and the WHERE HasCreatedDate = 0 query goes from 5 rows to 4. Stubbing the four values back to literal zeros fails TestEntitiesCarryTheAuditMembers with the pre-fix symptom. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BNDe35kDNsMX5cz4Ahn4rk --- mdl/catalog/builder_audit_members_test.go | 93 +++++++++++++++++++++++ mdl/catalog/builder_modules.go | 7 +- mdl/catalog/tables.go | 19 ++++- 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 mdl/catalog/builder_audit_members_test.go diff --git a/mdl/catalog/builder_audit_members_test.go b/mdl/catalog/builder_audit_members_test.go new file mode 100644 index 0000000000..3bd4e9d426 --- /dev/null +++ b/mdl/catalog/builder_audit_members_test.go @@ -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) + } +} diff --git a/mdl/catalog/builder_modules.go b/mdl/catalog/builder_modules.go index 6f5d88f7a7..ce90d6a4d6 100644 --- a/mdl/catalog/builder_modules.go +++ b/mdl/catalog/builder_modules.go @@ -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 @@ -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, diff --git a/mdl/catalog/tables.go b/mdl/catalog/tables.go index 60f1b1f59c..69d1225fa6 100644 --- a/mdl/catalog/tables.go +++ b/mdl/catalog/tables.go @@ -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 @@ -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. @@ -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, From d6c4059c6cfa809de016afa4711d3b846ed5c0d0 Mon Sep 17 00:00:00 2001 From: Ako Date: Thu, 17 Sep 2026 17:46:43 +0000 Subject: [PATCH 2/2] feat(lint): expose an entity's audit members to Starlark rules Follow-up to the catalog columns in the previous commit: with the data indexed, a rule still could not read it. `entity` carried has_event_handlers and is_external and nothing about the audit trail, and the two obvious workarounds do not exist -- the members are not attributes, so attributes_for() never yields them and attribute_count excludes them. "Which persistent entities have no CreatedDate" was unwritable as a rule. Four fields: has_created_date, has_changed_date, has_owner, has_changed_by. Scanned as sql.NullInt64 rather than int, which is load-bearing and is what the first cut got wrong. A row without the columns -- any fixture that does not name them -- scans as NULL, NULL into a plain int fails the whole query, and Entities() swallows a query error by yielding nothing. Every rule then sees zero entities and reports zero violations: a green run that checked nothing. That is the same failure shape the catalog-mode bump guards against, reached through the scan instead. Three rule tests went green with the entity table empty before this was fixed. The linter's test fixtures declare their own copy of the entities schema, so adding a column broke them -- three of them INSERTed positionally. They now name their columns, which is why a fourth column will not break them again. That duplication is still a maintenance risk and is worth collapsing onto the real schema separately. Two tests, and the control is what makes them worth having: wiring all four to starlark.Bool(false) makes the first report both entities instead of one and the second report none, so a field that silently reads False everywhere cannot pass. Verified end to end as well -- a QUAL900-style rule over a real 11.14 project names 6 entities, and after `add attribute if not exists CreatedDate: AutoCreatedDate` on one of them it names 5 and stops naming that one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BNDe35kDNsMX5cz4Ahn4rk --- mdl/linter/context.go | 23 +++- mdl/linter/context_queryerror_test.go | 9 +- mdl/linter/context_test.go | 10 +- mdl/linter/rules/helpers_test.go | 13 ++- mdl/linter/starlark.go | 4 + mdl/linter/starlark_audit_members_test.go | 132 ++++++++++++++++++++++ 6 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 mdl/linter/starlark_audit_members_test.go diff --git a/mdl/linter/context.go b/mdl/linter/context.go index efa313f8d7..6326095ea4 100644 --- a/mdl/linter/context.go +++ b/mdl/linter/context.go @@ -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). @@ -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 @@ -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 @@ -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 diff --git a/mdl/linter/context_queryerror_test.go b/mdl/linter/context_queryerror_test.go index 6dc4279ff5..744e72477e 100644 --- a/mdl/linter/context_queryerror_test.go +++ b/mdl/linter/context_queryerror_test.go @@ -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) diff --git a/mdl/linter/context_test.go b/mdl/linter/context_test.go index 72c9731324..e37ceb1fcd 100644 --- a/mdl/linter/context_test.go +++ b/mdl/linter/context_test.go @@ -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) @@ -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) } diff --git a/mdl/linter/rules/helpers_test.go b/mdl/linter/rules/helpers_test.go index 7b233342e2..c7fbcf47b7 100644 --- a/mdl/linter/rules/helpers_test.go +++ b/mdl/linter/rules/helpers_test.go @@ -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) diff --git a/mdl/linter/starlark.go b/mdl/linter/starlark.go index a74745b641..b69fb5572d 100644 --- a/mdl/linter/starlark.go +++ b/mdl/linter/starlark.go @@ -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), }) } diff --git a/mdl/linter/starlark_audit_members_test.go b/mdl/linter/starlark_audit_members_test.go new file mode 100644 index 0000000000..4e8c0bc3cb --- /dev/null +++ b/mdl/linter/starlark_audit_members_test.go @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 + +package linter_test + +import ( + "database/sql" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/catalog" + "github.com/mendixlabs/mxcli/mdl/linter" + _ "modernc.org/sqlite" +) + +// The four audit members are booleans on the entity's generalization node, not +// attributes, so a Starlark rule cannot find them by walking attributes() and +// attribute_count does not include them. Without these fields a rule asking +// "which persistent entities have no audit trail" has nothing to read. +func TestStarlarkEntityExposesAuditMembers(t *testing.T) { + db := auditFixtureDB(t) + + dir := t.TempDir() + src := ` +RULE_ID = "TEST001" +RULE_NAME = "AuditTrail" +DESCRIPTION = "entities without an audit trail" +CATEGORY = "quality" +SEVERITY = "warning" + +def check(): + violations = [] + for e in entities(): + if e.entity_type == "Persistent" and not e.has_created_date: + violations.append(violation( + message="{} has no CreatedDate (owner={} changed_by={} changed_date={})".format( + e.qualified_name, e.has_owner, e.has_changed_by, e.has_changed_date), + location=location(module=e.module_name, document_type="Entity", + document_name=e.qualified_name), + )) + return violations +` + path := filepath.Join(dir, "audit.star") + if err := os.WriteFile(path, []byte(src), 0o644); err != nil { + t.Fatal(err) + } + r, err := linter.LoadStarlarkRule(path) + if err != nil { + t.Fatalf("LoadStarlarkRule: %v -- a field the rule names must exist on the struct", err) + } + + got := r.Check(linter.NewLintContextFromDB(db)) + if len(got) != 1 { + t.Fatalf("got %d violations, want 1 (only the bare entity): %v", len(got), got) + } + msg := got[0].Message + if !strings.Contains(msg, "Sales.Lookup") { + t.Errorf("violation names %q, want the entity without CreatedDate", msg) + } + // The other three must be readable too, and false on this entity -- a + // field that silently reads False everywhere would pass a weaker test. + for _, want := range []string{"owner=False", "changed_by=False", "changed_date=False"} { + if !strings.Contains(msg, want) { + t.Errorf("message %q missing %q", msg, want) + } + } +} + +// The audited entity must read True, or the fields are wired to a constant. +func TestStarlarkEntityAuditMembersReadTrue(t *testing.T) { + db := auditFixtureDB(t) + + dir := t.TempDir() + src := ` +RULE_ID = "TEST002" +RULE_NAME = "Audited" +DESCRIPTION = "audited entities" +CATEGORY = "quality" +SEVERITY = "info" + +def check(): + violations = [] + for e in entities(): + if e.has_created_date and e.has_changed_date and e.has_owner and e.has_changed_by: + violations.append(violation( + message=e.qualified_name, + location=location(module=e.module_name, document_type="Entity", + document_name=e.qualified_name))) + return violations +` + path := filepath.Join(dir, "audited.star") + if err := os.WriteFile(path, []byte(src), 0o644); err != nil { + t.Fatal(err) + } + r, err := linter.LoadStarlarkRule(path) + if err != nil { + t.Fatal(err) + } + got := r.Check(linter.NewLintContextFromDB(db)) + if len(got) != 1 || got[0].Message != "Sales.Order" { + t.Fatalf("got %v, want exactly Sales.Order -- all four flags must read True there", got) + } +} + +func auditFixtureDB(t *testing.T) catalog.CatalogDB { + t.Helper() + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + stmts := []string{ + `CREATE TABLE modules (Id TEXT, Name TEXT, Source TEXT)`, + `INSERT INTO modules VALUES ('m1', 'Sales', '')`, + `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, + HasCreatedDate INTEGER, HasChangedDate INTEGER, + HasOwner INTEGER, HasChangedBy INTEGER)`, + `INSERT INTO entities VALUES + ('e1','Order','Sales.Order','Sales','','PERSISTENT','','',3,1,0,0,0, 1,1,1,1), + ('e2','Lookup','Sales.Lookup','Sales','','PERSISTENT','','',2,1,0,0,0, 0,0,0,0)`, + } + for _, s := range stmts { + if _, err := db.Exec(s); err != nil { + t.Fatalf("fixture %q: %v", s, err) + } + } + return catalog.WrapSqlDB(db) +}