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 } 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 }