From 93cf5c8bfa616cbbbb69087c18c6717d207b7944 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Thu, 14 May 2026 17:47:00 +0900 Subject: [PATCH] =?UTF-8?q?refactor(FiveSql2):=20per-session=20state=20?= =?UTF-8?q?=E2=80=94=20TSqlSession=20isolates=20txn=20+=20plan=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- _FiveSql2/src/FiveSqlCls.prg | 14 ++++-- _FiveSql2/src/TFiveSQL.prg | 65 +++++++++++++------------ _FiveSql2/src/TSqlExecutor.prg | 34 ++++++++++---- _FiveSql2/src/TSqlSession.prg | 79 +++++++++++++++++++++++++++++++ _FiveSql2/src/TSqlTxn.prg | 86 +++++++++++++++++++--------------- 5 files changed, 194 insertions(+), 84 deletions(-) create mode 100644 _FiveSql2/src/TSqlSession.prg diff --git a/_FiveSql2/src/FiveSqlCls.prg b/_FiveSql2/src/FiveSqlCls.prg index 62b0a4d..9718a08 100644 --- a/_FiveSql2/src/FiveSqlCls.prg +++ b/_FiveSql2/src/FiveSqlCls.prg @@ -12,7 +12,7 @@ #include "FiveSqlDef.ch" /* - * five_SQL( cSQL [, aParams ] [, bBlock ] ) --> aResult | NIL + * five_SQL( cSQL [, aParams ] [, bBlock ] [, oSession ] ) --> aResult | NIL * * Execute a SQL statement against the current DBF workareas. * @@ -30,14 +30,22 @@ * array mode even when a block is supplied, and the block is invoked * once per row after the fact as a compatibility layer. * + * Session parameter: + * When oSession is NIL (the common embedded case), the engine uses + * a lazy process-default session via SqlDefaultSession() so existing + * callers keep working unchanged. The pgserver frontend passes a + * fresh TSqlSession instance per connection so concurrent clients + * don't share transaction logs or plan caches. + * * Accepts both parameter positions so existing callers still work: * five_SQL( cSQL ) * five_SQL( cSQL, aParams ) * five_SQL( cSQL, aParams, bBlock ) * five_SQL( cSQL, NIL, bBlock ) + * five_SQL( cSQL, NIL, NIL, oSession ) */ -FUNCTION five_SQL( cSQL, aParams, bBlock ) +FUNCTION five_SQL( cSQL, aParams, bBlock, oSession ) - LOCAL oSql := TFiveSQL():New( aParams ) + LOCAL oSql := TFiveSQL():New( aParams, oSession ) RETURN oSql:Execute( cSQL, bBlock ) diff --git a/_FiveSql2/src/TFiveSQL.prg b/_FiveSql2/src/TFiveSQL.prg index b017153..84e9ea1 100644 --- a/_FiveSql2/src/TFiveSQL.prg +++ b/_FiveSql2/src/TFiveSQL.prg @@ -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 diff --git a/_FiveSql2/src/TSqlExecutor.prg b/_FiveSql2/src/TSqlExecutor.prg index f7862fb..ea3694a 100644 --- a/_FiveSql2/src/TSqlExecutor.prg +++ b/_FiveSql2/src/TSqlExecutor.prg @@ -83,7 +83,13 @@ CLASS TSqlExecutor DATA hSubCache - METHOD New( hQuery, aParams ) CONSTRUCTOR + /* Session container — carries txn log, plan cache, current user, + * role grants. Nested subquery executors inherit the parent's + * session so a child query's BEGIN/COMMIT operates on the same + * connection-scoped state. */ + DATA oSession + + METHOD New( hQuery, aParams, oSession ) CONSTRUCTOR METHOD Run() METHOD RunImpl() METHOD RunSelect() @@ -134,15 +140,23 @@ CLASS TSqlExecutor ENDCLASS -METHOD New( hQuery, aParams ) CLASS TSqlExecutor +METHOD New( hQuery, aParams, oSession ) CLASS TSqlExecutor ::hQuery := hQuery ::aParams := iif( aParams == NIL, {}, aParams ) + /* Inherit caller's session if provided; otherwise fall back to + * the process-default session so embedded `five_SQL(cSQL)` callers + * and any TSqlExecutor created without the new arg keep working. */ + IF oSession == NIL + ::oSession := SqlDefaultSession() + ELSE + ::oSession := oSession + ENDIF ::oIndex := TSqlIndex():New() ::oAgg := TSqlAgg():New() ::oSort := TSqlSort():New() ::oDDL := TSqlDDL():New() - ::oTxn := TSqlTxn():New() + ::oTxn := TSqlTxn():New( ::oSession ) ::oAlias := TSqlAlias():New() ::nDepth := 0 ::aOpened := {} @@ -2390,7 +2404,7 @@ METHOD RunSelect() CLASS TSqlExecutor * which meant LIMIT clipped the first SELECT before UNION added * the second's rows, producing more rows than intended. */ IF hUnion != NIL - aU := TSqlExecutor():New( hUnion, ::aParams ):Run() + aU := TSqlExecutor():New( hUnion, ::aParams, ::oSession ):Run() /* SQL standard: set operations require the same column count on * both sides. Previously a mismatch silently truncated the wider * side to the narrower's width, masking schema bugs (`SELECT a @@ -2843,7 +2857,7 @@ METHOD TryBuildSemiJoin( xSubNode ) CLASS TSqlExecutor hLifted[ "having" ] := NIL /* Run the lifted query once. No PushOuter — it's now non-correlated. */ - oSub := TSqlExecutor():New( hLifted, ::aParams ) + oSub := TSqlExecutor():New( hLifted, ::aParams, ::oSession ) oSub:nDepth := ::nDepth aResult := oSub:Run() IF ValType( aResult ) != "A" .OR. Len( aResult ) < 2 .OR. ValType( aResult[ 2 ] ) != "A" @@ -3033,7 +3047,7 @@ METHOD SubqueryCached( xSubNode ) CLASS TSqlExecutor aRecSave := ::SnapshotAreaRecNos() ::PushOuter() BEGIN SEQUENCE - oSub := TSqlExecutor():New( hQ, ::aParams ) + oSub := TSqlExecutor():New( hQ, ::aParams, ::oSession ) /* +2 (not +1): the alias-rename gate in the table-open loop * requires `nDepth > 1` to fire. Bumping by 1 from a top-level * (depth-0) outer landed at depth 1 which still shares aliases @@ -3244,7 +3258,7 @@ METHOD CacheSubquery( xSubExpr ) CLASS TSqlExecutor * negligible; correlated callers go through SubqueryCached. */ nSavedWA := Select() aRecSave := ::SnapshotAreaRecNos() - oSub := TSqlExecutor():New( xSubExpr, ::aParams ) + oSub := TSqlExecutor():New( xSubExpr, ::aParams, ::oSession ) oSub:nDepth := ::nDepth + 2 aSubResult := oSub:Run() ::RestoreAreaRecNos( aRecSave ) @@ -3269,7 +3283,7 @@ METHOD MaterializeCTE( aCTE ) CLASS TSqlExecutor /* Execute the CTE subquery */ IF ValType( xSubQ ) == "A" .AND. xSubQ[ 1 ] == ND_SUB .AND. xSubQ[ 2 ] != NIL - aSub := TSqlExecutor():New( xSubQ[ 2 ], ::aParams ):Run() + aSub := TSqlExecutor():New( xSubQ[ 2 ], ::aParams, ::oSession ):Run() ELSE aSub := NIL ENDIF @@ -3396,7 +3410,7 @@ METHOD RunInsert() CLASS TSqlExecutor * from VALUES (...) tuples or from a SELECT. */ IF hb_HHasKey( ::hQuery, "select" ) hSelect := ::hQuery[ "select" ] - aSelResult := TSqlExecutor():New( hSelect, ::aParams ):Run() + aSelResult := TSqlExecutor():New( hSelect, ::aParams, ::oSession ):Run() IF ValType( aSelResult ) == "A" .AND. Len( aSelResult ) >= 1 .AND. ; ValType( aSelResult[ 1 ] ) == "A" .AND. Len( aSelResult[ 1 ] ) > 0 .AND. ; aSelResult[ 1 ][ 1 ] == "__error__" @@ -4629,7 +4643,7 @@ METHOD MaterializeRecursiveCTE( aCTE ) CLASS TSqlExecutor hSubQ[ "union" ] := NIL /* Execute anchor query (the first SELECT before UNION ALL) */ - aSub := TSqlExecutor():New( hSubQ, ::aParams ):Run() + aSub := TSqlExecutor():New( hSubQ, ::aParams, ::oSession ):Run() /* Restore the union reference for later use */ hSubQ[ "union" ] := hRecQuery diff --git a/_FiveSql2/src/TSqlSession.prg b/_FiveSql2/src/TSqlSession.prg new file mode 100644 index 0000000..bf28c60 --- /dev/null +++ b/_FiveSql2/src/TSqlSession.prg @@ -0,0 +1,79 @@ +/* + * TSqlSession.prg — Per-connection session container + * + * Holds all state that must be isolated between concurrent FiveSql2 + * clients (transaction log, plan cache, current user, role grants). + * Previously this state lived in module-level STATICs of TSqlTxn / + * TFiveSQL, but gengo emits PRG STATIC as Go *package* variables — + * so two PostgreSQL-wire clients sharing the same Go process would + * stomp each other's transaction state. + * + * Embedded usage (`five_SQL(cSQL)` with no explicit session) keeps + * working because FiveSqlCls.prg falls back to a process-default + * session lazily created by 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" + +CLASS TSqlSession + + /* Transaction state (was STATIC in TSqlTxn.prg). */ + DATA aTxnLog INIT {} + DATA lInTxn INIT .F. + DATA hSavepoints INIT NIL + + /* Plan cache (was STATIC s_hPlanCache in TFiveSQL.prg). Keyed by + * "|". Bounded by SQL_PLAN_CACHE_MAX — + * wiped wholesale on overflow (cheaper than per-entry LRU). */ + DATA hPlanCache INIT { => } + + /* Auth / ACL state — populated by pgserver after AuthenticationOk. + * Empty in embedded mode (no row-level checks). */ + DATA cCurrentUser INIT "" + DATA hRolePerms INIT { => } + + /* Workarea aliases this session opened, for cleanup on disconnect. */ + DATA aOpenAreas INIT {} + + METHOD New() CONSTRUCTOR + METHOD Reset() /* drop txn + plan cache, keep auth */ + +ENDCLASS + + +METHOD New() CLASS TSqlSession +RETURN SELF + + +METHOD Reset() CLASS TSqlSession + ::aTxnLog := {} + ::lInTxn := .F. + ::hSavepoints := NIL + ::hPlanCache := { => } +RETURN SELF + + +/* + * SqlDefaultSession — process-wide default session. + * + * Used by embedded callers that don't manage their own session + * (the existing `five_SQL(cSQL)` callsite). Lazy-initialized on + * first use so the cost is paid only by callers that use the + * engine at all. pgserver creates a fresh session per connection + * and bypasses this entirely. + */ +STATIC s_oDefaultSession := NIL + +FUNCTION SqlDefaultSession() + IF s_oDefaultSession == NIL + s_oDefaultSession := TSqlSession():New() + ENDIF +RETURN s_oDefaultSession diff --git a/_FiveSql2/src/TSqlTxn.prg b/_FiveSql2/src/TSqlTxn.prg index 7fa8048..69fb2c2 100644 --- a/_FiveSql2/src/TSqlTxn.prg +++ b/_FiveSql2/src/TSqlTxn.prg @@ -1,9 +1,15 @@ /* * TSqlTxn.prg — Transaction manager (BEGIN/COMMIT/ROLLBACK) * + * Transaction state lives on the TSqlSession passed at construction + * time so concurrent connections (pgserver and embedded callers) + * don't share a single global txn log. New() without an oSession + * argument falls back to SqlDefaultSession() so legacy embedded + * call sites keep working. + * * FiveSql — SQL Engine for Harbour DBF/NTX * - * Copyright (c) 2025 Charles KWON (Charles KWON OhJun) + * Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun) * Email: charleskwonohjun@gmail.com * * All rights reserved. @@ -12,14 +18,11 @@ #include "hbclass.ch" #include "FiveSqlDef.ch" -/* Transaction state must be global across all executor instances */ -STATIC s_aTxnLog := {} -STATIC s_lInTxn := .F. -STATIC s_hSavepoints := NIL - CLASS TSqlTxn - METHOD New() CONSTRUCTOR + DATA oSession /* per-connection state container */ + + METHOD New( oSession ) CONSTRUCTOR METHOD Begin() METHOD Commit() METHOD Rollback() @@ -31,19 +34,24 @@ CLASS TSqlTxn ENDCLASS -METHOD New() CLASS TSqlTxn +METHOD New( oSession ) CLASS TSqlTxn + IF oSession == NIL + ::oSession := SqlDefaultSession() + ELSE + ::oSession := oSession + ENDIF RETURN SELF METHOD IsActive() CLASS TSqlTxn -RETURN s_lInTxn +RETURN ::oSession:lInTxn METHOD Begin() CLASS TSqlTxn - s_aTxnLog := {} - s_lInTxn := .T. - s_hSavepoints := { => } + ::oSession:aTxnLog := {} + ::oSession:lInTxn := .T. + ::oSession:hSavepoints := { => } RETURN { { "result" }, { { "Transaction started" } } } @@ -52,7 +60,7 @@ METHOD Commit() CLASS TSqlTxn LOCAL nArea - IF ! s_lInTxn + IF ! ::oSession:lInTxn RETURN { { "__error__" }, { { SQL_ERR_TXN, "No active transaction to COMMIT", "" } } } ENDIF @@ -63,8 +71,8 @@ METHOD Commit() CLASS TSqlTxn ENDIF NEXT - s_aTxnLog := {} - s_lInTxn := .F. + ::oSession:aTxnLog := {} + ::oSession:lInTxn := .F. RETURN { { "result" }, { { "Transaction committed" } } } @@ -73,17 +81,18 @@ METHOD Rollback() CLASS TSqlTxn LOCAL i, j, cAlias, nRecNo, aFldVals, nWA, nSaved LOCAL lOpened + LOCAL aLog := ::oSession:aTxnLog - IF ! s_lInTxn + IF ! ::oSession:lInTxn RETURN { { "__error__" }, { { SQL_ERR_TXN, "No active transaction to ROLLBACK", "" } } } ENDIF nSaved := Select() - FOR i := Len( s_aTxnLog ) TO 1 STEP -1 - cAlias := s_aTxnLog[ i ][ 1 ] - nRecNo := s_aTxnLog[ i ][ 2 ] - aFldVals := s_aTxnLog[ i ][ 3 ] + FOR i := Len( aLog ) TO 1 STEP -1 + cAlias := aLog[ i ][ 1 ] + nRecNo := aLog[ i ][ 2 ] + aFldVals := aLog[ i ][ 3 ] lOpened := .F. nWA := Select( cAlias ) @@ -103,7 +112,7 @@ METHOD Rollback() CLASS TSqlTxn FOR j := 1 TO Len( aFldVals ) FieldPut( j, aFldVals[ j ] ) NEXT - IF Len( s_aTxnLog[ i ] ) >= 4 .AND. s_aTxnLog[ i ][ 4 ] == "INSERT" + IF Len( aLog[ i ] ) >= 4 .AND. aLog[ i ][ 4 ] == "INSERT" dbDelete() ENDIF dbRUnlock( nRecNo ) @@ -117,22 +126,22 @@ METHOD Rollback() CLASS TSqlTxn dbSelectArea( nSaved ) - s_aTxnLog := {} - s_lInTxn := .F. + ::oSession:aTxnLog := {} + ::oSession:lInTxn := .F. RETURN { { "result" }, { { "Transaction rolled back" } } } METHOD SetSavepoint( cName ) CLASS TSqlTxn - IF ! s_lInTxn + IF ! ::oSession:lInTxn RETURN { { "__error__" }, { { SQL_ERR_TXN, "No active transaction for SAVEPOINT", "" } } } ENDIF - IF s_hSavepoints == NIL - s_hSavepoints := { => } + IF ::oSession:hSavepoints == NIL + ::oSession:hSavepoints := { => } ENDIF - s_hSavepoints[ Upper( cName ) ] := Len( s_aTxnLog ) + ::oSession:hSavepoints[ Upper( cName ) ] := Len( ::oSession:aTxnLog ) RETURN { { "result" }, { { "Savepoint " + cName + " set" } } } @@ -141,23 +150,24 @@ METHOD RollbackTo( cName ) CLASS TSqlTxn LOCAL i, j, cAlias, nRecNo, aFldVals, nWA, nSaved, nSpPos LOCAL lOpened + LOCAL aLog := ::oSession:aTxnLog - IF ! s_lInTxn + IF ! ::oSession:lInTxn RETURN { { "__error__" }, { { SQL_ERR_TXN, "No active transaction for ROLLBACK TO", "" } } } ENDIF - IF s_hSavepoints == NIL .OR. ! hb_HHasKey( s_hSavepoints, Upper( cName ) ) + IF ::oSession:hSavepoints == NIL .OR. ! hb_HHasKey( ::oSession:hSavepoints, Upper( cName ) ) RETURN { { "__error__" }, { { SQL_ERR_TXN, "Savepoint " + cName + " not found", "" } } } ENDIF - nSpPos := s_hSavepoints[ Upper( cName ) ] + nSpPos := ::oSession:hSavepoints[ Upper( cName ) ] nSaved := Select() /* Undo log entries from end back to savepoint position */ - FOR i := Len( s_aTxnLog ) TO nSpPos + 1 STEP -1 - cAlias := s_aTxnLog[ i ][ 1 ] - nRecNo := s_aTxnLog[ i ][ 2 ] - aFldVals := s_aTxnLog[ i ][ 3 ] + FOR i := Len( aLog ) TO nSpPos + 1 STEP -1 + cAlias := aLog[ i ][ 1 ] + nRecNo := aLog[ i ][ 2 ] + aFldVals := aLog[ i ][ 3 ] lOpened := .F. nWA := Select( cAlias ) @@ -177,7 +187,7 @@ METHOD RollbackTo( cName ) CLASS TSqlTxn FOR j := 1 TO Len( aFldVals ) FieldPut( j, aFldVals[ j ] ) NEXT - IF Len( s_aTxnLog[ i ] ) >= 4 .AND. s_aTxnLog[ i ][ 4 ] == "INSERT" + IF Len( aLog[ i ] ) >= 4 .AND. aLog[ i ][ 4 ] == "INSERT" dbDelete() ENDIF dbRUnlock( nRecNo ) @@ -190,7 +200,7 @@ METHOD RollbackTo( cName ) CLASS TSqlTxn NEXT /* Trim the log back to the savepoint position */ - ASize( s_aTxnLog, nSpPos ) + ASize( ::oSession:aTxnLog, nSpPos ) dbSelectArea( nSaved ) @@ -201,7 +211,7 @@ METHOD LogRecord( cAlias, nRecNo, cAction ) CLASS TSqlTxn LOCAL nWA, nSaved, aFldVals := {}, i - IF ! s_lInTxn + IF ! ::oSession:lInTxn RETURN NIL ENDIF @@ -213,7 +223,7 @@ METHOD LogRecord( cAlias, nRecNo, cAction ) CLASS TSqlTxn FOR i := 1 TO FCount() AAdd( aFldVals, FieldGet( i ) ) NEXT - AAdd( s_aTxnLog, { cAlias, nRecNo, aFldVals, cAction } ) + AAdd( ::oSession:aTxnLog, { cAlias, nRecNo, aFldVals, cAction } ) ENDIF dbSelectArea( nSaved )