package orm import ( "context" "fmt" "strconv" "strings" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // Environment is the central context for all ORM operations. // Mirrors: odoo/orm/environments.py Environment // // Odoo: self.env['res.partner'].search([('name', 'ilike', 'test')]) // Go: env.Model("res.partner").Search(And(Leaf("name", "ilike", "test"))) // // An Environment wraps: // - A database transaction (cursor in Odoo terms) // - The current user ID // - The current company ID // - Context values (lang, tz, etc.) type Environment struct { ctx context.Context pool *pgxpool.Pool tx pgx.Tx uid int64 companyID int64 su bool // sudo mode (bypass access checks) context map[string]interface{} cache *Cache } // EnvConfig configures a new Environment. type EnvConfig struct { Pool *pgxpool.Pool UID int64 CompanyID int64 Context map[string]interface{} } // NewEnvironment creates a new ORM environment. // Mirrors: odoo.api.Environment(cr, uid, context) func NewEnvironment(ctx context.Context, cfg EnvConfig) (*Environment, error) { tx, err := cfg.Pool.Begin(ctx) if err != nil { return nil, fmt.Errorf("orm: begin transaction: %w", err) } envCtx := cfg.Context if envCtx == nil { envCtx = make(map[string]interface{}) } return &Environment{ ctx: ctx, pool: cfg.Pool, tx: tx, uid: cfg.UID, companyID: cfg.CompanyID, context: envCtx, cache: NewCache(), }, nil } // Model returns a Recordset bound to this environment for the given model. // Mirrors: self.env['model.name'] func (env *Environment) Model(name string) *Recordset { m := Registry.MustGet(name) return &Recordset{ env: env, model: m, ids: nil, } } // Ref returns a record by its XML ID (external identifier). // Mirrors: self.env.ref('module.xml_id') func (env *Environment) Ref(xmlID string) (*Recordset, error) { // Try direct integer ID (for programmatic use) — reject since model is unknown if id, err := strconv.ParseInt(xmlID, 10, 64); err == nil { return nil, fmt.Errorf("orm: ref requires module.name format, not bare ID %d", id) } // Query ir_model_data for the external ID var resModel string var resID int64 parts := strings.SplitN(xmlID, ".", 2) if len(parts) != 2 { return nil, fmt.Errorf("orm: ref %q must be module.name format", xmlID) } err := env.tx.QueryRow(env.ctx, `SELECT model, res_id FROM ir_model_data WHERE module = $1 AND name = $2 LIMIT 1`, parts[0], parts[1], ).Scan(&resModel, &resID) if err != nil { return nil, fmt.Errorf("orm: ref %q not found: %w", xmlID, err) } return env.Model(resModel).Browse(resID), nil } // UID returns the current user ID. func (env *Environment) UID() int64 { return env.uid } // CompanyID returns the current company ID. func (env *Environment) CompanyID() int64 { return env.companyID } // IsSuperuser returns true if this environment bypasses access checks. func (env *Environment) IsSuperuser() bool { return env.su } // Context returns the environment context (Odoo-style key-value context). func (env *Environment) Context() map[string]interface{} { return env.context } // Ctx returns the Go context.Context for database operations. func (env *Environment) Ctx() context.Context { return env.ctx } // Lang returns the language from context. func (env *Environment) Lang() string { if lang, ok := env.context["lang"].(string); ok { return lang } return "en_US" } // Tx returns the underlying database transaction. func (env *Environment) Tx() pgx.Tx { return env.tx } // Sudo returns a new environment with superuser privileges. // Mirrors: self.env['model'].sudo() func (env *Environment) Sudo() *Environment { return &Environment{ ctx: env.ctx, pool: env.pool, tx: env.tx, uid: env.uid, companyID: env.companyID, su: true, context: env.context, cache: env.cache, } } // WithUser returns a new environment for a different user. // Mirrors: self.with_user(user_id) func (env *Environment) WithUser(uid int64) *Environment { return &Environment{ ctx: env.ctx, pool: env.pool, tx: env.tx, uid: uid, companyID: env.companyID, su: false, context: env.context, cache: env.cache, } } // WithCompany returns a new environment for a different company. // Mirrors: self.with_company(company_id) func (env *Environment) WithCompany(companyID int64) *Environment { return &Environment{ ctx: env.ctx, pool: env.pool, tx: env.tx, uid: env.uid, companyID: companyID, su: env.su, context: env.context, cache: env.cache, } } // WithContext returns a new environment with additional context values. // Mirrors: self.with_context(key=value) func (env *Environment) WithContext(extra map[string]interface{}) *Environment { merged := make(map[string]interface{}, len(env.context)+len(extra)) for k, v := range env.context { merged[k] = v } for k, v := range extra { merged[k] = v } return &Environment{ ctx: env.ctx, pool: env.pool, tx: env.tx, uid: env.uid, companyID: env.companyID, su: env.su, context: merged, cache: env.cache, } } // Commit commits the database transaction. func (env *Environment) Commit() error { return env.tx.Commit(env.ctx) } // Rollback rolls back the database transaction. func (env *Environment) Rollback() error { return env.tx.Rollback(env.ctx) } // Close rolls back uncommitted changes and releases the connection. func (env *Environment) Close() error { return env.tx.Rollback(env.ctx) } // --- Cache --- // Cache is a per-environment record cache. // Mirrors: odoo/orm/environments.py Transaction cache type Cache struct { // model_name → record_id → field_name → value data map[string]map[int64]map[string]Value } // NewCache creates an empty cache. func NewCache() *Cache { return &Cache{ data: make(map[string]map[int64]map[string]Value), } } // Get retrieves a cached value. func (c *Cache) Get(model string, id int64, field string) (Value, bool) { records, ok := c.data[model] if !ok { return nil, false } fields, ok := records[id] if !ok { return nil, false } val, ok := fields[field] return val, ok } // Set stores a value in the cache. func (c *Cache) Set(model string, id int64, field string, value Value) { records, ok := c.data[model] if !ok { records = make(map[int64]map[string]Value) c.data[model] = records } fields, ok := records[id] if !ok { fields = make(map[string]Value) records[id] = fields } fields[field] = value } // Invalidate removes all cached values for a model. func (c *Cache) Invalidate(model string) { delete(c.data, model) } // InvalidateRecord removes cached values for a specific record. func (c *Cache) InvalidateRecord(model string, id int64) { if records, ok := c.data[model]; ok { delete(records, id) } }