Continues the multi-session concurrency cleanup. Phase 1 moved
the visible txn + plan-cache state onto TSqlSession; this pass
takes the next batch of "shared by accident" STATICs that
surfaced as Go-level `concurrent map writes` panics under
5-worker pgserver load:
s_aOuterStack — subquery-nesting stack
s_hDmlPcodeCache — DML pcode cache (schema-version keyed)
s_lCteDiskSeen — CTE-materialised-to-DBF flag
Each is now a DATA field on TSqlSession, initialised in New().
TSqlExecutor's 25 access sites (sed-rewritten under inspection)
now route through `::oSession:fieldname`. The standalone
`SqlDmlPcodeCacheReset()` helper keeps a backward-compatible
signature: callers may pass an explicit oSession, otherwise it
falls back to SqlDefaultSession() (preserves embedded-mode
ergonomics).
Remaining STATICs in TSqlExecutor.prg (s_nSchemaVer, s_nRCJSeq,
s_hAutoInc) are cross-session-shared by design — schema-version
bumps must invalidate every peer's plan cache, RCJ alias
sequence needs cross-connection uniqueness, and IDENTITY columns
must hand out monotonically increasing values across all writers
into the same table. Those need atomic / mutex guards rather
than per-session ownership; tracked as a follow-up.
Measured impact on the pgserver stress harness (20 runs each):
3-worker 5-worker
Layer 1+2: 16/20 (80%) 10/20 (50%)
+3a: 16/20 (80%) 10/20 (50%)
+THIS: 18/20 (90%) 16/20 (80%)
The remaining flake comes from s_hAutoInc's lazy map init under
concurrent IDENTITY-table writers and a few interleavings of the
header max-merge path. Both are tractable with the planned
atomic / mutex shims and the multi-area mmap-gen registry; both
deferred to the follow-up commit to keep this diff focused on
the move-to-session pattern.
All six release gates green:
go test ./... ✓
FiveSql2 SQL:1999 43/43 ✓
Harbour compat 56/56 ✓
std.ch 17/17 ✓
FRB 7/7 ✓
pgserver integration 6/6 ✓
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
135 lines
4.1 KiB
Plaintext
135 lines
4.1 KiB
Plaintext
/*
|
|
* TFiveSQL.prg — Main facade class for FiveSql2 engine
|
|
*
|
|
* 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)
|
|
* Email: charleskwonohjun@gmail.com
|
|
*
|
|
* All rights reserved.
|
|
*/
|
|
|
|
#include "hbclass.ch"
|
|
#include "FiveSqlDef.ch"
|
|
|
|
/* Plan cache size cap, per-session. Wipe wholesale on overflow.
|
|
*
|
|
* 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
|
|
|
|
CLASS TFiveSQL
|
|
|
|
DATA oLexer
|
|
DATA oParser
|
|
DATA oExec
|
|
DATA aParams INIT {}
|
|
DATA oSession /* per-connection state container */
|
|
|
|
METHOD New( aParams, oSession ) CONSTRUCTOR
|
|
METHOD Execute( cSQL, bBlock )
|
|
METHOD ExecuteWith( cSQL, aParams )
|
|
|
|
ENDCLASS
|
|
|
|
|
|
METHOD New( aParams, oSession ) CLASS TFiveSQL
|
|
|
|
IF aParams != NIL
|
|
::aParams := aParams
|
|
ENDIF
|
|
IF oSession == NIL
|
|
::oSession := SqlDefaultSession()
|
|
ELSE
|
|
::oSession := oSession
|
|
ENDIF
|
|
|
|
RETURN SELF
|
|
|
|
|
|
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
|
|
* schema misses the cache on the next call and gets re-parsed /
|
|
* re-compiled against the current layout. The prefix also flows
|
|
* through to s_hDmlPcodeCache via ::oExec:cCacheKey below. */
|
|
cVerPrefix := hb_NToS( SqlSchemaVer() ) + "|"
|
|
|
|
/* Fast path: no explicit aParams → single Go RTL lex+normalize call
|
|
* (SqlLexAndExtractTemplate). Returns {aTokens, cKey, aParams}; the
|
|
* tokens already have TK_TEXT/TK_NUM replaced with TK_QMARK, so
|
|
* TSqlParser2 sees the template shape and emits ND_PAR references
|
|
* against the extracted aParams. */
|
|
IF Empty( ::aParams )
|
|
aLex := SqlLexAndExtractTemplate( cSQL )
|
|
aTokens := aLex[ 1 ]
|
|
cKey := cVerPrefix + aLex[ 2 ]
|
|
aParams := aLex[ 3 ]
|
|
|
|
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( hPlanCache ) >= SQL_PLAN_CACHE_MAX
|
|
::oSession:hPlanCache := { => }
|
|
hPlanCache := ::oSession:hPlanCache
|
|
SqlDmlPcodeCacheReset( ::oSession )
|
|
ENDIF
|
|
hPlanCache[ cKey ] := HbDeepClone( hQuery )
|
|
ENDIF
|
|
|
|
::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( hPlanCache, cKey )
|
|
hQuery := HbDeepClone( hPlanCache[ cKey ] )
|
|
ELSE
|
|
aTokens := SqlLexerTokenize( cSQL )
|
|
::oParser := TSqlParser2():New( aTokens, ::aParams )
|
|
hQuery := ::oParser:Parse()
|
|
IF hQuery == NIL
|
|
RETURN { { "__error__" }, { { SQL_ERR_SYNTAX, "Failed to parse SQL", cSQL } } }
|
|
ENDIF
|
|
IF Len( hPlanCache ) >= SQL_PLAN_CACHE_MAX
|
|
::oSession:hPlanCache := { => }
|
|
hPlanCache := ::oSession:hPlanCache
|
|
SqlDmlPcodeCacheReset( ::oSession )
|
|
ENDIF
|
|
hPlanCache[ cKey ] := HbDeepClone( hQuery )
|
|
ENDIF
|
|
|
|
::oExec := TSqlExecutor():New( hQuery, ::aParams, ::oSession )
|
|
::oExec:cCacheKey := cKey
|
|
ENDIF
|
|
|
|
::oExec:bRowBlock := bBlock
|
|
aResult := ::oExec:Run()
|
|
|
|
RETURN aResult
|
|
|
|
|
|
METHOD ExecuteWith( cSQL, aParams ) CLASS TFiveSQL
|
|
|
|
::aParams := aParams
|
|
|
|
RETURN ::Execute( cSQL )
|