Skip to content

Commit 37ce2db

Browse files
engalarclaude
andcommitted
feat: diag --check-units + bson dump --format bson
diag --check-units: - New diagnostic command for MPR v2 projects - Detects orphan units (DB entry without mxunit file) and stale files - --fix flag removes stale mxunit files and empty parent directories - Added ListAllUnitIDs() to Reader for querying Unit table bson dump --format bson: - New raw BSON output format for roundtrip testing baseline extraction Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 425dfdb commit 37ce2db

3 files changed

Lines changed: 119 additions & 1 deletion

File tree

cmd/mxcli/cmd_bson_dump.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ Examples:
4444
4545
# Save dump to file
4646
mxcli bson dump -p app.mpr --type page --object "PgTest.MyPage" > mypage.json
47+
48+
# Extract raw BSON baseline for roundtrip testing
49+
mxcli bson dump -p app.mpr --type page --object "PgTest.MyPage" --format bson > mypage.mxunit
4750
`,
4851
Run: func(cmd *cobra.Command, args []string) {
4952
projectPath, _ := cmd.Flags().GetString("project")
@@ -136,6 +139,12 @@ Examples:
136139
os.Exit(1)
137140
}
138141

142+
if format == "bson" {
143+
// Write raw BSON bytes to stdout (for baseline extraction)
144+
os.Stdout.Write(obj.Contents)
145+
return
146+
}
147+
139148
if format == "ndsl" {
140149
var doc bson.D
141150
if err := bson.Unmarshal(obj.Contents, &doc); err != nil {
@@ -342,5 +351,5 @@ func init() {
342351
bsonDumpCmd.Flags().StringP("object", "o", "", "Object qualified name to dump (e.g., Module.PageName)")
343352
bsonDumpCmd.Flags().BoolP("list", "l", false, "List all objects of the specified type")
344353
bsonDumpCmd.Flags().StringSliceP("compare", "c", nil, "Compare two objects: --compare Obj1,Obj2")
345-
bsonDumpCmd.Flags().String("format", "json", "Output format: json, ndsl")
354+
bsonDumpCmd.Flags().String("format", "json", "Output format: json, ndsl, bson (raw bytes)")
346355
}

cmd/mxcli/diag.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"time"
1717

1818
"github.com/mendixlabs/mxcli/mdl/diaglog"
19+
"github.com/mendixlabs/mxcli/sdk/mpr"
1920
"github.com/spf13/cobra"
2021
)
2122

@@ -47,6 +48,18 @@ Examples:
4748
return
4849
}
4950

51+
checkUnits, _ := cmd.Flags().GetBool("check-units")
52+
fix, _ := cmd.Flags().GetBool("fix")
53+
if checkUnits {
54+
projectPath, _ := cmd.Flags().GetString("project")
55+
if projectPath == "" {
56+
fmt.Fprintln(os.Stderr, "Error: --check-units requires -p <project.mpr>")
57+
os.Exit(1)
58+
}
59+
runCheckUnits(projectPath, fix)
60+
return
61+
}
62+
5063
if tail > 0 {
5164
runDiagTail(logDir, tail)
5265
return
@@ -60,6 +73,8 @@ func init() {
6073
diagCmd.Flags().Bool("log-path", false, "Print log directory path")
6174
diagCmd.Flags().Bool("bundle", false, "Create tar.gz with logs for bug reports")
6275
diagCmd.Flags().Int("tail", 0, "Show last N log entries")
76+
diagCmd.Flags().Bool("check-units", false, "Check for orphan units and stale mxunit files (MPR v2)")
77+
diagCmd.Flags().Bool("fix", false, "Auto-fix issues found by --check-units")
6378
}
6479

6580
// runDiagInfo shows diagnostic summary.
@@ -252,3 +267,79 @@ func formatBytes(b int64) string {
252267
}
253268
return fmt.Sprintf("%d KB", b/1024)
254269
}
270+
271+
// runCheckUnits checks for orphan units (Unit table entry without mxunit file)
272+
// and stale mxunit files (file exists but no Unit table entry). MPR v2 only.
273+
func runCheckUnits(mprPath string, fix bool) {
274+
reader, err := mpr.Open(mprPath)
275+
if err != nil {
276+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
277+
os.Exit(1)
278+
}
279+
defer reader.Close()
280+
281+
contentsDir := reader.ContentsDir()
282+
if contentsDir == "" {
283+
fmt.Println("Not an MPR v2 project (no mprcontents directory)")
284+
return
285+
}
286+
287+
// Build set of unit UUIDs from database
288+
unitIDs, err := reader.ListAllUnitIDs()
289+
if err != nil {
290+
fmt.Fprintf(os.Stderr, "Error listing units: %v\n", err)
291+
os.Exit(1)
292+
}
293+
unitSet := make(map[string]bool, len(unitIDs))
294+
for _, id := range unitIDs {
295+
unitSet[id] = true
296+
}
297+
298+
// Scan mxunit files
299+
files, err := filepath.Glob(filepath.Join(contentsDir, "*", "*", "*.mxunit"))
300+
if err != nil {
301+
fmt.Fprintf(os.Stderr, "Error scanning mxunit files: %v\n", err)
302+
os.Exit(1)
303+
}
304+
fileSet := make(map[string]string, len(files)) // uuid → filepath
305+
for _, f := range files {
306+
uuid := strings.TrimSuffix(filepath.Base(f), ".mxunit")
307+
fileSet[uuid] = f
308+
}
309+
310+
// Check for orphan units (in DB but no file)
311+
orphans := 0
312+
for _, id := range unitIDs {
313+
if _, ok := fileSet[id]; !ok {
314+
fmt.Printf("ORPHAN UNIT: %s (in Unit table but no mxunit file)\n", id)
315+
orphans++
316+
}
317+
}
318+
319+
// Check for stale files (file exists but not in DB)
320+
stale := 0
321+
for uuid, fpath := range fileSet {
322+
if !unitSet[uuid] {
323+
fmt.Printf("STALE FILE: %s\n", uuid)
324+
stale++
325+
if fix {
326+
if err := os.Remove(fpath); err != nil {
327+
fmt.Fprintf(os.Stderr, " ERROR removing: %v\n", err)
328+
} else {
329+
fmt.Printf(" REMOVED: %s\n", fpath)
330+
// Clean empty parent dirs
331+
dir2 := filepath.Dir(fpath)
332+
os.Remove(dir2)
333+
dir1 := filepath.Dir(dir2)
334+
os.Remove(dir1)
335+
}
336+
}
337+
}
338+
}
339+
340+
fmt.Printf("\nSummary: %d units in DB, %d mxunit files, %d orphans, %d stale\n",
341+
len(unitIDs), len(files), orphans, stale)
342+
if stale > 0 && !fix {
343+
fmt.Println("Run with --fix to auto-remove stale files")
344+
}
345+
}

sdk/mpr/reader.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,24 @@ func (r *Reader) ContentsDir() string {
178178
return r.contentsDir
179179
}
180180

181+
// ListAllUnitIDs returns all unit UUIDs from the Unit table.
182+
func (r *Reader) ListAllUnitIDs() ([]string, error) {
183+
rows, err := r.db.Query("SELECT UnitID FROM Unit")
184+
if err != nil {
185+
return nil, err
186+
}
187+
defer rows.Close()
188+
var ids []string
189+
for rows.Next() {
190+
var unitID []byte
191+
if err := rows.Scan(&unitID); err != nil {
192+
return nil, fmt.Errorf("scanning unit ID: %w", err)
193+
}
194+
ids = append(ids, BlobToUUID(unitID))
195+
}
196+
return ids, rows.Err()
197+
}
198+
181199
// ProjectVersion returns the Mendix project version information.
182200
func (r *Reader) ProjectVersion() *version.ProjectVersion {
183201
return r.projectVersion

0 commit comments

Comments
 (0)