// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Workarea cache for FiveSql2 DML — opt-in persistent workarea slots // keyed by alias. Eliminates per-query dbUseArea + dbCloseArea syscall // overhead for repeated INSERT / UPDATE / DELETE against the same table. // // Semantics: // * Disabled by default. Callers opt in via SqlWACacheEnable(). Tests // and short one-shot scripts can stay on the safe per-query open/ // close behavior; long-running bench loops or servers pay the open // cost once. // * Entries map uppercase alias → workarea number. The PRG side is // responsible for the actual dbUseArea / dbSelectArea — this layer // only stores the handle. // * Invalidation is explicit. CREATE TABLE / DROP TABLE in // TSqlDDL.prg call SqlWACacheInvalidate before any filesystem // operation that would otherwise collide with a still-open handle. // * SqlWACacheCloseAll drops every entry; callers then decide how // to actually close the workareas (dbCloseAll, per-alias close, …). package hbrtl import ( "strings" "sync" "five/hbrt" ) var ( waCacheMu sync.Mutex waCacheEntries = map[string]int{} waCacheEnabled bool ) // SqlWACacheEnable() → NIL // Turns on the workarea cache for this process. Existing opens are not // retroactively registered — the cache populates on next SqlWAOpenCached. func SqlWACacheEnable(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() waCacheMu.Lock() waCacheEnabled = true waCacheMu.Unlock() t.RetNil() } // SqlWACacheDisable() → NIL // Turns the cache off and drops all entries. Workareas themselves // are left in whatever state the caller last put them in — callers // typically follow with dbCloseAll() or per-table close. func SqlWACacheDisable(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() waCacheMu.Lock() waCacheEnabled = false waCacheEntries = map[string]int{} waCacheMu.Unlock() t.RetNil() } // SqlWACacheIsEnabled() → lBool func SqlWACacheIsEnabled(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() waCacheMu.Lock() on := waCacheEnabled waCacheMu.Unlock() t.RetBool(on) } // SqlWACacheGet(cAlias) → nWA | 0 // Lookup a cached workarea number by alias. Returns 0 if disabled or // no entry. PRG side still verifies Used() / Select() before relying // on the number — another process or manual close may have invalidated // the handle between cache hits. func SqlWACacheGet(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() waCacheMu.Lock() on := waCacheEnabled nWA := 0 if on { nWA = waCacheEntries[strings.ToUpper(t.Local(1).AsString())] } waCacheMu.Unlock() t.RetInt(int64(nWA)) } // SqlWACachePut(cAlias, nWA) → NIL // Register (or overwrite) a cache entry. No-op when cache is disabled // so callers can unconditionally call Put after a successful open. func SqlWACachePut(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() alias := strings.ToUpper(t.Local(1).AsString()) nWA := int(t.Local(2).AsNumInt()) waCacheMu.Lock() if waCacheEnabled && nWA > 0 { waCacheEntries[alias] = nWA } waCacheMu.Unlock() t.RetNil() } // SqlWACacheInvalidate(cAlias) → NIL // Drop a single cache entry. Called before CREATE TABLE / DROP TABLE / // FErase so the PRG side can then close and recreate the file without // conflicting with a stale cached open. func SqlWACacheInvalidate(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() alias := strings.ToUpper(t.Local(1).AsString()) waCacheMu.Lock() delete(waCacheEntries, alias) waCacheMu.Unlock() t.RetNil() } // SqlWACacheCloseAll() → aKeys // Empties the cache and returns the list of aliases that were in it. // Callers can iterate and close each corresponding workarea. func SqlWACacheCloseAll(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() waCacheMu.Lock() keys := make([]string, 0, len(waCacheEntries)) for k := range waCacheEntries { keys = append(keys, k) } waCacheEntries = map[string]int{} waCacheMu.Unlock() out := make([]hbrt.Value, len(keys)) for i, k := range keys { out[i] = hbrt.MakeString(k) } t.PushValue(hbrt.MakeArrayFrom(out)) t.RetValue() }