Skip to content

Commit b31bc4b

Browse files
committed
Merge branch 'bugs'
2 parents cc48fdf + 68ced13 commit b31bc4b

5 files changed

Lines changed: 246 additions & 16 deletions

File tree

mdl/executor/cmd_entities.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ func (e *Executor) execDropEntity(s *ast.DropEntityStmt) error {
698698
e.warnEntityReferences(s.Name.String())
699699

700700
// If this is a view entity, also delete the associated ViewEntitySourceDocument
701-
if entity.Source == "OqlViewEntitySource" {
701+
if entity.Source == "DomainModels$OqlViewEntitySource" {
702702
if err := e.writer.DeleteViewEntitySourceDocumentByName(s.Name.Module, s.Name.Name); err != nil {
703703
return fmt.Errorf("failed to delete view entity source document: %w", err)
704704
}

mdl/executor/cmd_pages_builder_v3_pluggable.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (pb *pageBuilder) buildComboBoxV3(w *ast.WidgetV3) (*pages.CustomWidget, er
6363
// resolved to a 3-part member path: Module.Entity.AssociationName
6464
// (same format as attributes, since associations are entity members)
6565
if attr := w.GetAttribute(); attr != "" {
66-
assocPath := pb.resolveAttributePath(attr)
66+
assocPath := pb.resolveAssociationPath(attr)
6767
updatedObject = updateWidgetPropertyValue(updatedObject, propertyTypeIDs, "attributeAssociation", func(val bson.D) bson.D {
6868
return setAssociationRef(val, assocPath)
6969
})

mdl/executor/oql_type_inference.go

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func inferTypeStatic(expr string) ast.DataType {
160160
}
161161
}
162162

163-
// count(...) → Integer
163+
// count(...) → Integer (Mendix OQL COUNT returns Integer)
164164
if strings.HasPrefix(upper, "COUNT(") {
165165
return ast.DataType{Kind: ast.TypeInteger}
166166
}
@@ -529,7 +529,7 @@ func (e *Executor) inferAttributeTypeFromEntity(entityQualifiedName, attrName st
529529
func (e *Executor) inferAggregateType(expr string, col *OQLColumnInfo, aliasMap map[string]string) ast.DataType {
530530
upperExpr := strings.ToUpper(strings.TrimSpace(expr))
531531

532-
// COUNT(*) or COUNT(expression)
532+
// COUNT(*) or COUNT(expression) → Integer (Mendix OQL COUNT returns Integer)
533533
if strings.HasPrefix(upperExpr, "COUNT(") {
534534
col.IsAggregate = true
535535
col.AggregateFunc = "COUNT"
@@ -695,25 +695,16 @@ func convertDomainModelTypeToAST(attrType domainmodel.AttributeType) ast.DataTyp
695695
}
696696

697697
// typesStrictlyCompatible checks if declared and inferred types match exactly.
698-
// Integer and Long are interchangeable, but Decimal does not accept Integer.
699698
// This is used for static OQL type checking where the inferred type is definitive
700699
// (e.g., count() always returns Integer, sum() always returns Decimal).
700+
// MxBuild treats Integer and Long as distinct types for VIEW entity sync validation,
701+
// so we must not treat them as interchangeable here.
701702
func typesStrictlyCompatible(declared, inferred ast.DataType) bool {
702703
if inferred.Kind == ast.TypeUnknown {
703704
return true
704705
}
705706

706-
if declared.Kind == inferred.Kind {
707-
return true
708-
}
709-
710-
// Integer and Long are interchangeable
711-
if (declared.Kind == ast.TypeInteger && inferred.Kind == ast.TypeLong) ||
712-
(declared.Kind == ast.TypeLong && inferred.Kind == ast.TypeInteger) {
713-
return true
714-
}
715-
716-
return false
707+
return declared.Kind == inferred.Kind
717708
}
718709

719710
// typesCompatible checks if declared and inferred types are compatible.

mdl/executor/roundtrip_mxcheck_test.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,3 +617,232 @@ func TestMxCheck_MicroflowWithCallParams(t *testing.T) {
617617
t.Logf("mx check passed:\n%s", output)
618618
}
619619
}
620+
621+
// TestMxCheck_ViewEntitySimple creates a simple VIEW entity (no aggregates)
622+
// and verifies mx check passes.
623+
func TestMxCheck_ViewEntitySimple(t *testing.T) {
624+
if !mxCheckAvailable() {
625+
t.Skip("mx command not available")
626+
}
627+
628+
env := setupTestEnv(t)
629+
defer env.teardown()
630+
631+
mod := testModule
632+
633+
entityName := mod + ".MxCheckProduct"
634+
env.registerCleanup("entity", entityName)
635+
636+
if err := env.executeMDL(`CREATE OR MODIFY PERSISTENT ENTITY ` + entityName + ` (
637+
Name: String(100),
638+
Price: Decimal
639+
);`); err != nil {
640+
t.Fatalf("Failed to create source entity: %v", err)
641+
}
642+
643+
viewName := mod + ".MxCheckProductView"
644+
env.registerCleanup("entity", viewName)
645+
646+
viewMDL := `CREATE VIEW ENTITY ` + viewName + ` (
647+
Name: String(100),
648+
Price: Decimal
649+
) AS (
650+
SELECT p.Name AS Name, p.Price AS Price
651+
FROM ` + entityName + ` AS p
652+
);`
653+
654+
if err := env.executeMDL(viewMDL); err != nil {
655+
t.Fatalf("Failed to create view entity: %v", err)
656+
}
657+
658+
env.executor.Execute(&ast.DisconnectStmt{})
659+
660+
output, err := runMxCheck(t, env.projectPath)
661+
if err != nil {
662+
if strings.Contains(output, "out of sync") {
663+
t.Errorf("mx check reports view entity out of sync with OQL:\n%s", output)
664+
} else if strings.Contains(output, "error") || strings.Contains(output, "Error") {
665+
t.Errorf("mx check found errors:\n%s", output)
666+
} else {
667+
t.Logf("mx check output:\n%s", output)
668+
}
669+
} else {
670+
t.Logf("mx check passed:\n%s", output)
671+
}
672+
}
673+
674+
// TestMxCheck_ViewEntityWithAggregates creates a VIEW entity with aggregate OQL
675+
// (COUNT, SUM, AVG, GROUP BY) and verifies mx check passes.
676+
// Regression test for GitHub issue: COUNT must return Long, not Integer.
677+
func TestMxCheck_ViewEntityWithAggregates(t *testing.T) {
678+
if !mxCheckAvailable() {
679+
t.Skip("mx command not available")
680+
}
681+
682+
env := setupTestEnv(t)
683+
defer env.teardown()
684+
685+
mod := testModule
686+
687+
// Create source entity with numeric fields for aggregation
688+
entityName := mod + ".MxCheckDeal"
689+
env.registerCleanup("entity", entityName)
690+
691+
if err := env.executeMDL(`CREATE OR MODIFY PERSISTENT ENTITY ` + entityName + ` (
692+
Stage: String(50),
693+
Amount: Decimal
694+
);`); err != nil {
695+
t.Fatalf("Failed to create source entity: %v", err)
696+
}
697+
698+
// Create VIEW entity with aggregate OQL
699+
// Note: COUNT returns Long in Mendix OQL, not Integer
700+
viewName := mod + ".MxCheckDealsByStage"
701+
env.registerCleanup("entity", viewName)
702+
703+
viewMDL := `CREATE VIEW ENTITY ` + viewName + ` (
704+
Stage: String(50),
705+
DealCount: Integer,
706+
TotalAmount: Decimal,
707+
AvgAmount: Decimal
708+
) AS (
709+
SELECT
710+
d.Stage AS Stage,
711+
count(d.ID) AS DealCount,
712+
sum(d.Amount) AS TotalAmount,
713+
avg(d.Amount) AS AvgAmount
714+
FROM ` + entityName + ` AS d
715+
GROUP BY d.Stage
716+
);`
717+
718+
if err := env.executeMDL(viewMDL); err != nil {
719+
t.Fatalf("Failed to create view entity: %v", err)
720+
}
721+
722+
// Disconnect to flush changes
723+
env.executor.Execute(&ast.DisconnectStmt{})
724+
725+
// Run mx check
726+
output, err := runMxCheck(t, env.projectPath)
727+
if err != nil {
728+
if strings.Contains(output, "out of sync") {
729+
t.Errorf("mx check reports view entity out of sync with OQL:\n%s", output)
730+
} else if strings.Contains(output, "error") || strings.Contains(output, "Error") {
731+
t.Errorf("mx check found errors:\n%s", output)
732+
} else {
733+
t.Logf("mx check output:\n%s", output)
734+
}
735+
} else {
736+
t.Logf("mx check passed:\n%s", output)
737+
}
738+
}
739+
740+
// TestMxCheck_ComboBoxWithAssociation creates a page with a COMBOBOX widget that
741+
// uses an association attribute and verifies mx check passes.
742+
// Regression test for: COMBOBOX Attribute should resolve as association path (2-part),
743+
// not regular attribute path (3-part).
744+
func TestMxCheck_ComboBoxWithAssociation(t *testing.T) {
745+
if !mxCheckAvailable() {
746+
t.Skip("mx command not available")
747+
}
748+
749+
env := setupTestEnv(t)
750+
defer env.teardown()
751+
752+
mod := testModule
753+
754+
// Create target entity (for the association)
755+
companyEntity := mod + ".MxCheckCompany"
756+
env.registerCleanup("entity", companyEntity)
757+
758+
if err := env.executeMDL(`CREATE OR MODIFY PERSISTENT ENTITY ` + companyEntity + ` (
759+
Name: String(100)
760+
);`); err != nil {
761+
t.Fatalf("Failed to create Company entity: %v", err)
762+
}
763+
764+
// Create source entity (with association to Company)
765+
contactEntity := mod + ".MxCheckContact"
766+
env.registerCleanup("entity", contactEntity)
767+
768+
if err := env.executeMDL(`CREATE OR MODIFY PERSISTENT ENTITY ` + contactEntity + ` (
769+
FullName: String(100),
770+
Email: String(200)
771+
);`); err != nil {
772+
t.Fatalf("Failed to create Contact entity: %v", err)
773+
}
774+
775+
// Create association
776+
assocName := mod + ".MxCheckContact_MxCheckCompany"
777+
778+
if err := env.executeMDL(`CREATE ASSOCIATION ` + assocName + ` FROM ` + contactEntity + ` TO ` + companyEntity + `;`); err != nil {
779+
t.Fatalf("Failed to create association: %v", err)
780+
}
781+
782+
// Create a microflow that returns a Contact (for dataview source)
783+
mfName := mod + ".MxCheckGetContact"
784+
env.registerCleanup("microflow", mfName)
785+
786+
mfMDL := `CREATE MICROFLOW ` + mfName + ` () RETURNS ` + contactEntity + `
787+
BEGIN
788+
RETRIEVE $Contact FROM ` + contactEntity + ` LIMIT 1;
789+
RETURN $Contact;
790+
END;`
791+
792+
if err := env.executeMDL(mfMDL); err != nil {
793+
t.Fatalf("Failed to create microflow: %v", err)
794+
}
795+
796+
// Create page with COMBOBOX using association attribute
797+
pageName := mod + ".MxCheckContactEdit"
798+
env.registerCleanup("page", pageName)
799+
800+
pageMDL := `CREATE PAGE ` + pageName + ` (
801+
Title: 'Contact Edit',
802+
Layout: Atlas_Core.Atlas_Default
803+
) {
804+
DATAVIEW dvContact (DataSource: MICROFLOW ` + mfName + `) {
805+
LAYOUTGRID lgMain {
806+
ROW r1 {
807+
COLUMN c1 (DesktopWidth: 12) {
808+
TEXTBOX txtName (Attribute: FullName, Label: 'Full Name')
809+
COMBOBOX cmbCompany (
810+
Label: 'Company',
811+
Attribute: MxCheckContact_MxCheckCompany,
812+
DataSource: DATABASE ` + companyEntity + `,
813+
CaptionAttribute: Name
814+
)
815+
}
816+
}
817+
}
818+
}
819+
};`
820+
821+
if err := env.executeMDL(pageMDL); err != nil {
822+
t.Fatalf("Failed to create page with ComboBox: %v", err)
823+
}
824+
825+
// Disconnect to flush changes
826+
env.executor.Execute(&ast.DisconnectStmt{})
827+
828+
// Run mx check
829+
output, err := runMxCheck(t, env.projectPath)
830+
if err != nil {
831+
if strings.Contains(output, "no longer exists") {
832+
t.Errorf("mx check reports attribute no longer exists (association not resolved correctly):\n%s", output)
833+
} else if strings.Contains(output, "error") || strings.Contains(output, "Error") {
834+
// CE0642 "Property 'Entity' is required" on the DataView is a known
835+
// limitation of microflow-sourced dataviews — not related to the
836+
// ComboBox association fix under test.
837+
if strings.Contains(output, "CE0642") && !strings.Contains(output, "no longer exists") {
838+
t.Logf("mx check has unrelated DataView error (CE0642), ComboBox association resolved correctly:\n%s", output)
839+
} else {
840+
t.Errorf("mx check found errors:\n%s", output)
841+
}
842+
} else {
843+
t.Logf("mx check output:\n%s", output)
844+
}
845+
} else {
846+
t.Logf("mx check passed:\n%s", output)
847+
}
848+
}

sdk/mpr/reader.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ func OpenWithOptions(path string, opts OpenOptions) (*Reader, error) {
9191
return nil, fmt.Errorf("failed to open database: %w", err)
9292
}
9393

94+
// Limit to single connection to avoid lock contention with SQLite
95+
db.SetMaxOpenConns(1)
96+
97+
// Set busy timeout to prevent SQLITE_BUSY errors during multi-statement
98+
// script execution (e.g., 12+ CREATE PAGE commands in sequence)
99+
if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil {
100+
db.Close()
101+
return nil, fmt.Errorf("failed to set busy_timeout: %w", err)
102+
}
103+
94104
r.db = db
95105

96106
// Detect project version from metadata

0 commit comments

Comments
 (0)