// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // WorkArea manager — goroutine-local (no locks needed). // Harbour: HB_STACKRDD in hbstack.h — per-thread workarea list. // // Each Thread owns its own WorkAreaManager. No cross-goroutine sharing. // This eliminates Harbour's global workarea table threading issues. package hbrdd import ( "five/hbrt" "fmt" "strings" ) // WorkAreaManager manages open work areas for a single goroutine. // Harbour: waList[], waNums[], pCurrArea in HB_STACKRDD. type WorkAreaManager struct { areas map[uint16]Area // area number → Area aliases map[string]uint16 // alias name (UPPER) → area number current uint16 // currently selected area number nextNum uint16 // next available area number } // NewWorkAreaManager creates a new goroutine-local workarea manager. func NewWorkAreaManager() *WorkAreaManager { return &WorkAreaManager{ areas: make(map[uint16]Area), aliases: make(map[string]uint16), nextNum: 1, // Harbour: area numbers start at 1 } } // Open opens a table and registers it with an alias. // Returns the assigned area number. func (wm *WorkAreaManager) Open(driverName, path, alias string, shared, readOnly bool) (uint16, error) { drv, err := GetDriver(driverName) if err != nil { return 0, err } if alias == "" { // Default alias: filename without extension alias = extractBaseName(path) } alias = strings.ToUpper(alias) // Check duplicate alias if _, exists := wm.aliases[alias]; exists { return 0, fmt.Errorf("alias already in use: %s", alias) } area, err := drv.Open(OpenParams{ Path: path, Alias: alias, Shared: shared, ReadOnly: readOnly, }) if err != nil { return 0, err } // Store alias in the area so Alias() returns it area.SetAlias(alias) // Use the pre-selected area number if available and empty num := wm.current if num == 0 || wm.areas[num] != nil { num = wm.nextNum } if num >= wm.nextNum { wm.nextNum = num + 1 } wm.areas[num] = area wm.aliases[alias] = num wm.current = num return num, nil } // Close closes the current work area. func (wm *WorkAreaManager) Close() error { if wm.current == 0 { return nil } area, ok := wm.areas[wm.current] if !ok { return nil } // Remove alias for alias, num := range wm.aliases { if num == wm.current { delete(wm.aliases, alias) break } } err := area.Close() delete(wm.areas, wm.current) wm.current = 0 return err } // Select switches to a work area by alias name or number. // Harbour: SELECT command. func (wm *WorkAreaManager) Select(aliasOrNum interface{}) error { switch v := aliasOrNum.(type) { case string: // Try as alias first num, ok := wm.aliases[strings.ToUpper(v)] if ok { wm.current = num return nil } // Try as numeric string ("1", "2", etc.) if n := parseAreaNum(v); n > 0 { wm.current = n return nil } // Select 0 = select unused area if v == "0" || v == "" { wm.current = wm.nextNum return nil } return fmt.Errorf("alias not found: %s", v) case int: // Harbour: dbSelectArea(n) always switches, even to empty areas wm.current = uint16(v) case uint16: wm.current = v default: return fmt.Errorf("invalid area selector: %T", aliasOrNum) } return nil } // Current returns the currently selected work area, or nil. func (wm *WorkAreaManager) Current() Area { if wm.current == 0 { return nil } return wm.areas[wm.current] } // CurrentNum returns the current work area number. func (wm *WorkAreaManager) CurrentNum() uint16 { return wm.current } // ByAlias returns a work area by alias name. func (wm *WorkAreaManager) ByAlias(alias string) Area { num, ok := wm.aliases[strings.ToUpper(alias)] if !ok { return nil } return wm.areas[num] } // FindByAlias returns the area number for an alias (0 if not found). func (wm *WorkAreaManager) FindByAlias(alias string) uint16 { num, ok := wm.aliases[strings.ToUpper(alias)] if !ok { return 0 } return num } // SelectByAlias switches to a work area by alias name. Returns true // when the alias resolved, false when nothing matched. Callers that // intend to evaluate an expression in the named workarea (alias // context: `FOO->(...)`) must check the return value — a missed // alias used to silently leave the original WA selected, so a // subsequent DbCloseArea/RecCount/etc. ran against the *current* // workarea instead of the named one. That's the exact data-loss // foot-gun that bit `CLOSE bad_alias`. WASaveAndSelectAlias now // switches to the no-area sentinel (0) on miss so the inner // expression sees Current() == nil and short-circuits cleanly. func (wm *WorkAreaManager) SelectByAlias(alias string) bool { num, ok := wm.aliases[strings.ToUpper(alias)] if ok { wm.current = num } return ok } // SelectByNum switches to a work area by number, silently allowing unused areas. // Used by workarea context expressions: (nArea)->(expr) // Unlike Select(), this does NOT error on unused area numbers — // Harbour allows selecting any area 1-250 even if nothing is open there. func (wm *WorkAreaManager) SelectByNum(num uint16) { wm.current = num } // AreaAt returns the Area at a given area number (nil if not in use). func (wm *WorkAreaManager) AreaAt(num uint16) Area { return wm.areas[num] } // parseAreaNum tries to parse a string as a work area number. func parseAreaNum(s string) uint16 { s = strings.TrimSpace(s) n := 0 for _, c := range s { if c >= '0' && c <= '9' { n = n*10 + int(c-'0') } else { return 0 } } return uint16(n) } // EnumerateAreas invokes fn once per open workarea with (nWA, alias, area). // Snapshot of the slot→area map is taken first so fn can safely manipulate // workareas without mutating the loop. Used by the diagnostic ErrorLog // writer to dump every open table's state. func (wm *WorkAreaManager) EnumerateAreas(fn func(nWA uint16, alias string, area Area)) { type slot struct { num uint16 alias string area Area } snapshot := make([]slot, 0, len(wm.areas)) for num, area := range wm.areas { snapshot = append(snapshot, slot{num, area.Alias(), area}) } for _, s := range snapshot { fn(s.num, s.alias, s.area) } } // CloseAll closes all open work areas. func (wm *WorkAreaManager) CloseAll() { for num, area := range wm.areas { area.Close() delete(wm.areas, num) } wm.aliases = make(map[string]uint16) wm.current = 0 } // GetAliasField returns a field value from a named alias. // Used by alias->field syntax (e.g., CUSTOMERS->NAME, FIELD->AGE). func (wm *WorkAreaManager) GetAliasField(alias, field string) hbrt.Value { var area Area // FIELD-> is a special alias meaning "current workarea" if strings.EqualFold(alias, "FIELD") || strings.EqualFold(alias, "_FIELD") { area = wm.Current() } else { area = wm.ByAlias(alias) } if area == nil { return hbrt.MakeNil() } // Find field by name for i := 0; i < area.FieldCount(); i++ { fi := area.GetFieldInfo(i) if strings.EqualFold(fi.Name, field) { val, _ := area.GetValue(i) return val } } return hbrt.MakeNil() } // SetAliasField sets a field value by alias->field syntax. func (wm *WorkAreaManager) SetAliasField(alias, field string, val hbrt.Value) { var area Area if strings.EqualFold(alias, "FIELD") || strings.EqualFold(alias, "_FIELD") { area = wm.Current() } else { area = wm.ByAlias(alias) } if area == nil { return } for i := 0; i < area.FieldCount(); i++ { fi := area.GetFieldInfo(i) if strings.EqualFold(fi.Name, field) { area.PutValue(i, val) return } } } // --- Helpers --- func extractBaseName(path string) string { // Extract filename without path and extension name := path // Remove path for i := len(name) - 1; i >= 0; i-- { if name[i] == '/' || name[i] == '\\' { name = name[i+1:] break } } // Remove extension for i := len(name) - 1; i >= 0; i-- { if name[i] == '.' { name = name[:i] break } } return name }