/* * TFiveSQL.prg — Main facade class for FiveSql2 engine * * Uses TSqlParser2 (Pratt parser) exclusively. * * 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: cSQL → parsed hQuery. * * 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. */ #define SQL_PLAN_CACHE_MAX 1000 STATIC s_hPlanCache := { => } CLASS TFiveSQL DATA oLexer DATA oParser DATA oExec DATA aParams INIT {} METHOD New( aParams ) CONSTRUCTOR METHOD Execute( cSQL, bBlock ) METHOD ExecuteWith( cSQL, aParams ) ENDCLASS METHOD New( aParams ) CLASS TFiveSQL IF aParams != NIL ::aParams := aParams ENDIF RETURN SELF METHOD Execute( cSQL, bBlock ) CLASS TFiveSQL LOCAL aTokens, hQuery, aResult LOCAL aLex, cKey, aParams, cVerPrefix /* 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( s_hPlanCache, cKey ) hQuery := HbDeepClone( s_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 := { => } SqlDmlPcodeCacheReset() ENDIF s_hPlanCache[ cKey ] := HbDeepClone( hQuery ) ENDIF ::oExec := TSqlExecutor():New( hQuery, aParams ) ::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 ] ) 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( s_hPlanCache ) >= SQL_PLAN_CACHE_MAX s_hPlanCache := { => } SqlDmlPcodeCacheReset() ENDIF s_hPlanCache[ cKey ] := HbDeepClone( hQuery ) ENDIF ::oExec := TSqlExecutor():New( hQuery, ::aParams ) ::oExec:cCacheKey := cKey ENDIF ::oExec:bRowBlock := bBlock aResult := ::oExec:Run() RETURN aResult METHOD ExecuteWith( cSQL, aParams ) CLASS TFiveSQL ::aParams := aParams RETURN ::Execute( cSQL )