refactor(FiveSql2): per-session state — TSqlSession isolates txn + plan cache
Foundation for the upcoming PostgreSQL-wire server. The SQL engine
previously held transaction state and the plan cache in module-level
STATICs:
TSqlTxn.prg:16-18
STATIC s_aTxnLog := {}
STATIC s_lInTxn := .F.
STATIC s_hSavepoints := NIL
TFiveSQL.prg:37
STATIC s_hPlanCache := { => }
gengo emits PRG STATIC as Go *package* variables, so two clients
sharing one process serialised through a single transaction log:
client A's `BEGIN; INSERT;` followed by client B's `ROLLBACK`
would silently undo A's insert. Acceptable for embedded single-
caller use; show-stopper for a multi-connection daemon.
Moved each of those into instance fields on a new TSqlSession class.
Every executor instance now carries an oSession pointer that's
inherited by nested subquery executors. A process-default session
is lazy-initialised by SqlDefaultSession() so embedded
`five_SQL(cSQL)` callers (today's only consumer) keep working
unchanged.
Changes
-------
* `_FiveSql2/src/TSqlSession.prg` (new) — class holding the four
ex-STATICs plus seats for auth/ACL state and a list of workareas
the session opened (used later for disconnect cleanup). Module-
level `SqlDefaultSession()` lazily creates one process-wide
default for embedded callers.
* `_FiveSql2/src/TSqlTxn.prg` — added `oSession` DATA; New() takes
an optional oSession and falls back to the default. All STATIC
reads/writes rewritten as `::oSession:aTxnLog`,
`::oSession:lInTxn`, etc.
* `_FiveSql2/src/TFiveSQL.prg` — added `oSession` DATA; New() takes
an optional second arg. Plan-cache reads/writes route through
`::oSession:hPlanCache`. SQL_PLAN_CACHE_MAX now caps each session
independently (a chatty client only flushes its own cache, not
the shared one).
* `_FiveSql2/src/TSqlExecutor.prg` — added `oSession` DATA; New()
takes an optional third arg; `::oTxn := TSqlTxn():New(::oSession)`
propagates the binding. Every in-class `TSqlExecutor():New(...)`
call site for subqueries / UNION / IN-list materialisation /
EXISTS / lifted subqueries now passes `::oSession` through, so a
child executor inherits the parent's session. Standalone helper
functions (SqlEvalExprNode / SqlFetchRowArr / SqlJoinRecurse /
SqlMaterializeSubquery) intentionally fall back to the default
session — they don't BEGIN/COMMIT and the plan cache is keyed by
schema-version anyway.
* `_FiveSql2/src/FiveSqlCls.prg` — `five_SQL()` gains an optional
fourth arg `oSession`. Existing 1-/2-/3-arg callers keep working;
pgserver will create one TSqlSession per connection and pass it.
Verification
------------
Per-session isolation pinned by a fresh PRG-level regression
(reproducer not committed yet — will land with pgserver test
suite). The scenario:
oSessA := TSqlSession():New()
oSessB := TSqlSession():New()
oSqlA := TFiveSQL():New(NIL, oSessA)
oSqlB := TFiveSQL():New(NIL, oSessB)
oSqlA:Execute("BEGIN") -- A in txn
oSqlB:Execute("BEGIN") -- B in txn, A unaffected
oSqlB:Execute("INSERT ... VALUES(2,'b-row')")
oSqlB:Execute("COMMIT") -- B committed, A still in txn
oSqlA:Execute("ROLLBACK") -- A's empty rollback, B's row survives
All four assertions pass post-refactor, would fail pre-refactor
because both sessions wrote the same `s_aTxnLog`.
All six release gates green:
go test ./... ✓
FiveSql2 SQL:1999 43/43 ✓
Harbour compat 56/56 ✓
std.ch 17/17 ✓
FRB 7/7 ✓
examples 65/71 ✓ (unchanged baseline)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
*
|
||||
* Uses TSqlParser2 (Pratt parser) exclusively.
|
||||
*
|
||||
* The plan cache (formerly STATIC s_hPlanCache) now lives on
|
||||
* TSqlSession so concurrent connections don't share parsed ASTs.
|
||||
* Embedded callers that didn't pass an oSession transparently use
|
||||
* the process-default session via SqlDefaultSession().
|
||||
*
|
||||
* FiveSql2 — SQL Engine for Harbour DBF/NTX
|
||||
*
|
||||
* Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun)
|
||||
@@ -14,27 +19,12 @@
|
||||
#include "hbclass.ch"
|
||||
#include "FiveSqlDef.ch"
|
||||
|
||||
/* Plan cache: cSQL → parsed hQuery.
|
||||
/* Plan cache size cap, per-session. Wipe wholesale on overflow.
|
||||
*
|
||||
* The FiveSql2 parser runs lex + Pratt-style AST build per call; for
|
||||
* repeated identical SQL (typical in report / loop / benchmark workloads)
|
||||
* this is pure overhead. We cache the pristine parse result keyed by
|
||||
* the raw SQL text and hand every subsequent call a deep clone via
|
||||
* HbDeepClone so in-place mutations (SqlFoldConst, aTables rewriting)
|
||||
* during Run() never corrupt the cached tree.
|
||||
*
|
||||
* Cached entries live until process exit; distinct SQL text count is
|
||||
* bounded by the caller's template set in well-behaved callers, but
|
||||
* a long-running server with diverse dynamic SQL (or one that bypasses
|
||||
* the `?` placeholder convention and bakes literals into every query)
|
||||
* can grow this hash without bound. SQL_PLAN_CACHE_MAX caps the entry
|
||||
* count; on overflow we wipe the whole cache. Coarser than LRU but
|
||||
* Five hashes have no insertion-order guarantee and the per-query
|
||||
* bookkeeping for true LRU would dominate the parse cost we're
|
||||
* trying to amortise. Reset cost is one extra parse per template
|
||||
* already evicted, accepted in exchange for bounded memory. */
|
||||
* Each session's hPlanCache is independent — a chatty client running
|
||||
* thousands of unique SQL templates only flushes its own cache, not
|
||||
* a global one shared with quiet siblings. */
|
||||
#define SQL_PLAN_CACHE_MAX 1000
|
||||
STATIC s_hPlanCache := { => }
|
||||
|
||||
CLASS TFiveSQL
|
||||
|
||||
@@ -42,19 +32,25 @@ CLASS TFiveSQL
|
||||
DATA oParser
|
||||
DATA oExec
|
||||
DATA aParams INIT {}
|
||||
DATA oSession /* per-connection state container */
|
||||
|
||||
METHOD New( aParams ) CONSTRUCTOR
|
||||
METHOD New( aParams, oSession ) CONSTRUCTOR
|
||||
METHOD Execute( cSQL, bBlock )
|
||||
METHOD ExecuteWith( cSQL, aParams )
|
||||
|
||||
ENDCLASS
|
||||
|
||||
|
||||
METHOD New( aParams ) CLASS TFiveSQL
|
||||
METHOD New( aParams, oSession ) CLASS TFiveSQL
|
||||
|
||||
IF aParams != NIL
|
||||
::aParams := aParams
|
||||
ENDIF
|
||||
IF oSession == NIL
|
||||
::oSession := SqlDefaultSession()
|
||||
ELSE
|
||||
::oSession := oSession
|
||||
ENDIF
|
||||
|
||||
RETURN SELF
|
||||
|
||||
@@ -63,6 +59,7 @@ METHOD Execute( cSQL, bBlock ) CLASS TFiveSQL
|
||||
|
||||
LOCAL aTokens, hQuery, aResult
|
||||
LOCAL aLex, cKey, aParams, cVerPrefix
|
||||
LOCAL hPlanCache := ::oSession:hPlanCache
|
||||
|
||||
/* Schema-version prefix: DDL (CREATE/ALTER/DROP) bumps SqlSchemaVer()
|
||||
* so any plan that resolved columns or indexes against the pre-DDL
|
||||
@@ -82,28 +79,29 @@ METHOD Execute( cSQL, bBlock ) CLASS TFiveSQL
|
||||
cKey := cVerPrefix + aLex[ 2 ]
|
||||
aParams := aLex[ 3 ]
|
||||
|
||||
IF hb_HHasKey( s_hPlanCache, cKey )
|
||||
hQuery := HbDeepClone( s_hPlanCache[ cKey ] )
|
||||
IF hb_HHasKey( hPlanCache, cKey )
|
||||
hQuery := HbDeepClone( hPlanCache[ cKey ] )
|
||||
ELSE
|
||||
::oParser := TSqlParser2():New( aTokens, aParams )
|
||||
hQuery := ::oParser:Parse()
|
||||
IF hQuery == NIL
|
||||
RETURN { { "__error__" }, { { SQL_ERR_SYNTAX, "Failed to parse SQL", cSQL } } }
|
||||
ENDIF
|
||||
IF Len( s_hPlanCache ) >= SQL_PLAN_CACHE_MAX
|
||||
s_hPlanCache := { => }
|
||||
IF Len( hPlanCache ) >= SQL_PLAN_CACHE_MAX
|
||||
::oSession:hPlanCache := { => }
|
||||
hPlanCache := ::oSession:hPlanCache
|
||||
SqlDmlPcodeCacheReset()
|
||||
ENDIF
|
||||
s_hPlanCache[ cKey ] := HbDeepClone( hQuery )
|
||||
hPlanCache[ cKey ] := HbDeepClone( hQuery )
|
||||
ENDIF
|
||||
|
||||
::oExec := TSqlExecutor():New( hQuery, aParams )
|
||||
::oExec := TSqlExecutor():New( hQuery, aParams, ::oSession )
|
||||
::oExec:cCacheKey := cKey
|
||||
ELSE
|
||||
/* Caller supplied explicit params — cache by raw SQL text. */
|
||||
cKey := cVerPrefix + cSQL
|
||||
IF hb_HHasKey( s_hPlanCache, cKey )
|
||||
hQuery := HbDeepClone( s_hPlanCache[ cKey ] )
|
||||
IF hb_HHasKey( hPlanCache, cKey )
|
||||
hQuery := HbDeepClone( hPlanCache[ cKey ] )
|
||||
ELSE
|
||||
aTokens := SqlLexerTokenize( cSQL )
|
||||
::oParser := TSqlParser2():New( aTokens, ::aParams )
|
||||
@@ -111,14 +109,15 @@ METHOD Execute( cSQL, bBlock ) CLASS TFiveSQL
|
||||
IF hQuery == NIL
|
||||
RETURN { { "__error__" }, { { SQL_ERR_SYNTAX, "Failed to parse SQL", cSQL } } }
|
||||
ENDIF
|
||||
IF Len( s_hPlanCache ) >= SQL_PLAN_CACHE_MAX
|
||||
s_hPlanCache := { => }
|
||||
IF Len( hPlanCache ) >= SQL_PLAN_CACHE_MAX
|
||||
::oSession:hPlanCache := { => }
|
||||
hPlanCache := ::oSession:hPlanCache
|
||||
SqlDmlPcodeCacheReset()
|
||||
ENDIF
|
||||
s_hPlanCache[ cKey ] := HbDeepClone( hQuery )
|
||||
hPlanCache[ cKey ] := HbDeepClone( hQuery )
|
||||
ENDIF
|
||||
|
||||
::oExec := TSqlExecutor():New( hQuery, ::aParams )
|
||||
::oExec := TSqlExecutor():New( hQuery, ::aParams, ::oSession )
|
||||
::oExec:cCacheKey := cKey
|
||||
ENDIF
|
||||
|
||||
|
||||
Reference in New Issue
Block a user