Files
five/_FiveSql2/src/FiveSqlCls.prg
CharlesKWON 93cf5c8bfa 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>
2026-05-14 17:47:00 +09:00

52 lines
1.9 KiB
Plaintext

/*
* FiveSqlCls.prg — Public API wrapper: five_SQL() function
*
* FiveSql2 — SQL Engine for Harbour DBF/NTX
*
* Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun)
* Email: charleskwonohjun@gmail.com
*
* All rights reserved.
*/
#include "FiveSqlDef.ch"
/*
* five_SQL( cSQL [, aParams ] [, bBlock ] [, oSession ] ) --> aResult | NIL
*
* Execute a SQL statement against the current DBF workareas.
*
* Two return modes:
* 1. Without bBlock: returns { aFieldNames, aRows } on success,
* or { {"__error__"}, {{nCode, cMsg, cSQL}} } on failure.
* 2. With bBlock: streams matching rows into the block, spreading
* the SELECT list as positional params. Returns NIL.
* Block mode is the high-performance path — no
* intermediate row array is built.
*
* Block mode only fires for simple SELECT queries that the fast path
* already supports (single table, no JOIN, no GROUP BY, no aggregates,
* all projections are plain column refs). Complex queries fall back to
* 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, oSession )
LOCAL oSql := TFiveSQL():New( aParams, oSession )
RETURN oSql:Execute( cSQL, bBlock )