Simplify: O2M batch, domain dedup, db.go split, constants

Performance:
- O2M formatO2MFields: N+1 → single batched query per O2M field
  (collect parent IDs, WHERE inverse IN (...), group by parent)

Code dedup:
- domain.go compileSimpleCondition: 80 lines → 12 lines by delegating
  to compileQualifiedCondition (eliminates duplicate operator handling)
- db.go SeedWithSetup: 170-line monolith → 5 focused sub-functions
  (seedCurrencyAndCountry, seedCompanyAndAdmin, seedJournalsAndSequences,
  seedChartOfAccounts, resetSequences)

Quality:
- Magic numbers (80, 200, 8) replaced with named constants
- Net -34 lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-04-03 13:11:37 +02:00
parent 70320b6b29
commit 6fd9cdea1b
4 changed files with 132 additions and 166 deletions

View File

@@ -7,6 +7,12 @@ import (
"odoo-go/pkg/orm"
)
const (
defaultWebSearchLimit = 80
defaultO2MFetchLimit = 200
defaultNameSearchLimit = 8
)
// handleWebSearchRead implements the web_search_read method.
// Mirrors: odoo/addons/web/models/models.py web_search_read()
// Returns {length: N, records: [...]} instead of just records.
@@ -39,7 +45,7 @@ func handleWebSearchRead(env *orm.Environment, model string, params CallKWParams
// Parse offset, limit, order
offset := 0
limit := 80
limit := defaultWebSearchLimit
order := ""
if v, ok := params.KW["offset"].(float64); ok {
offset = int(v)
@@ -178,6 +184,8 @@ func formatO2MFields(env *orm.Environment, modelName string, records []orm.Value
continue
}
// Collect all parent IDs from records
var parentIDs []int64
for _, rec := range records {
parentID, ok := rec["id"].(int64)
if !ok {
@@ -185,38 +193,58 @@ func formatO2MFields(env *orm.Environment, modelName string, records []orm.Value
parentID = int64(pid)
}
}
if parentID == 0 {
rec[fieldName] = []interface{}{}
continue
if parentID > 0 {
parentIDs = append(parentIDs, parentID)
}
}
// Search child records where inverse_field = parent_id
childRS := env.Model(comodel)
domain := orm.And(orm.Leaf(inverseField, "=", parentID))
found, err := childRS.Search(domain, orm.SearchOpts{Limit: 200})
if err != nil || found.IsEmpty() {
rec[fieldName] = []interface{}{}
continue
// Initialize all records with empty slices
for _, rec := range records {
rec[fieldName] = []interface{}{}
}
if len(parentIDs) == 0 {
continue
}
// Single batched search: WHERE inverse_field IN (all parent IDs)
childRS := env.Model(comodel)
domain := orm.And(orm.Leaf(inverseField, "in", parentIDs))
found, err := childRS.Search(domain, orm.SearchOpts{Limit: defaultO2MFetchLimit})
if err != nil || found.IsEmpty() {
continue
}
// Single batched read
childRecords, err := found.Read(childFields)
if err != nil {
continue
}
// Format child records (M2O fields, dates, nulls)
formatM2OFields(env, comodel, childRecords, subFieldsSpec)
formatDateFields(comodel, childRecords)
normalizeNullFields(comodel, childRecords)
// Group child records by their inverse field (parent ID)
grouped := make(map[int64][]interface{})
for _, cr := range childRecords {
if pid, ok := orm.ToRecordID(cr[inverseField]); ok && pid > 0 {
grouped[pid] = append(grouped[pid], cr)
}
}
// Read child records
childRecords, err := found.Read(childFields)
if err != nil {
rec[fieldName] = []interface{}{}
continue
// Assign grouped children to each parent record
for _, rec := range records {
parentID, ok := rec["id"].(int64)
if !ok {
if pid, ok := rec["id"].(int32); ok {
parentID = int64(pid)
}
}
// Format child records (M2O fields, dates, nulls)
formatM2OFields(env, comodel, childRecords, subFieldsSpec)
formatDateFields(comodel, childRecords)
normalizeNullFields(comodel, childRecords)
// Convert to []interface{} for JSON
lines := make([]interface{}, len(childRecords))
for i, cr := range childRecords {
lines[i] = cr
if children, ok := grouped[parentID]; ok {
rec[fieldName] = children
}
rec[fieldName] = lines
}
}
}