// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // sqlglobals.go — Process-wide FiveSql2 counters that genuinely need // cross-connection visibility, exposed to PRG via HB_FUNCs that wrap // atomic / mutex primitives on the Go side. The PRG STATICs they // replace (s_nSchemaVer, s_nRCJSeq, s_hAutoInc in TSqlExecutor.prg) // were Go package vars under gengo; multi-pgserver-connection use // raced on them and produced wrong cache invalidation, duplicate // temp aliases, and "concurrent map writes" panics on the AUTOINC // map's lazy first-init path. // // Wrapping these in Go-level synchronization is cheaper than the // alternative (move to TSqlSession + thread oSession through every // DDL caller in TSqlDDL.prg + IDENTITY handler in TSqlExecutor.prg) // AND gives the right semantic — a CREATE TABLE on connection A // MUST invalidate connection B's plan cache, an IDENTITY column // MUST hand out monotonic values across all writers. package hbrtl import ( "strings" "sync" "sync/atomic" "five/hbrt" ) // --- Schema version counter --- // // Every DDL statement (CREATE / ALTER / DROP TABLE / INDEX / VIEW) // calls SQL_BUMP_SCHEMA_VER. Plan-cache + DML-pcode-cache keys embed // the current value as a prefix, so a bump invalidates every cached // entry across every connection in one atomic write. var sqlSchemaVer uint64 // --- RCJ sequence counter --- // // Recursive Common Join helper temp-alias counter. Each // `RCJ_NNN` alias needs cross-connection uniqueness because two // concurrent queries materialising different CTEs would otherwise // collide on RCJ_1. Mod 100000 to keep aliases short. var sqlRCJSeq uint64 // --- IDENTITY column registry --- // // Maps tableName (upper) → list of IDENTITY field names (upper). // Populated by CREATE TABLE; read by INSERT to compute next value. // Genuinely process-global: two connections inserting into the // same table must see the same IDENTITY-field set. var ( sqlAutoIncMu sync.RWMutex sqlAutoInc = map[string][]string{} ) func init() { hbrt.HB_FUNC("SQLSCHEMAVER", hbSqlSchemaVer) hbrt.HB_FUNC("SQLBUMPSCHEMAVER", hbSqlBumpSchemaVer) hbrt.HB_FUNC("SQLNEXTRCJSEQ", hbSqlNextRCJSeq) hbrt.HB_FUNC("SQLSETAUTOINC", hbSqlSetAutoInc) hbrt.HB_FUNC("SQLGETAUTOINCFIELDS", hbSqlGetAutoIncFields) } func hbSqlSchemaVer(ctx *hbrt.HBContext) { ctx.RetNI(int(atomic.LoadUint64(&sqlSchemaVer))) } func hbSqlBumpSchemaVer(ctx *hbrt.HBContext) { ctx.RetNI(int(atomic.AddUint64(&sqlSchemaVer, 1))) } // hbSqlNextRCJSeq returns the next (atomic) RCJ sequence value // modulo 100000, matching the previous PRG `s_nRCJSeq := (s_nRCJSeq + 1) % 100000` // behaviour. Atomic increment first, then mod for the user-facing value. func hbSqlNextRCJSeq(ctx *hbrt.HBContext) { n := atomic.AddUint64(&sqlRCJSeq, 1) ctx.RetNI(int(n % 100000)) } // hbSqlSetAutoInc registers a field as IDENTITY for a table. // Replaces TSqlExecutor.prg's SqlSetAutoInc which lazily init'd // a STATIC hash → "concurrent map writes" panic under two // concurrent CREATE TABLE calls. func hbSqlSetAutoInc(ctx *hbrt.HBContext) { if ctx.PCount() < 2 || !ctx.IsChar(1) || !ctx.IsChar(2) { ctx.RetNil() return } key := strings.ToUpper(ctx.ParC(1)) field := strings.ToUpper(ctx.ParC(2)) sqlAutoIncMu.Lock() defer sqlAutoIncMu.Unlock() sqlAutoInc[key] = append(sqlAutoInc[key], field) ctx.RetNil() } // hbSqlGetAutoIncFields returns the list of IDENTITY field names // for a table, or an empty array if none. PRG signature returns // an Array of strings. func hbSqlGetAutoIncFields(ctx *hbrt.HBContext) { if ctx.PCount() < 1 || !ctx.IsChar(1) { arr := hbrt.MakeArrayFrom(nil) ctx.RetVal(arr) return } key := strings.ToUpper(ctx.ParC(1)) sqlAutoIncMu.RLock() fields := sqlAutoInc[key] sqlAutoIncMu.RUnlock() items := make([]hbrt.Value, len(fields)) for i, f := range fields { items[i] = hbrt.MakeString(f) } ctx.RetVal(hbrt.MakeArrayFrom(items)) }