Code reuse: - formatRecordsForWeb() consolidates 4-call formatting sequence (was duplicated in handleWebSearchRead + handleWebRead) - ToRecordID() public alias for cross-package ID extraction Performance: - M2O NameGet now batched: collect all FK IDs per field, single NameGet per comodel instead of per-record (N+1 → 1) Quality: - normalizeNullFields: 30-line switch (all returning false) → 5 lines - domain.go: remove unused _ = subParams assignment - Net -14 lines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
288 lines
6.9 KiB
Go
288 lines
6.9 KiB
Go
package orm
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// ProcessO2MCommands processes One2many field commands after a Create/Write.
|
|
// Mirrors: odoo/orm/fields_relational.py One2many write logic
|
|
//
|
|
// Commands:
|
|
//
|
|
// CmdCreate(vals) → Create child record with inverse_field = parentID
|
|
// CmdUpdate(id, vals) → Update child record
|
|
// CmdDelete(id) → Delete child record
|
|
func ProcessO2MCommands(env *Environment, f *Field, parentID int64, cmds []Command) error {
|
|
if f.Comodel == "" || f.InverseField == "" {
|
|
return fmt.Errorf("orm: O2M field %q missing comodel or inverse_field", f.Name)
|
|
}
|
|
|
|
comodelRS := env.Model(f.Comodel)
|
|
|
|
for _, cmd := range cmds {
|
|
switch cmd.Operation {
|
|
case CommandCreate:
|
|
vals := cmd.Values
|
|
if vals == nil {
|
|
vals = make(Values)
|
|
}
|
|
vals[f.InverseField] = parentID
|
|
if _, err := comodelRS.Create(vals); err != nil {
|
|
return fmt.Errorf("orm: O2M create on %s: %w", f.Comodel, err)
|
|
}
|
|
|
|
case CommandUpdate:
|
|
if cmd.ID <= 0 {
|
|
continue
|
|
}
|
|
if err := comodelRS.Browse(cmd.ID).Write(cmd.Values); err != nil {
|
|
return fmt.Errorf("orm: O2M update %s(%d): %w", f.Comodel, cmd.ID, err)
|
|
}
|
|
|
|
case CommandDelete:
|
|
if cmd.ID <= 0 {
|
|
continue
|
|
}
|
|
if err := comodelRS.Browse(cmd.ID).Unlink(); err != nil {
|
|
return fmt.Errorf("orm: O2M delete %s(%d): %w", f.Comodel, cmd.ID, err)
|
|
}
|
|
|
|
case CommandClear:
|
|
// Delete all children linked to this parent
|
|
children, err := comodelRS.Search(And(Leaf(f.InverseField, "=", parentID)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := children.Unlink(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ProcessM2MCommands processes Many2many field commands after a Create/Write.
|
|
// Mirrors: odoo/orm/fields_relational.py Many2many write logic
|
|
//
|
|
// Commands:
|
|
//
|
|
// CmdLink(id) → Add link in junction table
|
|
// CmdUnlink(id) → Remove link from junction table
|
|
// CmdClear() → Remove all links
|
|
// CmdSet(ids) → Replace all links
|
|
// CmdCreate(vals) → Create comodel record then link it
|
|
func ProcessM2MCommands(env *Environment, f *Field, parentID int64, cmds []Command) error {
|
|
jt := f.JunctionTable()
|
|
col1 := f.JunctionCol1()
|
|
col2 := f.JunctionCol2()
|
|
|
|
if jt == "" || col1 == "" || col2 == "" {
|
|
return fmt.Errorf("orm: M2M field %q: cannot determine junction table", f.Name)
|
|
}
|
|
|
|
for _, cmd := range cmds {
|
|
switch cmd.Operation {
|
|
case CommandLink:
|
|
_, err := env.tx.Exec(env.ctx, fmt.Sprintf(
|
|
`INSERT INTO %q (%q, %q) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
jt, col1, col2,
|
|
), parentID, cmd.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("orm: M2M link %s: %w", f.Name, err)
|
|
}
|
|
|
|
case CommandUnlink:
|
|
_, err := env.tx.Exec(env.ctx, fmt.Sprintf(
|
|
`DELETE FROM %q WHERE %q = $1 AND %q = $2`,
|
|
jt, col1, col2,
|
|
), parentID, cmd.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("orm: M2M unlink %s: %w", f.Name, err)
|
|
}
|
|
|
|
case CommandClear:
|
|
_, err := env.tx.Exec(env.ctx, fmt.Sprintf(
|
|
`DELETE FROM %q WHERE %q = $1`,
|
|
jt, col1,
|
|
), parentID)
|
|
if err != nil {
|
|
return fmt.Errorf("orm: M2M clear %s: %w", f.Name, err)
|
|
}
|
|
|
|
case CommandSet:
|
|
// Clear then link all
|
|
if _, err := env.tx.Exec(env.ctx, fmt.Sprintf(
|
|
`DELETE FROM %q WHERE %q = $1`,
|
|
jt, col1,
|
|
), parentID); err != nil {
|
|
return err
|
|
}
|
|
for _, targetID := range cmd.IDs {
|
|
if _, err := env.tx.Exec(env.ctx, fmt.Sprintf(
|
|
`INSERT INTO %q (%q, %q) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
jt, col1, col2,
|
|
), parentID, targetID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
case CommandCreate:
|
|
// Create comodel record then link
|
|
comodelRS := env.Model(f.Comodel)
|
|
created, err := comodelRS.Create(cmd.Values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := env.tx.Exec(env.ctx, fmt.Sprintf(
|
|
`INSERT INTO %q (%q, %q) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
jt, col1, col2,
|
|
), parentID, created.ID()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReadM2MField reads the linked IDs for a Many2many field.
|
|
func ReadM2MField(env *Environment, f *Field, parentIDs []int64) (map[int64][]int64, error) {
|
|
jt := f.JunctionTable()
|
|
col1 := f.JunctionCol1()
|
|
col2 := f.JunctionCol2()
|
|
|
|
if jt == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
placeholders := make([]string, len(parentIDs))
|
|
args := make([]interface{}, len(parentIDs))
|
|
for i, id := range parentIDs {
|
|
args[i] = id
|
|
placeholders[i] = fmt.Sprintf("$%d", i+1)
|
|
}
|
|
|
|
query := fmt.Sprintf(
|
|
`SELECT %q, %q FROM %q WHERE %q IN (%s)`,
|
|
col1, col2, jt, col1, strings.Join(placeholders, ", "),
|
|
)
|
|
|
|
rows, err := env.tx.Query(env.ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
result := make(map[int64][]int64)
|
|
for rows.Next() {
|
|
var pid, tid int64
|
|
if err := rows.Scan(&pid, &tid); err != nil {
|
|
return nil, err
|
|
}
|
|
result[pid] = append(result[pid], tid)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// ParseCommands converts a JSON value to a slice of Commands.
|
|
// JSON format: [[0, 0, {vals}], [4, id, 0], [6, 0, [ids]], ...]
|
|
func ParseCommands(raw interface{}) ([]Command, bool) {
|
|
list, ok := raw.([]interface{})
|
|
if !ok || len(list) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
// Check if this looks like commands (list of lists)
|
|
first, ok := list[0].([]interface{})
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
if len(first) < 2 {
|
|
return nil, false
|
|
}
|
|
|
|
var cmds []Command
|
|
for _, item := range list {
|
|
tuple, ok := item.([]interface{})
|
|
if !ok || len(tuple) < 2 {
|
|
continue
|
|
}
|
|
|
|
op, ok := toInt64(tuple[0])
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
switch CommandOp(op) {
|
|
case CommandCreate: // [0, _, {vals}]
|
|
vals := make(Values)
|
|
if len(tuple) > 2 {
|
|
if m, ok := tuple[2].(map[string]interface{}); ok {
|
|
vals = m
|
|
}
|
|
}
|
|
cmds = append(cmds, CmdCreate(vals))
|
|
|
|
case CommandUpdate: // [1, id, {vals}]
|
|
id, _ := toInt64(tuple[1])
|
|
vals := make(Values)
|
|
if len(tuple) > 2 {
|
|
if m, ok := tuple[2].(map[string]interface{}); ok {
|
|
vals = m
|
|
}
|
|
}
|
|
cmds = append(cmds, CmdUpdate(id, vals))
|
|
|
|
case CommandDelete: // [2, id, _]
|
|
id, _ := toInt64(tuple[1])
|
|
cmds = append(cmds, CmdDelete(id))
|
|
|
|
case CommandUnlink: // [3, id, _]
|
|
id, _ := toInt64(tuple[1])
|
|
cmds = append(cmds, CmdUnlink(id))
|
|
|
|
case CommandLink: // [4, id, _]
|
|
id, _ := toInt64(tuple[1])
|
|
cmds = append(cmds, CmdLink(id))
|
|
|
|
case CommandClear: // [5, _, _]
|
|
cmds = append(cmds, CmdClear())
|
|
|
|
case CommandSet: // [6, _, [ids]]
|
|
var ids []int64
|
|
if len(tuple) > 2 {
|
|
if arr, ok := tuple[2].([]interface{}); ok {
|
|
for _, v := range arr {
|
|
if id, ok := toInt64(v); ok {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
cmds = append(cmds, CmdSet(ids))
|
|
}
|
|
}
|
|
|
|
if len(cmds) == 0 {
|
|
return nil, false
|
|
}
|
|
return cmds, true
|
|
}
|
|
|
|
// ToRecordID extracts an int64 ID from various numeric types. Public alias.
|
|
func ToRecordID(v interface{}) (int64, bool) { return toInt64(v) }
|
|
|
|
func toInt64(v interface{}) (int64, bool) {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return int64(n), true
|
|
case int64:
|
|
return n, true
|
|
case int32:
|
|
return int64(n), true
|
|
case int:
|
|
return int64(n), true
|
|
}
|
|
return 0, false
|
|
}
|