/* * 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 )