Skip to content

Commit 1547f5b

Browse files
akoclaude
andcommitted
fix(#397): replace O(N²) GetMicroflow/GetNanoflow with direct unit lookup
Lint rules that call GetMicroflow(id) inside a catalog microflow loop were triggering N full ListMicroflows() scans — one per iteration — each re-parsing all microflow BSON from the MPR database. On large projects this caused mxcli report to hang indefinitely. Add getUnitByID(): V1 does a single-row SQLite query by BLOB primary key, V2 uses the existing unit cache then reads one mxunit file. Both paths are effectively O(1) vs the previous O(N) full scan, making the overall lint pass O(N) instead of O(N²). Affected rules: ValidationFeedback, OverlappingActivities, NoCommitInLoop, ExclusiveSplitCaption, ErrorHandlingOnCalls, NoContinueErrorHandling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 24a939b commit 1547f5b

2 files changed

Lines changed: 74 additions & 16 deletions

File tree

sdk/mpr/reader_documents.go

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -201,19 +201,16 @@ func (r *Reader) ListMicroflows() ([]*microflows.Microflow, error) {
201201
}
202202

203203
// GetMicroflow retrieves a microflow by ID.
204+
// Uses a direct unit lookup (O(1) for V1, O(cache) for V2) instead of loading all microflows.
204205
func (r *Reader) GetMicroflow(id model.ID) (*microflows.Microflow, error) {
205-
microflowsList, err := r.ListMicroflows()
206+
unit, err := r.getUnitByID(string(id))
206207
if err != nil {
207208
return nil, err
208209
}
209-
210-
for _, mf := range microflowsList {
211-
if mf.ID == id {
212-
return mf, nil
213-
}
210+
if unit == nil {
211+
return nil, fmt.Errorf("microflow not found: %s", id)
214212
}
215-
216-
return nil, fmt.Errorf("microflow not found: %s", id)
213+
return r.parseMicroflow(unit.ID, unit.ContainerID, unit.Contents)
217214
}
218215

219216
// IsRule reports whether the given qualified name refers to a rule
@@ -285,19 +282,16 @@ func (r *Reader) ListNanoflows() ([]*microflows.Nanoflow, error) {
285282
}
286283

287284
// GetNanoflow retrieves a nanoflow by ID.
285+
// Uses a direct unit lookup (O(1) for V1, O(cache) for V2) instead of loading all nanoflows.
288286
func (r *Reader) GetNanoflow(id model.ID) (*microflows.Nanoflow, error) {
289-
nanoflows, err := r.ListNanoflows()
287+
unit, err := r.getUnitByID(string(id))
290288
if err != nil {
291289
return nil, err
292290
}
293-
294-
for _, nf := range nanoflows {
295-
if nf.ID == id {
296-
return nf, nil
297-
}
291+
if unit == nil {
292+
return nil, fmt.Errorf("nanoflow not found: %s", id)
298293
}
299-
300-
return nil, fmt.Errorf("nanoflow not found: %s", id)
294+
return r.parseNanoflow(unit.ID, unit.ContainerID, unit.Contents)
301295
}
302296

303297
// ListPages returns all pages in the project.

sdk/mpr/reader_units.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
package mpr
55

66
import (
7+
"database/sql"
8+
"errors"
79
"fmt"
810
"os"
911
"path/filepath"
1012
"strings"
1113

14+
"github.com/mendixlabs/mxcli/mdl/types"
1215
"go.mongodb.org/mongo-driver/bson"
1316
)
1417

@@ -587,3 +590,64 @@ func (r *Reader) ListRawUnits(objectType string) ([]*RawUnitInfo, error) {
587590

588591
return result, nil
589592
}
593+
594+
// getUnitByID fetches a single rawUnit by its UUID string without loading all units.
595+
// Returns (nil, nil) when the ID is not found.
596+
// V1: direct SQLite BLOB lookup — O(1). V2: cache lookup + single file read — O(cache size).
597+
func (r *Reader) getUnitByID(id string) (*rawUnit, error) {
598+
if r.version == MPRVersionV2 {
599+
return r.getUnitByIDV2(id)
600+
}
601+
return r.getUnitByIDV1(id)
602+
}
603+
604+
func (r *Reader) getUnitByIDV1(id string) (*rawUnit, error) {
605+
blob := types.UUIDToBlob(id)
606+
if blob == nil {
607+
return nil, fmt.Errorf("invalid unit ID: %s", id)
608+
}
609+
row := r.db.QueryRow(
610+
"SELECT UnitID, ContainerID, ContainmentName, Contents FROM Unit WHERE UnitID = ?",
611+
blob,
612+
)
613+
var unitID, containerID []byte
614+
var containmentName string
615+
var contents []byte
616+
if err := row.Scan(&unitID, &containerID, &containmentName, &contents); err != nil {
617+
if errors.Is(err, sql.ErrNoRows) {
618+
return nil, nil
619+
}
620+
return nil, fmt.Errorf("failed to query unit %s: %w", id, err)
621+
}
622+
return &rawUnit{
623+
ID: blobToUUID(unitID),
624+
ContainerID: blobToUUID(containerID),
625+
ContainmentName: containmentName,
626+
Type: getTypeFromContents(contents),
627+
Contents: contents,
628+
}, nil
629+
}
630+
631+
func (r *Reader) getUnitByIDV2(id string) (*rawUnit, error) {
632+
if !r.unitCacheValid {
633+
if err := r.buildUnitCache(); err != nil {
634+
return nil, err
635+
}
636+
}
637+
for _, cu := range r.unitCache {
638+
if cu.ID == id {
639+
contents, err := r.readMprContents(id)
640+
if err != nil {
641+
return nil, err
642+
}
643+
return &rawUnit{
644+
ID: cu.ID,
645+
ContainerID: cu.ContainerID,
646+
ContainmentName: cu.ContainmentName,
647+
Type: cu.Type,
648+
Contents: contents,
649+
}, nil
650+
}
651+
}
652+
return nil, nil
653+
}

0 commit comments

Comments
 (0)