feat: FiveSql2 43/43, @byref, mutable closure, RTL 479, DateTime fix
Major changes since last commit: - FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS - 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.) - @byref pass-by-reference via RefCell pattern - Mutable closure capture (EnsureLocalRef + RefCell sharing) - RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8) - DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display) - Reserved word guard (39 keywords blocked from function calls) - AEval arg order fix (element before index) - Closure capture redecl fix (unique _cap_ names per block) - Hash/string indexing in ArrayPush/ArrayPop - Harbour compat test suite: 51/51 - 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
25
_FiveSql2/src/FiveSqlCls.prg
Normal file
25
_FiveSql2/src/FiveSqlCls.prg
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 ] ) --> aResult
|
||||
*
|
||||
* Execute a SQL statement against the current DBF workareas.
|
||||
* Returns { aFieldNames, aRows } on success,
|
||||
* { {"__error__"}, {{nCode, cMsg, cSQL}} } on failure.
|
||||
*/
|
||||
FUNCTION five_SQL( cSQL, aParams )
|
||||
|
||||
LOCAL oSql := TFiveSQL():New( aParams )
|
||||
|
||||
RETURN oSql:Execute( cSQL )
|
||||
72
_FiveSql2/src/FiveSqlDef.ch
Normal file
72
_FiveSql2/src/FiveSqlDef.ch
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* FiveSqlDef.ch — Shared constant definitions for FiveSql engine
|
||||
*
|
||||
* Copyright (c) 2025 Charles KWON (Charles KWON OhJun)
|
||||
* Email: charleskwonohjun@gmail.com
|
||||
*
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
#ifndef FIVESQLDEF_CH
|
||||
#define FIVESQLDEF_CH
|
||||
|
||||
/* Token types produced by the lexical analyzer */
|
||||
#define TK_END 0 /* End of input */
|
||||
#define TK_NAME 1 /* Identifier or keyword */
|
||||
#define TK_TEXT 2 /* String literal */
|
||||
#define TK_NUM 3 /* Numeric literal */
|
||||
#define TK_COMMA 4 /* , */
|
||||
#define TK_DOT 5 /* . */
|
||||
#define TK_STAR 6 /* * */
|
||||
#define TK_LPAR 7 /* ( */
|
||||
#define TK_RPAR 8 /* ) */
|
||||
#define TK_EQ 9 /* = */
|
||||
#define TK_NEQ 10 /* <> or != */
|
||||
#define TK_LT 11 /* < */
|
||||
#define TK_GT 12 /* > */
|
||||
#define TK_LTE 13 /* <= */
|
||||
#define TK_GTE 14 /* >= */
|
||||
#define TK_QMARK 15 /* ? (parameter placeholder) */
|
||||
#define TK_PLUS 16 /* + */
|
||||
#define TK_MINUS 17 /* - */
|
||||
#define TK_SLASH 18 /* / */
|
||||
#define TK_PIPES 19 /* || (string concatenation) */
|
||||
|
||||
/* Token array element indices */
|
||||
#define TK_TYPE 1
|
||||
#define TK_VALUE 2
|
||||
|
||||
/* Expression AST node types */
|
||||
#define ND_LIT 1 /* Literal value */
|
||||
#define ND_COL 2 /* Column reference */
|
||||
#define ND_FN 3 /* Function call */
|
||||
#define ND_BIN 4 /* Binary operation */
|
||||
#define ND_UNI 5 /* Unary operation */
|
||||
#define ND_CASE 6 /* CASE expression */
|
||||
#define ND_SUB 7 /* Subquery */
|
||||
#define ND_LIST 8 /* Value list */
|
||||
#define ND_PAR 9 /* Bound parameter */
|
||||
#define ND_NIL 10 /* NULL value */
|
||||
#define ND_RANGE 11 /* BETWEEN range */
|
||||
#define ND_WINDOW 12 /* Window function */
|
||||
#define ND_FRAME 13 /* Window frame specification */
|
||||
#define ND_LATERAL 14 /* LATERAL subquery */
|
||||
|
||||
/* Error codes */
|
||||
#define SQL_ERR_NONE 0
|
||||
#define SQL_ERR_SYNTAX 1001
|
||||
#define SQL_ERR_NO_TABLE 1002
|
||||
#define SQL_ERR_NO_FIELD 1003
|
||||
#define SQL_ERR_TYPE 1004
|
||||
#define SQL_ERR_LOCKED 1005
|
||||
#define SQL_ERR_GRAMMAR 1006
|
||||
#define SQL_ERR_UNSUPPORTED 1007
|
||||
#define SQL_ERR_TXN 1008
|
||||
|
||||
/* Recognized aggregate function names */
|
||||
#define AGG_FUNCTIONS "COUNT,SUM,AVG,MIN,MAX,GROUP_CONCAT,STRING_AGG,LISTAGG,JSON_ARRAYAGG,JSON_OBJECTAGG,XMLAGG,ANY_VALUE,BOOL_AND,BOOL_OR"
|
||||
|
||||
/* Recognized scalar function names */
|
||||
#define SCALAR_FUNCTIONS "UPPER,LOWER,TRIM,LTRIM,RTRIM,SUBSTR,SUBSTRING,LEN,LENGTH,REPLACE,SPACE,REPLICATE,STUFF,CHARINDEX,CONCAT,ABS,ROUND,INT,FLOOR,CEILING,CEIL,MOD,POWER,SQRT,SIGN,YEAR,MONTH,DAY,NOW,DATE,TIME,DTOS,DTOC,CTOD,STOD,CAST,CONVERT,STR,VAL,ALLTRIM,IIF,COALESCE,NULLIF,DATEADD,DATEDIFF,EOMONTH,INSTR,REVERSE,PADL,PADR,PADC,ISNUMERIC,ISDATE,ISVALID,TYPEOF,TYPE,FORMAT,HB_HOUR,HB_MINUTE,HB_SECOND,HB_DATETIME,HB_TTOC,HB_CTOT,TIMESTAMP,ROUND_BANKER,EXISTS,EXTRACT,POSITION,OVERLAY,ARRAY,ROW,JSON_VALUE,JSON_QUERY,JSON_EXISTS,JSON_TABLE,JSON_OBJECT,JSON_ARRAY,JSON_OBJECTAGG,JSON_ARRAYAGG,XMLELEMENT,XMLFOREST,XMLAGG,GREATEST,LEAST,LPAD,RPAD,ANY_VALUE,BOOL_AND,BOOL_OR"
|
||||
|
||||
#endif
|
||||
66
_FiveSql2/src/TFiveSQL.prg
Normal file
66
_FiveSql2/src/TFiveSQL.prg
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
CLASS TFiveSQL
|
||||
|
||||
DATA oLexer
|
||||
DATA oParser
|
||||
DATA oExec
|
||||
DATA aParams INIT {}
|
||||
|
||||
METHOD New( aParams ) CONSTRUCTOR
|
||||
METHOD Execute( cSQL )
|
||||
METHOD ExecuteWith( cSQL, aParams )
|
||||
|
||||
ENDCLASS
|
||||
|
||||
|
||||
METHOD New( aParams ) CLASS TFiveSQL
|
||||
|
||||
IF aParams != NIL
|
||||
::aParams := aParams
|
||||
ENDIF
|
||||
|
||||
RETURN SELF
|
||||
|
||||
|
||||
METHOD Execute( cSQL ) CLASS TFiveSQL
|
||||
|
||||
LOCAL aTokens, hQuery, aResult
|
||||
|
||||
/* Parse — no caching (plan trees are mutated during execution) */
|
||||
::oLexer := TSqlLexer():New( cSQL )
|
||||
::oLexer:Tokenize()
|
||||
aTokens := ::oLexer:GetTokens()
|
||||
|
||||
::oParser := TSqlParser2():New( aTokens, ::aParams )
|
||||
hQuery := ::oParser:Parse()
|
||||
|
||||
IF hQuery == NIL
|
||||
RETURN { { "__error__" }, { { SQL_ERR_SYNTAX, "Failed to parse SQL", cSQL } } }
|
||||
ENDIF
|
||||
|
||||
::oExec := TSqlExecutor():New( hQuery, ::aParams )
|
||||
aResult := ::oExec:Run()
|
||||
|
||||
RETURN aResult
|
||||
|
||||
|
||||
METHOD ExecuteWith( cSQL, aParams ) CLASS TFiveSQL
|
||||
|
||||
::aParams := aParams
|
||||
|
||||
RETURN ::Execute( cSQL )
|
||||
336
_FiveSql2/src/TSqlAgg.prg
Normal file
336
_FiveSql2/src/TSqlAgg.prg
Normal file
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* TSqlAgg.prg — GROUP BY aggregation and HAVING filter
|
||||
*
|
||||
* FiveSql — SQL Engine for Harbour DBF/NTX
|
||||
*
|
||||
* Copyright (c) 2025 Charles KWON (Charles KWON OhJun)
|
||||
* Email: charleskwonohjun@gmail.com
|
||||
*
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
#include "hbclass.ch"
|
||||
#include "FiveSqlDef.ch"
|
||||
|
||||
CLASS TSqlAgg
|
||||
|
||||
METHOD New() CONSTRUCTOR
|
||||
METHOD GroupBy( aRows, aFN, aCols, aGroupBy, xHaving, aTables, aParams )
|
||||
METHOD ComputeAgg( xE, aGR, aFN )
|
||||
METHOD FindColIdx( xExpr, aFN )
|
||||
METHOD FindColIdx2( cN, aFN )
|
||||
METHOD EvalHaving( xHaving, aNewRow, aCols, aGroupRows, aFN, aParams )
|
||||
METHOD HasAgg( aCols )
|
||||
METHOD EvalHavingExpr( xE, aNewRow, aCols, aGR, aFN, aParams )
|
||||
|
||||
ENDCLASS
|
||||
|
||||
|
||||
METHOD New() CLASS TSqlAgg
|
||||
RETURN SELF
|
||||
|
||||
|
||||
METHOD HasAgg( aCols ) CLASS TSqlAgg
|
||||
|
||||
LOCAL i
|
||||
|
||||
FOR i := 1 TO Len( aCols )
|
||||
IF SqlExprHasAgg( aCols[ i ][ 1 ] )
|
||||
RETURN .T.
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN .F.
|
||||
|
||||
|
||||
METHOD GroupBy( aRows, aFN, aCols, aGroupBy, xHaving, aTables, aParams ) CLASS TSqlAgg
|
||||
|
||||
LOCAL hGroups := { => }
|
||||
LOCAL i, j, cKey, aGroupRows, aResult := {}
|
||||
LOCAL aNewRow
|
||||
LOCAL nGCol, cN, nCI, lPass
|
||||
|
||||
/* Aggregate on empty set */
|
||||
IF Len( aRows ) == 0 .AND. ::HasAgg( aCols )
|
||||
aNewRow := {}
|
||||
FOR j := 1 TO Len( aCols )
|
||||
IF SqlExprHasAgg( aCols[ j ][ 1 ] )
|
||||
AAdd( aNewRow, 0 )
|
||||
ELSE
|
||||
AAdd( aNewRow, NIL )
|
||||
ENDIF
|
||||
NEXT
|
||||
RETURN { aNewRow }
|
||||
ENDIF
|
||||
|
||||
/* Build group buckets */
|
||||
IF Len( aGroupBy ) == 0 .AND. ::HasAgg( aCols )
|
||||
hGroups[ "__ALL__" ] := aRows
|
||||
ELSE
|
||||
FOR i := 1 TO Len( aRows )
|
||||
cKey := ""
|
||||
FOR j := 1 TO Len( aGroupBy )
|
||||
nGCol := ::FindColIdx( aGroupBy[ j ], aFN )
|
||||
IF nGCol > 0 .AND. nGCol <= Len( aRows[ i ] )
|
||||
cKey += SqlValToStr( aRows[ i ][ nGCol ] ) + "|"
|
||||
ENDIF
|
||||
NEXT
|
||||
IF ! hb_HHasKey( hGroups, cKey )
|
||||
hGroups[ cKey ] := {}
|
||||
ENDIF
|
||||
AAdd( hGroups[ cKey ], aRows[ i ] )
|
||||
NEXT
|
||||
ENDIF
|
||||
|
||||
/* Compute aggregates for each group */
|
||||
FOR EACH aGroupRows IN hb_HValues( hGroups )
|
||||
aNewRow := {}
|
||||
FOR j := 1 TO Len( aCols )
|
||||
IF SqlExprHasAgg( aCols[ j ][ 1 ] )
|
||||
AAdd( aNewRow, ::ComputeAgg( aCols[ j ][ 1 ], aGroupRows, aFN ) )
|
||||
ELSE
|
||||
cN := SqlExprName( aCols[ j ][ 1 ] )
|
||||
nCI := ::FindColIdx2( cN, aFN )
|
||||
IF nCI > 0 .AND. Len( aGroupRows ) > 0 .AND. nCI <= Len( aGroupRows[ 1 ] )
|
||||
AAdd( aNewRow, aGroupRows[ 1 ][ nCI ] )
|
||||
ELSE
|
||||
AAdd( aNewRow, NIL )
|
||||
ENDIF
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
/* HAVING filter */
|
||||
IF xHaving != NIL
|
||||
lPass := ::EvalHaving( xHaving, aNewRow, aCols, aGroupRows, aFN, aParams )
|
||||
IF ! lPass
|
||||
LOOP
|
||||
ENDIF
|
||||
ENDIF
|
||||
|
||||
AAdd( aResult, aNewRow )
|
||||
NEXT
|
||||
|
||||
RETURN aResult
|
||||
|
||||
|
||||
METHOD FindColIdx( xExpr, aFN ) CLASS TSqlAgg
|
||||
|
||||
LOCAL cN, i
|
||||
|
||||
IF xExpr != NIL .AND. xExpr[ 1 ] == ND_COL
|
||||
cN := Upper( xExpr[ 2 ] )
|
||||
IF "." $ cN
|
||||
cN := SubStr( cN, At( ".", cN ) + 1 )
|
||||
ENDIF
|
||||
FOR i := 1 TO Len( aFN )
|
||||
IF Upper( aFN[ i ] ) == cN
|
||||
RETURN i
|
||||
ENDIF
|
||||
NEXT
|
||||
ENDIF
|
||||
|
||||
RETURN 0
|
||||
|
||||
|
||||
METHOD FindColIdx2( cN, aFN ) CLASS TSqlAgg
|
||||
|
||||
LOCAL i
|
||||
|
||||
cN := Upper( cN )
|
||||
FOR i := 1 TO Len( aFN )
|
||||
IF Upper( aFN[ i ] ) == cN
|
||||
RETURN i
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN 0
|
||||
|
||||
|
||||
METHOD ComputeAgg( xE, aGR, aFN ) CLASS TSqlAgg
|
||||
|
||||
LOCAL cFunc, cArgName, nCol, i, xVal
|
||||
LOCAL nCount := 0, nSum := 0, xMin := NIL, xMax := NIL
|
||||
LOCAL cResult, cSep
|
||||
LOCAL xArg
|
||||
|
||||
IF xE == NIL .OR. xE[ 1 ] != ND_FN
|
||||
RETURN 0
|
||||
ENDIF
|
||||
|
||||
cFunc := Upper( xE[ 2 ] )
|
||||
|
||||
IF Len( xE[ 3 ] ) > 0
|
||||
xArg := xE[ 3 ][ 1 ]
|
||||
IF xArg[ 1 ] == ND_COL .AND. xArg[ 2 ] == "*"
|
||||
IF cFunc == "COUNT"
|
||||
RETURN Len( aGR )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
ENDIF
|
||||
cArgName := SqlExprName( xArg )
|
||||
ELSE
|
||||
IF cFunc == "COUNT"
|
||||
RETURN Len( aGR )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
ENDIF
|
||||
|
||||
nCol := ::FindColIdx2( cArgName, aFN )
|
||||
IF nCol == 0 .AND. xArg[ 1 ] == ND_COL
|
||||
IF cFunc == "COUNT"
|
||||
RETURN Len( aGR )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
ENDIF
|
||||
|
||||
FOR i := 1 TO Len( aGR )
|
||||
IF nCol > 0 .AND. nCol <= Len( aGR[ i ] )
|
||||
xVal := aGR[ i ][ nCol ]
|
||||
ELSEIF nCol == 0
|
||||
/* Complex expression (CASE, BIN, etc.) inside aggregate:
|
||||
* evaluate the expression tree against the current row data. */
|
||||
xVal := SqlEvalRowExpr( xArg, aFN, aGR[ i ] )
|
||||
ELSE
|
||||
xVal := NIL
|
||||
ENDIF
|
||||
IF xVal != NIL
|
||||
nCount++
|
||||
nSum += SqlCoerceNum( xVal )
|
||||
IF xMin == NIL .OR. SqlCoerceNum( xVal ) < SqlCoerceNum( xMin )
|
||||
xMin := xVal
|
||||
ENDIF
|
||||
IF xMax == NIL .OR. SqlCoerceNum( xVal ) > SqlCoerceNum( xMax )
|
||||
xMax := xVal
|
||||
ENDIF
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
DO CASE
|
||||
CASE cFunc == "COUNT"
|
||||
RETURN nCount
|
||||
CASE cFunc == "SUM"
|
||||
RETURN nSum
|
||||
CASE cFunc == "AVG"
|
||||
RETURN iif( nCount > 0, nSum / nCount, 0 )
|
||||
CASE cFunc == "MIN"
|
||||
RETURN iif( xMin != NIL, xMin, 0 )
|
||||
CASE cFunc == "MAX"
|
||||
RETURN iif( xMax != NIL, xMax, 0 )
|
||||
CASE cFunc == "GROUP_CONCAT" .OR. cFunc == "STRING_AGG"
|
||||
cResult := ""
|
||||
cSep := ", "
|
||||
FOR i := 1 TO Len( aGR )
|
||||
IF nCol > 0 .AND. nCol <= Len( aGR[ i ] )
|
||||
xVal := aGR[ i ][ nCol ]
|
||||
ELSEIF nCol == 0
|
||||
xVal := SqlEvalRowExpr( xArg, aFN, aGR[ i ] )
|
||||
ELSE
|
||||
xVal := NIL
|
||||
ENDIF
|
||||
IF xVal != NIL
|
||||
IF ! Empty( cResult )
|
||||
cResult += cSep
|
||||
ENDIF
|
||||
cResult += SqlCoerceStr( xVal )
|
||||
ENDIF
|
||||
NEXT
|
||||
RETURN cResult
|
||||
ENDCASE
|
||||
|
||||
RETURN 0
|
||||
|
||||
|
||||
METHOD EvalHaving( xHaving, aNewRow, aCols, aGroupRows, aFN, aParams ) CLASS TSqlAgg
|
||||
|
||||
LOCAL xResult
|
||||
|
||||
xResult := ::EvalHavingExpr( xHaving, aNewRow, aCols, aGroupRows, aFN, aParams )
|
||||
|
||||
RETURN SqlIsTrue( xResult )
|
||||
|
||||
|
||||
METHOD EvalHavingExpr( xE, aNewRow, aCols, aGR, aFN, aParams ) CLASS TSqlAgg
|
||||
|
||||
LOCAL xL, xR, cOp, i, nCI, cN
|
||||
|
||||
IF xE == NIL
|
||||
RETURN NIL
|
||||
ENDIF
|
||||
|
||||
DO CASE
|
||||
CASE xE[ 1 ] == ND_LIT
|
||||
RETURN xE[ 2 ]
|
||||
|
||||
CASE xE[ 1 ] == ND_NIL
|
||||
RETURN NIL
|
||||
|
||||
CASE xE[ 1 ] == ND_COL
|
||||
cN := xE[ 2 ]
|
||||
IF "." $ cN
|
||||
cN := SubStr( cN, At( ".", cN ) + 1 )
|
||||
ENDIF
|
||||
FOR i := 1 TO Len( aCols )
|
||||
IF Upper( aCols[ i ][ 2 ] ) == Upper( cN ) .AND. i <= Len( aNewRow )
|
||||
RETURN aNewRow[ i ]
|
||||
ENDIF
|
||||
NEXT
|
||||
nCI := ::FindColIdx2( cN, aFN )
|
||||
IF nCI > 0 .AND. Len( aGR ) > 0 .AND. nCI <= Len( aGR[ 1 ] )
|
||||
RETURN aGR[ 1 ][ nCI ]
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
CASE xE[ 1 ] == ND_FN
|
||||
IF SqlIsAggName( xE[ 2 ] )
|
||||
RETURN ::ComputeAgg( xE, aGR, aFN )
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
CASE xE[ 1 ] == ND_BIN
|
||||
cOp := xE[ 2 ]
|
||||
IF cOp == "AND"
|
||||
xL := ::EvalHavingExpr( xE[ 3 ], aNewRow, aCols, aGR, aFN, aParams )
|
||||
xR := ::EvalHavingExpr( xE[ 4 ], aNewRow, aCols, aGR, aFN, aParams )
|
||||
RETURN SqlIsTrue( xL ) .AND. SqlIsTrue( xR )
|
||||
ENDIF
|
||||
IF cOp == "OR"
|
||||
xL := ::EvalHavingExpr( xE[ 3 ], aNewRow, aCols, aGR, aFN, aParams )
|
||||
xR := ::EvalHavingExpr( xE[ 4 ], aNewRow, aCols, aGR, aFN, aParams )
|
||||
RETURN SqlIsTrue( xL ) .OR. SqlIsTrue( xR )
|
||||
ENDIF
|
||||
xL := ::EvalHavingExpr( xE[ 3 ], aNewRow, aCols, aGR, aFN, aParams )
|
||||
xR := ::EvalHavingExpr( xE[ 4 ], aNewRow, aCols, aGR, aFN, aParams )
|
||||
xL := SqlCoerceForCmp( xL )
|
||||
xR := SqlCoerceForCmp( xR )
|
||||
IF cOp == "=" .OR. cOp == "=="
|
||||
RETURN SqlCmpEq( xL, xR )
|
||||
ENDIF
|
||||
IF cOp == "<>" .OR. cOp == "!="
|
||||
RETURN ! SqlCmpEq( xL, xR )
|
||||
ENDIF
|
||||
IF cOp == ">"
|
||||
RETURN SqlCmpLt( xR, xL )
|
||||
ENDIF
|
||||
IF cOp == "<"
|
||||
RETURN SqlCmpLt( xL, xR )
|
||||
ENDIF
|
||||
IF cOp == ">="
|
||||
RETURN SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xR, xL )
|
||||
ENDIF
|
||||
IF cOp == "<="
|
||||
RETURN SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xL, xR )
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
CASE xE[ 1 ] == ND_UNI
|
||||
IF xE[ 2 ] == "NOT"
|
||||
xL := ::EvalHavingExpr( xE[ 3 ], aNewRow, aCols, aGR, aFN, aParams )
|
||||
RETURN ! SqlIsTrue( xL )
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
ENDCASE
|
||||
|
||||
RETURN NIL
|
||||
|
||||
|
||||
246
_FiveSql2/src/TSqlAlias.prg
Normal file
246
_FiveSql2/src/TSqlAlias.prg
Normal file
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* TSqlAlias.prg — Central alias manager for FiveSql
|
||||
*
|
||||
* Eliminates hardcoded alias names and prevents workarea collisions.
|
||||
* Every table open goes through this manager, which:
|
||||
* - Generates unique, non-colliding alias names
|
||||
* - Tracks all open workareas per query execution
|
||||
* - Closes all managed workareas on cleanup
|
||||
* - Handles self-join (same table opened multiple times)
|
||||
*
|
||||
* Usage:
|
||||
* oAlias := TSqlAlias():New()
|
||||
* cAlias := oAlias:Acquire( "employees", "e" ) // returns "FA_001"
|
||||
* cAlias := oAlias:Acquire( "employees", "m" ) // returns "FA_002" (independent)
|
||||
* oAlias:Release( cAlias ) // closes one workarea
|
||||
* oAlias:ReleaseAll() // closes all managed workareas
|
||||
*
|
||||
* FiveSql — 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"
|
||||
|
||||
STATIC s_nGlobalSeq := 0 /* global sequence counter — never resets */
|
||||
|
||||
CLASS TSqlAlias
|
||||
|
||||
DATA aSlots /* array of { cAlias, cTable, cUserAlias, lOpen } */
|
||||
|
||||
METHOD New() CONSTRUCTOR
|
||||
METHOD Acquire( cTable, cUserAlias )
|
||||
METHOD AcquireCTE( cCteName )
|
||||
METHOD AcquireTemp( cPurpose )
|
||||
METHOD FindByUser( cUserAlias )
|
||||
METHOD FindByTable( cTable )
|
||||
METHOD RealAlias( cUserAlias )
|
||||
METHOD Release( cAlias )
|
||||
METHOD ReleaseAll()
|
||||
METHOD IsManaged( cAlias )
|
||||
|
||||
ENDCLASS
|
||||
|
||||
|
||||
METHOD New() CLASS TSqlAlias
|
||||
|
||||
::aSlots := {}
|
||||
|
||||
RETURN SELF
|
||||
|
||||
|
||||
/*
|
||||
* Acquire — open a table and return a unique alias name.
|
||||
*
|
||||
* cTable: DBF file name (without .dbf extension)
|
||||
* cUserAlias: the alias the SQL query uses (e.g., "e", "m", "t")
|
||||
*
|
||||
* Returns: the actual Harbour alias assigned (e.g., "FA_001")
|
||||
* or "" if the table cannot be opened.
|
||||
*
|
||||
* The same table can be acquired multiple times with different user
|
||||
* aliases — each gets an independent workarea. This is how self-join
|
||||
* works: "FROM employees e JOIN employees m" opens two copies.
|
||||
*/
|
||||
METHOD Acquire( cTable, cUserAlias ) CLASS TSqlAlias
|
||||
|
||||
LOCAL cAlias, cDbfFile, nWA
|
||||
|
||||
s_nGlobalSeq++
|
||||
cAlias := "FA_" + StrZero( s_nGlobalSeq, 4 )
|
||||
|
||||
cDbfFile := Lower( cTable )
|
||||
IF ! ( ".dbf" $ cDbfFile )
|
||||
cDbfFile := cDbfFile + ".dbf"
|
||||
ENDIF
|
||||
|
||||
/* Open the table with unique alias */
|
||||
BEGIN SEQUENCE
|
||||
USE ( cDbfFile ) NEW SHARED ALIAS ( cAlias )
|
||||
RECOVER
|
||||
/* Try exclusive if shared fails */
|
||||
BEGIN SEQUENCE
|
||||
USE ( cDbfFile ) NEW EXCLUSIVE ALIAS ( cAlias )
|
||||
RECOVER
|
||||
RETURN ""
|
||||
END SEQUENCE
|
||||
END SEQUENCE
|
||||
|
||||
AAdd( ::aSlots, { cAlias, Upper( cTable ), Upper( cUserAlias ), .T. } )
|
||||
|
||||
RETURN cAlias
|
||||
|
||||
|
||||
/*
|
||||
* AcquireCTE — open a CTE temp DBF with unique alias.
|
||||
* CTE temp files are named "__cte_<name>.dbf".
|
||||
*/
|
||||
METHOD AcquireCTE( cCteName ) CLASS TSqlAlias
|
||||
|
||||
LOCAL cAlias, cDbfFile
|
||||
|
||||
s_nGlobalSeq++
|
||||
cAlias := "FA_" + StrZero( s_nGlobalSeq, 4 )
|
||||
|
||||
cDbfFile := "__cte_" + Lower( cCteName ) + ".dbf"
|
||||
|
||||
BEGIN SEQUENCE
|
||||
USE ( cDbfFile ) NEW SHARED ALIAS ( cAlias )
|
||||
RECOVER
|
||||
RETURN ""
|
||||
END SEQUENCE
|
||||
|
||||
AAdd( ::aSlots, { cAlias, Upper( cCteName ), Upper( cCteName ), .T. } )
|
||||
|
||||
RETURN cAlias
|
||||
|
||||
|
||||
/*
|
||||
* AcquireTemp — create a unique alias for temp/derived tables.
|
||||
*/
|
||||
METHOD AcquireTemp( cPurpose ) CLASS TSqlAlias
|
||||
|
||||
LOCAL cAlias
|
||||
|
||||
s_nGlobalSeq++
|
||||
cAlias := "FA_" + StrZero( s_nGlobalSeq, 4 )
|
||||
|
||||
AAdd( ::aSlots, { cAlias, cPurpose, cPurpose, .F. } )
|
||||
|
||||
RETURN cAlias
|
||||
|
||||
|
||||
/*
|
||||
* FindByUser — find the real Harbour alias for a user-specified alias.
|
||||
* Example: FindByUser("e") returns "FA_001"
|
||||
*/
|
||||
METHOD FindByUser( cUserAlias ) CLASS TSqlAlias
|
||||
|
||||
LOCAL i, cUpper
|
||||
|
||||
cUpper := Upper( cUserAlias )
|
||||
FOR i := Len( ::aSlots ) TO 1 STEP -1
|
||||
IF ::aSlots[ i ][ 3 ] == cUpper .AND. ::aSlots[ i ][ 4 ]
|
||||
RETURN ::aSlots[ i ][ 1 ]
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN ""
|
||||
|
||||
|
||||
/*
|
||||
* FindByTable — find the real alias for a table name.
|
||||
* Returns the FIRST open alias for this table.
|
||||
*/
|
||||
METHOD FindByTable( cTable ) CLASS TSqlAlias
|
||||
|
||||
LOCAL i, cUpper
|
||||
|
||||
cUpper := Upper( cTable )
|
||||
FOR i := 1 TO Len( ::aSlots )
|
||||
IF ::aSlots[ i ][ 2 ] == cUpper .AND. ::aSlots[ i ][ 4 ]
|
||||
RETURN ::aSlots[ i ][ 1 ]
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN ""
|
||||
|
||||
|
||||
/*
|
||||
* RealAlias — resolve any alias reference to the actual Harbour alias.
|
||||
* Checks user alias first, then table name.
|
||||
*/
|
||||
METHOD RealAlias( cUserAlias ) CLASS TSqlAlias
|
||||
|
||||
LOCAL cResult
|
||||
|
||||
cResult := ::FindByUser( cUserAlias )
|
||||
IF ! Empty( cResult )
|
||||
RETURN cResult
|
||||
ENDIF
|
||||
|
||||
RETURN ::FindByTable( cUserAlias )
|
||||
|
||||
|
||||
/*
|
||||
* Release — close one managed workarea.
|
||||
*/
|
||||
METHOD Release( cAlias ) CLASS TSqlAlias
|
||||
|
||||
LOCAL i, nWA
|
||||
|
||||
FOR i := 1 TO Len( ::aSlots )
|
||||
IF ::aSlots[ i ][ 1 ] == cAlias .AND. ::aSlots[ i ][ 4 ]
|
||||
nWA := Select( cAlias )
|
||||
IF nWA > 0
|
||||
dbSelectArea( nWA )
|
||||
dbCloseArea()
|
||||
ENDIF
|
||||
::aSlots[ i ][ 4 ] := .F.
|
||||
RETURN NIL
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN NIL
|
||||
|
||||
|
||||
/*
|
||||
* ReleaseAll — close ALL managed workareas.
|
||||
* Called at the end of query execution.
|
||||
*/
|
||||
METHOD ReleaseAll() CLASS TSqlAlias
|
||||
|
||||
LOCAL i, nWA
|
||||
|
||||
FOR i := 1 TO Len( ::aSlots )
|
||||
IF ::aSlots[ i ][ 4 ]
|
||||
nWA := Select( ::aSlots[ i ][ 1 ] )
|
||||
IF nWA > 0
|
||||
dbSelectArea( nWA )
|
||||
dbCloseArea()
|
||||
ENDIF
|
||||
::aSlots[ i ][ 4 ] := .F.
|
||||
ENDIF
|
||||
NEXT
|
||||
::aSlots := {}
|
||||
|
||||
RETURN NIL
|
||||
|
||||
|
||||
/*
|
||||
* IsManaged — check if an alias was opened by this manager.
|
||||
*/
|
||||
METHOD IsManaged( cAlias ) CLASS TSqlAlias
|
||||
|
||||
LOCAL i
|
||||
|
||||
FOR i := 1 TO Len( ::aSlots )
|
||||
IF ::aSlots[ i ][ 1 ] == cAlias
|
||||
RETURN .T.
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN .F.
|
||||
1056
_FiveSql2/src/TSqlDDL.prg
Normal file
1056
_FiveSql2/src/TSqlDDL.prg
Normal file
File diff suppressed because it is too large
Load Diff
2733
_FiveSql2/src/TSqlExecutor.prg
Normal file
2733
_FiveSql2/src/TSqlExecutor.prg
Normal file
File diff suppressed because it is too large
Load Diff
330
_FiveSql2/src/TSqlExpr.prg
Normal file
330
_FiveSql2/src/TSqlExpr.prg
Normal file
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* TSqlExpr.prg — Expression AST node constructor
|
||||
*
|
||||
* FiveSql — SQL Engine for Harbour DBF/NTX
|
||||
*
|
||||
* Copyright (c) 2025 Charles KWON (Charles KWON OhJun)
|
||||
* Email: charleskwonohjun@gmail.com
|
||||
*
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
#include "FiveSqlDef.ch"
|
||||
|
||||
/* Expression node: { nKind, xValue, xLeft, xRight, xExtra } */
|
||||
FUNCTION SqlNode( nKind, xVal, xL, xR, xE )
|
||||
RETURN { nKind, xVal, xL, xR, xE }
|
||||
|
||||
/* Derive a display name from an expression node */
|
||||
FUNCTION SqlExprName( xE )
|
||||
|
||||
LOCAL cR
|
||||
|
||||
IF xE == NIL
|
||||
RETURN "?"
|
||||
ENDIF
|
||||
IF xE[ 1 ] == ND_COL
|
||||
cR := xE[ 2 ]
|
||||
IF "." $ cR
|
||||
RETURN SubStr( cR, At( ".", cR ) + 1 )
|
||||
ENDIF
|
||||
RETURN cR
|
||||
ENDIF
|
||||
IF xE[ 1 ] == ND_FN
|
||||
RETURN xE[ 2 ] + "(...)"
|
||||
ENDIF
|
||||
IF xE[ 1 ] == ND_WINDOW
|
||||
RETURN xE[ 2 ] + "(...)"
|
||||
ENDIF
|
||||
IF xE[ 1 ] == ND_LIT
|
||||
RETURN SqlValToStr( xE[ 2 ] )
|
||||
ENDIF
|
||||
|
||||
RETURN "expr"
|
||||
|
||||
/* Check whether an expression node is an aggregate function call */
|
||||
FUNCTION SqlExprHasAgg( xE )
|
||||
|
||||
IF xE == NIL
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
IF xE[ 1 ] == ND_FN .AND. SqlIsAggName( xE[ 2 ] )
|
||||
RETURN .T.
|
||||
ENDIF
|
||||
|
||||
RETURN .F.
|
||||
|
||||
/* Return .T. if the function name is an aggregate */
|
||||
FUNCTION SqlIsAggName( c )
|
||||
RETURN ( "," + c + "," ) $ ( "," + AGG_FUNCTIONS + "," )
|
||||
|
||||
/* Return .T. if the function name is a recognized scalar */
|
||||
FUNCTION SqlIsScalarName( c )
|
||||
RETURN ( "," + c + "," ) $ ( "," + SCALAR_FUNCTIONS + "," )
|
||||
|
||||
/* Convert any value to string (NULL-safe) */
|
||||
FUNCTION SqlValToStr( x )
|
||||
|
||||
IF x == NIL
|
||||
RETURN "NULL"
|
||||
ENDIF
|
||||
DO CASE
|
||||
CASE ValType( x ) == "C" ; RETURN AllTrim( x )
|
||||
CASE ValType( x ) == "N" ; RETURN AllTrim( Str( x ) )
|
||||
CASE ValType( x ) == "D" ; RETURN DToC( x )
|
||||
CASE ValType( x ) == "L" ; RETURN iif( x, ".T.", ".F." )
|
||||
CASE ValType( x ) == "T" ; RETURN hb_TToC( x )
|
||||
ENDCASE
|
||||
|
||||
RETURN ""
|
||||
|
||||
/* Constant folding: pre-compute literal expressions at parse time */
|
||||
FUNCTION SqlFoldConst( xExpr )
|
||||
|
||||
LOCAL xL, xR, cOp, xResult, nPI
|
||||
|
||||
IF xExpr == NIL
|
||||
RETURN NIL
|
||||
ENDIF
|
||||
|
||||
DO CASE
|
||||
CASE xExpr[ 1 ] == ND_LIT .OR. xExpr[ 1 ] == ND_NIL .OR. ;
|
||||
xExpr[ 1 ] == ND_COL .OR. xExpr[ 1 ] == ND_PAR .OR. ;
|
||||
xExpr[ 1 ] == ND_SUB
|
||||
RETURN xExpr
|
||||
|
||||
CASE xExpr[ 1 ] == ND_BIN
|
||||
xExpr[ 3 ] := SqlFoldConst( xExpr[ 3 ] )
|
||||
xExpr[ 4 ] := SqlFoldConst( xExpr[ 4 ] )
|
||||
IF xExpr[ 3 ] != NIL .AND. xExpr[ 3 ][ 1 ] == ND_LIT .AND. ;
|
||||
xExpr[ 4 ] != NIL .AND. xExpr[ 4 ][ 1 ] == ND_LIT
|
||||
cOp := xExpr[ 2 ]
|
||||
xL := xExpr[ 3 ][ 2 ]
|
||||
xR := xExpr[ 4 ][ 2 ]
|
||||
xResult := NIL
|
||||
IF cOp == "+"
|
||||
IF ValType( xL ) == "C" .AND. ValType( xR ) == "C"
|
||||
xResult := xL + xR
|
||||
ELSEIF ValType( xL ) == "N" .AND. ValType( xR ) == "N"
|
||||
xResult := xL + xR
|
||||
ENDIF
|
||||
ELSEIF cOp == "-" .AND. ValType( xL ) == "N" .AND. ValType( xR ) == "N"
|
||||
xResult := xL - xR
|
||||
ELSEIF cOp == "*" .AND. ValType( xL ) == "N" .AND. ValType( xR ) == "N"
|
||||
xResult := xL * xR
|
||||
ELSEIF cOp == "/" .AND. ValType( xL ) == "N" .AND. ValType( xR ) == "N" .AND. xR != 0
|
||||
xResult := xL / xR
|
||||
ELSEIF cOp == "||" .AND. ValType( xL ) == "C" .AND. ValType( xR ) == "C"
|
||||
xResult := xL + xR
|
||||
ENDIF
|
||||
IF xResult != NIL
|
||||
RETURN SqlNode( ND_LIT, xResult, NIL, NIL, NIL )
|
||||
ENDIF
|
||||
ENDIF
|
||||
RETURN xExpr
|
||||
|
||||
CASE xExpr[ 1 ] == ND_UNI
|
||||
xExpr[ 3 ] := SqlFoldConst( xExpr[ 3 ] )
|
||||
IF xExpr[ 3 ] != NIL .AND. xExpr[ 3 ][ 1 ] == ND_LIT
|
||||
IF xExpr[ 2 ] == "-" .AND. ValType( xExpr[ 3 ][ 2 ] ) == "N"
|
||||
RETURN SqlNode( ND_LIT, -xExpr[ 3 ][ 2 ], NIL, NIL, NIL )
|
||||
ENDIF
|
||||
ENDIF
|
||||
RETURN xExpr
|
||||
|
||||
CASE xExpr[ 1 ] == ND_FN
|
||||
IF ValType( xExpr[ 3 ] ) == "A"
|
||||
xL := AClone( xExpr[ 3 ] )
|
||||
FOR nPI := 1 TO Len( xL )
|
||||
xL[ nPI ] := SqlFoldConst( xL[ nPI ] )
|
||||
NEXT
|
||||
xExpr[ 3 ] := xL
|
||||
ENDIF
|
||||
RETURN xExpr
|
||||
|
||||
CASE xExpr[ 1 ] == ND_CASE
|
||||
IF ValType( xExpr[ 2 ] ) == "A"
|
||||
FOR nPI := 1 TO Len( xExpr[ 2 ] )
|
||||
xExpr[ 2 ][ nPI ][ 1 ] := SqlFoldConst( xExpr[ 2 ][ nPI ][ 1 ] )
|
||||
xExpr[ 2 ][ nPI ][ 2 ] := SqlFoldConst( xExpr[ 2 ][ nPI ][ 2 ] )
|
||||
NEXT
|
||||
ENDIF
|
||||
xExpr[ 3 ] := SqlFoldConst( xExpr[ 3 ] )
|
||||
RETURN xExpr
|
||||
|
||||
CASE xExpr[ 1 ] == ND_RANGE
|
||||
xExpr[ 3 ] := SqlFoldConst( xExpr[ 3 ] )
|
||||
xExpr[ 4 ] := SqlFoldConst( xExpr[ 4 ] )
|
||||
xExpr[ 5 ] := SqlFoldConst( xExpr[ 5 ] )
|
||||
RETURN xExpr
|
||||
|
||||
CASE xExpr[ 1 ] == ND_WINDOW
|
||||
/* Window functions cannot be constant-folded */
|
||||
RETURN xExpr
|
||||
|
||||
ENDCASE
|
||||
|
||||
RETURN xExpr
|
||||
|
||||
|
||||
/*
|
||||
* Evaluate an expression against an in-memory row (no workarea).
|
||||
* Used for recursive CTE where temp files cause conflicts.
|
||||
*/
|
||||
FUNCTION SqlEvalRowExpr( xExpr, aFN, aRow )
|
||||
|
||||
LOCAL xL, xR, cOp, cName, i
|
||||
|
||||
IF xExpr == NIL
|
||||
RETURN NIL
|
||||
ENDIF
|
||||
|
||||
DO CASE
|
||||
CASE xExpr[ 1 ] == ND_LIT
|
||||
RETURN xExpr[ 2 ]
|
||||
|
||||
CASE xExpr[ 1 ] == ND_NIL
|
||||
RETURN NIL
|
||||
|
||||
CASE xExpr[ 1 ] == ND_COL
|
||||
cName := Upper( xExpr[ 2 ] )
|
||||
/* First try qualified name as-is (e.g. "E.MGR_ID") */
|
||||
FOR i := 1 TO Len( aFN )
|
||||
IF Upper( AllTrim( aFN[ i ] ) ) == cName .AND. i <= Len( aRow )
|
||||
RETURN aRow[ i ]
|
||||
ENDIF
|
||||
NEXT
|
||||
/* Fall back: strip alias prefix and match unqualified name */
|
||||
IF "." $ cName
|
||||
cName := SubStr( cName, At( ".", cName ) + 1 )
|
||||
FOR i := 1 TO Len( aFN )
|
||||
IF Upper( AllTrim( aFN[ i ] ) ) == cName .AND. i <= Len( aRow )
|
||||
RETURN aRow[ i ]
|
||||
ENDIF
|
||||
NEXT
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
CASE xExpr[ 1 ] == ND_BIN
|
||||
cOp := xExpr[ 2 ]
|
||||
xL := SqlEvalRowExpr( xExpr[ 3 ], aFN, aRow )
|
||||
xR := SqlEvalRowExpr( xExpr[ 4 ], aFN, aRow )
|
||||
IF cOp == "+"
|
||||
IF ValType( xL ) == "N" .AND. ValType( xR ) == "N"
|
||||
RETURN xL + xR
|
||||
ENDIF
|
||||
RETURN SqlCoerceNum( xL ) + SqlCoerceNum( xR )
|
||||
ENDIF
|
||||
IF cOp == "-"
|
||||
RETURN SqlCoerceNum( xL ) - SqlCoerceNum( xR )
|
||||
ENDIF
|
||||
IF cOp == "*"
|
||||
RETURN SqlCoerceNum( xL ) * SqlCoerceNum( xR )
|
||||
ENDIF
|
||||
IF cOp == "AND"
|
||||
RETURN SqlIsTrue( xL ) .AND. SqlIsTrue( xR )
|
||||
ENDIF
|
||||
IF cOp == "OR"
|
||||
RETURN SqlIsTrue( xL ) .OR. SqlIsTrue( xR )
|
||||
ENDIF
|
||||
IF cOp == "="
|
||||
RETURN SqlCmpEq( xL, xR )
|
||||
ENDIF
|
||||
IF cOp == "<"
|
||||
RETURN SqlCmpLt( xL, xR )
|
||||
ENDIF
|
||||
IF cOp == ">"
|
||||
RETURN SqlCmpLt( xR, xL )
|
||||
ENDIF
|
||||
IF cOp == "<="
|
||||
RETURN SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xL, xR )
|
||||
ENDIF
|
||||
IF cOp == ">="
|
||||
RETURN SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xR, xL )
|
||||
ENDIF
|
||||
IF cOp == "<>" .OR. cOp == "!="
|
||||
RETURN ! SqlCmpEq( xL, xR )
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
CASE xExpr[ 1 ] == ND_CASE
|
||||
IF ValType( xExpr[ 2 ] ) == "A"
|
||||
FOR i := 1 TO Len( xExpr[ 2 ] )
|
||||
xL := SqlEvalRowExpr( xExpr[ 2 ][ i ][ 1 ], aFN, aRow )
|
||||
IF SqlIsTrue( xL )
|
||||
RETURN SqlEvalRowExpr( xExpr[ 2 ][ i ][ 2 ], aFN, aRow )
|
||||
ENDIF
|
||||
NEXT
|
||||
ENDIF
|
||||
IF xExpr[ 3 ] != NIL
|
||||
RETURN SqlEvalRowExpr( xExpr[ 3 ], aFN, aRow )
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
CASE xExpr[ 1 ] == ND_FN
|
||||
IF Len( xExpr[ 3 ] ) > 0
|
||||
RETURN SqlEvalFunc( xExpr[ 2 ], { SqlEvalRowExpr( xExpr[ 3 ][ 1 ], aFN, aRow ) } )
|
||||
ENDIF
|
||||
RETURN SqlEvalFunc( xExpr[ 2 ], {} )
|
||||
|
||||
CASE xExpr[ 1 ] == ND_UNI
|
||||
xL := SqlEvalRowExpr( xExpr[ 3 ], aFN, aRow )
|
||||
IF xExpr[ 2 ] == "NOT"
|
||||
RETURN ! SqlIsTrue( xL )
|
||||
ENDIF
|
||||
IF xExpr[ 2 ] == "-"
|
||||
RETURN -SqlCoerceNum( xL )
|
||||
ENDIF
|
||||
RETURN xL
|
||||
|
||||
ENDCASE
|
||||
|
||||
RETURN NIL
|
||||
|
||||
|
||||
/* Collect all ND_COL leaf column names from an expression tree.
|
||||
* Returns an array of bare (unqualified) column name strings. */
|
||||
FUNCTION SqlCollectCols( xE, aCols )
|
||||
|
||||
LOCAL i
|
||||
|
||||
IF aCols == NIL
|
||||
aCols := {}
|
||||
ENDIF
|
||||
|
||||
IF xE == NIL
|
||||
RETURN aCols
|
||||
ENDIF
|
||||
|
||||
DO CASE
|
||||
CASE xE[ 1 ] == ND_COL
|
||||
IF xE[ 2 ] != "*"
|
||||
AAdd( aCols, SqlExprName( xE ) )
|
||||
ENDIF
|
||||
|
||||
CASE xE[ 1 ] == ND_BIN
|
||||
SqlCollectCols( xE[ 3 ], aCols )
|
||||
SqlCollectCols( xE[ 4 ], aCols )
|
||||
|
||||
CASE xE[ 1 ] == ND_UNI
|
||||
SqlCollectCols( xE[ 3 ], aCols )
|
||||
|
||||
CASE xE[ 1 ] == ND_FN
|
||||
IF ValType( xE[ 3 ] ) == "A"
|
||||
FOR i := 1 TO Len( xE[ 3 ] )
|
||||
SqlCollectCols( xE[ 3 ][ i ], aCols )
|
||||
NEXT
|
||||
ENDIF
|
||||
|
||||
CASE xE[ 1 ] == ND_CASE
|
||||
IF ValType( xE[ 2 ] ) == "A"
|
||||
FOR i := 1 TO Len( xE[ 2 ] )
|
||||
SqlCollectCols( xE[ 2 ][ i ][ 1 ], aCols )
|
||||
SqlCollectCols( xE[ 2 ][ i ][ 2 ], aCols )
|
||||
NEXT
|
||||
ENDIF
|
||||
SqlCollectCols( xE[ 3 ], aCols )
|
||||
|
||||
ENDCASE
|
||||
|
||||
RETURN aCols
|
||||
557
_FiveSql2/src/TSqlFunc.prg
Normal file
557
_FiveSql2/src/TSqlFunc.prg
Normal file
@@ -0,0 +1,557 @@
|
||||
/*
|
||||
* TSqlFunc.prg — Scalar function library and type coercion helpers
|
||||
*
|
||||
* FiveSql — SQL Engine for Harbour DBF/NTX
|
||||
*
|
||||
* Copyright (c) 2025 Charles KWON (Charles KWON OhJun)
|
||||
* Email: charleskwonohjun@gmail.com
|
||||
*
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
#include "FiveSqlDef.ch"
|
||||
|
||||
STATIC s_cCollation := ""
|
||||
|
||||
/* Evaluate a scalar or aggregate function by name */
|
||||
FUNCTION SqlEvalFunc( cName, aArgs )
|
||||
|
||||
LOCAL i, xV, xV2, cS, nN, nN2, cRev
|
||||
|
||||
/* Aggregate functions return placeholder during row-level fetch */
|
||||
IF SqlIsAggName( cName )
|
||||
IF Len( aArgs ) > 0
|
||||
RETURN aArgs[ 1 ]
|
||||
ENDIF
|
||||
RETURN 0
|
||||
ENDIF
|
||||
|
||||
/* String functions */
|
||||
DO CASE
|
||||
CASE cName == "UPPER"
|
||||
RETURN Upper( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "LOWER"
|
||||
RETURN Lower( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "TRIM" .OR. cName == "ALLTRIM"
|
||||
RETURN AllTrim( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "LTRIM"
|
||||
RETURN LTrim( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "RTRIM"
|
||||
RETURN RTrim( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "SUBSTR" .OR. cName == "SUBSTRING"
|
||||
cS := SqlCoerceStr( SqlArg(aArgs,1) )
|
||||
nN := Int( SqlCoerceNum( SqlArg(aArgs,2) ) )
|
||||
IF Len( aArgs ) >= 3
|
||||
RETURN SubStr( cS, nN, Int( SqlCoerceNum( SqlArg(aArgs,3) ) ) )
|
||||
ENDIF
|
||||
RETURN SubStr( cS, nN )
|
||||
CASE cName == "LEFT"
|
||||
RETURN Left( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) )
|
||||
CASE cName == "RIGHT"
|
||||
RETURN Right( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) )
|
||||
CASE cName == "LEN" .OR. cName == "LENGTH"
|
||||
RETURN Len( AllTrim( SqlCoerceStr( SqlArg(aArgs,1) ) ) )
|
||||
CASE cName == "REPLACE"
|
||||
RETURN StrTran( SqlCoerceStr( SqlArg(aArgs,1) ), SqlCoerceStr( SqlArg(aArgs,2) ), SqlCoerceStr( SqlArg(aArgs,3) ) )
|
||||
CASE cName == "SPACE"
|
||||
RETURN Space( Int( SqlCoerceNum( SqlArg(aArgs,1) ) ) )
|
||||
CASE cName == "REPLICATE"
|
||||
RETURN Replicate( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) )
|
||||
CASE cName == "STUFF"
|
||||
cS := SqlCoerceStr( SqlArg(aArgs,1) )
|
||||
RETURN Left( cS, Int(SqlCoerceNum(SqlArg(aArgs,2))) - 1 ) + SqlCoerceStr( SqlArg(aArgs,4) ) + ;
|
||||
SubStr( cS, Int(SqlCoerceNum(SqlArg(aArgs,2))) + Int(SqlCoerceNum(SqlArg(aArgs,3))) )
|
||||
CASE cName == "CHARINDEX"
|
||||
RETURN At( SqlCoerceStr( SqlArg(aArgs,1) ), SqlCoerceStr( SqlArg(aArgs,2) ) )
|
||||
CASE cName == "CONCAT"
|
||||
cS := ""
|
||||
FOR i := 1 TO Len( aArgs )
|
||||
cS += SqlCoerceStr( aArgs[ i ] )
|
||||
NEXT
|
||||
RETURN cS
|
||||
|
||||
/* Math functions */
|
||||
CASE cName == "ABS"
|
||||
RETURN Abs( SqlCoerceNum( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "ROUND"
|
||||
RETURN Round( SqlCoerceNum( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) )
|
||||
CASE cName == "INT" .OR. cName == "FLOOR"
|
||||
RETURN Int( SqlCoerceNum( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "CEILING" .OR. cName == "CEIL"
|
||||
nN := SqlCoerceNum( SqlArg(aArgs,1) )
|
||||
IF nN == Int( nN )
|
||||
RETURN nN
|
||||
ENDIF
|
||||
RETURN Int( nN ) + iif( nN > 0, 1, 0 )
|
||||
CASE cName == "MOD"
|
||||
nN2 := SqlCoerceNum( SqlArg(aArgs,2) )
|
||||
IF nN2 != 0
|
||||
RETURN SqlCoerceNum( SqlArg(aArgs,1) ) % nN2
|
||||
ENDIF
|
||||
RETURN 0
|
||||
CASE cName == "POWER"
|
||||
RETURN SqlCoerceNum( SqlArg(aArgs,1) ) ^ SqlCoerceNum( SqlArg(aArgs,2) )
|
||||
CASE cName == "SQRT"
|
||||
RETURN Sqrt( SqlCoerceNum( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "SIGN"
|
||||
nN := SqlCoerceNum( SqlArg(aArgs,1) )
|
||||
RETURN iif( nN > 0, 1, iif( nN < 0, -1, 0 ) )
|
||||
|
||||
/* Date/time functions */
|
||||
CASE cName == "YEAR"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "D"
|
||||
RETURN Year( xV )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
CASE cName == "MONTH"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "D"
|
||||
RETURN Month( xV )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
CASE cName == "DAY"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "D"
|
||||
RETURN Day( xV )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
CASE cName == "NOW"
|
||||
RETURN hb_DateTime()
|
||||
CASE cName == "DATE"
|
||||
RETURN Date()
|
||||
CASE cName == "TIME"
|
||||
RETURN Time()
|
||||
CASE cName == "DTOS"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "D"
|
||||
RETURN DToS( xV )
|
||||
ENDIF
|
||||
RETURN ""
|
||||
CASE cName == "DTOC"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "D"
|
||||
RETURN DToC( xV )
|
||||
ENDIF
|
||||
RETURN ""
|
||||
CASE cName == "CTOD"
|
||||
RETURN CToD( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "STOD"
|
||||
RETURN SToD( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
|
||||
/* Type conversion functions */
|
||||
CASE cName == "STR"
|
||||
IF Len( aArgs ) >= 2
|
||||
RETURN Str( SqlCoerceNum( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) )
|
||||
ENDIF
|
||||
RETURN AllTrim( Str( SqlCoerceNum( SqlArg(aArgs,1) ) ) )
|
||||
CASE cName == "VAL"
|
||||
RETURN Val( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "CAST"
|
||||
RETURN SqlArg(aArgs,1)
|
||||
CASE cName == "CONVERT"
|
||||
RETURN SqlArg(aArgs,1)
|
||||
|
||||
/* Conditional functions */
|
||||
CASE cName == "IIF"
|
||||
IF SqlIsTrue( SqlArg(aArgs,1) )
|
||||
RETURN SqlArg(aArgs,2)
|
||||
ENDIF
|
||||
RETURN SqlArg(aArgs,3)
|
||||
CASE cName == "COALESCE"
|
||||
FOR i := 1 TO Len( aArgs )
|
||||
IF aArgs[ i ] != NIL .AND. ;
|
||||
!( ValType( aArgs[ i ] ) == "C" .AND. Empty( AllTrim( aArgs[ i ] ) ) )
|
||||
RETURN aArgs[ i ]
|
||||
ENDIF
|
||||
NEXT
|
||||
RETURN NIL
|
||||
CASE cName == "NULLIF"
|
||||
IF SqlCmpEq( SqlArg(aArgs,1), SqlArg(aArgs,2) )
|
||||
RETURN NIL
|
||||
ENDIF
|
||||
RETURN SqlArg(aArgs,1)
|
||||
|
||||
/* Date arithmetic */
|
||||
CASE cName == "DATEADD"
|
||||
xV := SqlArg(aArgs,3)
|
||||
nN := Int( SqlCoerceNum( SqlArg(aArgs,2) ) )
|
||||
cS := Upper( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
IF ValType( xV ) == "D"
|
||||
IF cS == "D" .OR. cS == "DAY" .OR. cS == "DD"
|
||||
RETURN xV + nN
|
||||
ELSEIF cS == "M" .OR. cS == "MONTH" .OR. cS == "MM"
|
||||
RETURN SToD( StrZero( Year(xV), 4 ) + StrZero( Month(xV) + nN, 2 ) + StrZero( Day(xV), 2 ) )
|
||||
ELSEIF cS == "Y" .OR. cS == "YEAR" .OR. cS == "YY" .OR. cS == "YYYY"
|
||||
RETURN SToD( StrZero( Year(xV) + nN, 4 ) + StrZero( Month(xV), 2 ) + StrZero( Day(xV), 2 ) )
|
||||
ENDIF
|
||||
ENDIF
|
||||
RETURN xV
|
||||
CASE cName == "DATEDIFF"
|
||||
xV := SqlArg(aArgs,2)
|
||||
xV2 := SqlArg(aArgs,3)
|
||||
cS := Upper( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
IF ValType( xV ) == "D" .AND. ValType( xV2 ) == "D"
|
||||
IF cS == "D" .OR. cS == "DAY"
|
||||
RETURN xV2 - xV
|
||||
ELSEIF cS == "M" .OR. cS == "MONTH"
|
||||
RETURN ( Year(xV2) - Year(xV) ) * 12 + ( Month(xV2) - Month(xV) )
|
||||
ELSEIF cS == "Y" .OR. cS == "YEAR"
|
||||
RETURN Year(xV2) - Year(xV)
|
||||
ENDIF
|
||||
ENDIF
|
||||
RETURN 0
|
||||
CASE cName == "EOMONTH"
|
||||
xV := SqlArg(aArgs,1)
|
||||
nN := iif( Len(aArgs) >= 2, Int( SqlCoerceNum( SqlArg(aArgs,2) ) ), 0 )
|
||||
IF ValType( xV ) == "D"
|
||||
xV := SToD( StrZero( Year(xV), 4 ) + StrZero( Month(xV) + nN + 1, 2 ) + "01" ) - 1
|
||||
RETURN xV
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
/* Extended string functions */
|
||||
CASE cName == "INSTR"
|
||||
RETURN At( SqlCoerceStr( SqlArg(aArgs,1) ), SqlCoerceStr( SqlArg(aArgs,2) ) )
|
||||
CASE cName == "REVERSE"
|
||||
cS := SqlCoerceStr( SqlArg(aArgs,1) )
|
||||
cRev := ""
|
||||
FOR i := Len(cS) TO 1 STEP -1
|
||||
cRev += SubStr( cS, i, 1 )
|
||||
NEXT
|
||||
RETURN cRev
|
||||
CASE cName == "PADL"
|
||||
RETURN PadL( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) )
|
||||
CASE cName == "PADR"
|
||||
RETURN PadR( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) )
|
||||
CASE cName == "PADC"
|
||||
RETURN PadC( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) )
|
||||
|
||||
/* Type check functions */
|
||||
CASE cName == "ISNUMERIC"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "N"
|
||||
RETURN .T.
|
||||
ELSEIF ValType( xV ) == "C"
|
||||
RETURN Val( AllTrim( xV ) ) != 0 .OR. AllTrim( xV ) == "0"
|
||||
ENDIF
|
||||
RETURN .F.
|
||||
CASE cName == "ISDATE"
|
||||
xV := SqlArg(aArgs,1)
|
||||
RETURN ValType( xV ) == "D"
|
||||
CASE cName == "ISVALID"
|
||||
RETURN SqlArg(aArgs,1) != NIL
|
||||
CASE cName == "TYPEOF" .OR. cName == "TYPE"
|
||||
xV := SqlArg(aArgs,1)
|
||||
RETURN ValType( xV )
|
||||
|
||||
/* Timestamp functions */
|
||||
CASE cName == "HB_HOUR"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "T"
|
||||
RETURN hb_Hour( xV )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
CASE cName == "HB_MINUTE"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "T"
|
||||
RETURN hb_Minute( xV )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
CASE cName == "HB_SECOND"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "T"
|
||||
RETURN hb_Sec( xV )
|
||||
ENDIF
|
||||
RETURN 0
|
||||
CASE cName == "HB_DATETIME"
|
||||
RETURN hb_DateTime()
|
||||
CASE cName == "HB_TTOC"
|
||||
xV := SqlArg(aArgs,1)
|
||||
IF ValType( xV ) == "T"
|
||||
RETURN hb_TToC( xV )
|
||||
ENDIF
|
||||
RETURN ""
|
||||
CASE cName == "HB_CTOT"
|
||||
RETURN hb_CToT( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
CASE cName == "TIMESTAMP"
|
||||
RETURN hb_CToT( SqlCoerceStr( SqlArg(aArgs,1) ) )
|
||||
|
||||
/* Banker's rounding */
|
||||
CASE cName == "ROUND_BANKER"
|
||||
nN := SqlCoerceNum( SqlArg(aArgs,1) )
|
||||
nN2 := iif( Len(aArgs) >= 2, Int( SqlCoerceNum( SqlArg(aArgs,2) ) ), 2 )
|
||||
RETURN SqlBankerRound( nN, nN2 )
|
||||
|
||||
/* Format function */
|
||||
CASE cName == "FORMAT"
|
||||
nN := SqlCoerceNum( SqlArg(aArgs,1) )
|
||||
nN2 := iif( Len(aArgs) >= 2, Int( SqlCoerceNum( SqlArg(aArgs,2) ) ), 2 )
|
||||
RETURN AllTrim( Str( nN, 20, nN2 ) )
|
||||
|
||||
ENDCASE
|
||||
|
||||
RETURN NIL
|
||||
|
||||
|
||||
/* Safe argument accessor */
|
||||
FUNCTION SqlArg( a, n )
|
||||
|
||||
IF n <= Len( a )
|
||||
RETURN a[ n ]
|
||||
ENDIF
|
||||
|
||||
RETURN NIL
|
||||
|
||||
|
||||
/* Coerce to string */
|
||||
FUNCTION SqlCoerceStr( x )
|
||||
|
||||
IF x == NIL
|
||||
RETURN ""
|
||||
ENDIF
|
||||
IF ValType( x ) == "C"
|
||||
RETURN x
|
||||
ENDIF
|
||||
IF ValType( x ) == "N"
|
||||
RETURN AllTrim( Str( x ) )
|
||||
ENDIF
|
||||
IF ValType( x ) == "D"
|
||||
RETURN DToC( x )
|
||||
ENDIF
|
||||
IF ValType( x ) == "L"
|
||||
RETURN iif( x, "T", "F" )
|
||||
ENDIF
|
||||
|
||||
RETURN ""
|
||||
|
||||
|
||||
/* Coerce to numeric */
|
||||
FUNCTION SqlCoerceNum( x )
|
||||
|
||||
IF x == NIL
|
||||
RETURN 0
|
||||
ENDIF
|
||||
IF ValType( x ) == "N"
|
||||
RETURN x
|
||||
ENDIF
|
||||
IF ValType( x ) == "C"
|
||||
RETURN Val( AllTrim( x ) )
|
||||
ENDIF
|
||||
IF ValType( x ) == "L"
|
||||
RETURN iif( x, 1, 0 )
|
||||
ENDIF
|
||||
|
||||
RETURN 0
|
||||
|
||||
|
||||
/* Normalize for comparison: trim and uppercase strings */
|
||||
FUNCTION SqlCoerceForCmp( x )
|
||||
|
||||
IF x == NIL
|
||||
RETURN x
|
||||
ENDIF
|
||||
IF ValType( x ) == "C"
|
||||
RETURN Upper( AllTrim( x ) )
|
||||
ENDIF
|
||||
|
||||
RETURN x
|
||||
|
||||
|
||||
/* Evaluate truthiness */
|
||||
FUNCTION SqlIsTrue( x )
|
||||
|
||||
IF x == NIL
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
IF ValType( x ) == "L"
|
||||
RETURN x
|
||||
ENDIF
|
||||
IF ValType( x ) == "N"
|
||||
RETURN x != 0
|
||||
ENDIF
|
||||
IF ValType( x ) == "C"
|
||||
RETURN ! Empty( x )
|
||||
ENDIF
|
||||
|
||||
RETURN .F.
|
||||
|
||||
|
||||
/* Case-insensitive equality comparison with cross-type coercion */
|
||||
FUNCTION SqlCmpEq( a, b )
|
||||
|
||||
IF a == NIL .OR. b == NIL
|
||||
RETURN a == NIL .AND. b == NIL
|
||||
ENDIF
|
||||
IF ValType( a ) == ValType( b )
|
||||
IF ValType( a ) == "C"
|
||||
RETURN Upper( AllTrim( a ) ) == Upper( AllTrim( b ) )
|
||||
ENDIF
|
||||
RETURN a == b
|
||||
ENDIF
|
||||
IF ValType( a ) == "N" .AND. ValType( b ) == "C"
|
||||
RETURN a == Val( AllTrim( b ) )
|
||||
ENDIF
|
||||
IF ValType( a ) == "C" .AND. ValType( b ) == "N"
|
||||
RETURN Val( AllTrim( a ) ) == b
|
||||
ENDIF
|
||||
|
||||
RETURN .F.
|
||||
|
||||
|
||||
/* Case-insensitive less-than comparison */
|
||||
FUNCTION SqlCmpLt( a, b )
|
||||
|
||||
IF a == NIL .OR. b == NIL
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
IF ValType( a ) == ValType( b )
|
||||
IF ValType( a ) == "C"
|
||||
RETURN Upper( AllTrim( a ) ) < Upper( AllTrim( b ) )
|
||||
ENDIF
|
||||
RETURN a < b
|
||||
ENDIF
|
||||
IF ValType( a ) == "N" .AND. ValType( b ) == "C"
|
||||
RETURN a < Val( AllTrim( b ) )
|
||||
ENDIF
|
||||
IF ValType( a ) == "C" .AND. ValType( b ) == "N"
|
||||
RETURN Val( AllTrim( a ) ) < b
|
||||
ENDIF
|
||||
|
||||
RETURN .F.
|
||||
|
||||
|
||||
/* SQL LIKE pattern matching with optional escape character */
|
||||
FUNCTION SqlLikeMatch( cStr, cPat, cEscape )
|
||||
|
||||
LOCAL nS, nP, nSLen, nPLen, chP, chS
|
||||
|
||||
IF ValType( cStr ) != "C" .OR. ValType( cPat ) != "C"
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
cStr := Upper( AllTrim( cStr ) )
|
||||
cPat := Upper( AllTrim( cPat ) )
|
||||
|
||||
IF cEscape == NIL
|
||||
cEscape := ""
|
||||
ENDIF
|
||||
IF ValType( cEscape ) == "C"
|
||||
cEscape := Upper( AllTrim( cEscape ) )
|
||||
ELSE
|
||||
cEscape := ""
|
||||
ENDIF
|
||||
|
||||
nSLen := Len( cStr )
|
||||
nPLen := Len( cPat )
|
||||
nS := 1
|
||||
nP := 1
|
||||
|
||||
RETURN SqlLikeRecurse( cStr, cPat, nS, nP, nSLen, nPLen, cEscape )
|
||||
|
||||
|
||||
/* Recursive LIKE matcher supporting %, _ and ESCAPE */
|
||||
FUNCTION SqlLikeRecurse( cStr, cPat, nS, nP, nSLen, nPLen, cEsc )
|
||||
|
||||
LOCAL chP, chS
|
||||
|
||||
DO WHILE nP <= nPLen
|
||||
chP := SubStr( cPat, nP, 1 )
|
||||
|
||||
/* Escape character: next pattern char is literal */
|
||||
IF ! Empty( cEsc ) .AND. chP == cEsc .AND. nP < nPLen
|
||||
nP++
|
||||
chP := SubStr( cPat, nP, 1 )
|
||||
IF nS > nSLen
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
chS := SubStr( cStr, nS, 1 )
|
||||
IF chS != chP
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
nS++
|
||||
nP++
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
IF chP == "%"
|
||||
/* Skip consecutive % */
|
||||
DO WHILE nP <= nPLen .AND. SubStr( cPat, nP, 1 ) == "%"
|
||||
nP++
|
||||
ENDDO
|
||||
IF nP > nPLen
|
||||
RETURN .T.
|
||||
ENDIF
|
||||
DO WHILE nS <= nSLen
|
||||
IF SqlLikeRecurse( cStr, cPat, nS, nP, nSLen, nPLen, cEsc )
|
||||
RETURN .T.
|
||||
ENDIF
|
||||
nS++
|
||||
ENDDO
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
|
||||
IF chP == "_"
|
||||
IF nS > nSLen
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
nS++
|
||||
nP++
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* Literal character match */
|
||||
IF nS > nSLen
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
chS := SubStr( cStr, nS, 1 )
|
||||
IF chS != chP
|
||||
RETURN .F.
|
||||
ENDIF
|
||||
nS++
|
||||
nP++
|
||||
ENDDO
|
||||
|
||||
RETURN nS > nSLen
|
||||
|
||||
|
||||
/* Banker's rounding (round half to even) */
|
||||
FUNCTION SqlBankerRound( nVal, nDec )
|
||||
|
||||
LOCAL nFactor, nScaled, nInt, nFrac
|
||||
|
||||
IF nDec == NIL
|
||||
nDec := 2
|
||||
ENDIF
|
||||
|
||||
nFactor := 10 ^ nDec
|
||||
nScaled := nVal * nFactor
|
||||
nInt := Int( nScaled )
|
||||
nFrac := Abs( nScaled - nInt )
|
||||
|
||||
IF Abs( nFrac - 0.5 ) > 0.0000001
|
||||
RETURN Round( nVal, nDec )
|
||||
ENDIF
|
||||
|
||||
IF nInt % 2 == 0
|
||||
RETURN nInt / nFactor
|
||||
ENDIF
|
||||
|
||||
IF nVal >= 0
|
||||
RETURN ( nInt + 1 ) / nFactor
|
||||
ENDIF
|
||||
|
||||
RETURN ( nInt - 1 ) / nFactor
|
||||
|
||||
|
||||
/* Collation support */
|
||||
FUNCTION SqlSetCollation( cCollation )
|
||||
|
||||
s_cCollation := Upper( AllTrim( cCollation ) )
|
||||
IF s_cCollation != "NOCASE" .AND. ! Empty( s_cCollation )
|
||||
hb_cdpSelect( s_cCollation )
|
||||
ENDIF
|
||||
|
||||
RETURN NIL
|
||||
|
||||
FUNCTION SqlGetCollation()
|
||||
RETURN s_cCollation
|
||||
1050
_FiveSql2/src/TSqlIndex.prg
Normal file
1050
_FiveSql2/src/TSqlIndex.prg
Normal file
File diff suppressed because it is too large
Load Diff
208
_FiveSql2/src/TSqlLexer.prg
Normal file
208
_FiveSql2/src/TSqlLexer.prg
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* TSqlLexer.prg — SQL lexical analyzer (tokenizer)
|
||||
*
|
||||
* FiveSql — SQL Engine for Harbour DBF/NTX
|
||||
*
|
||||
* Copyright (c) 2025 Charles KWON (Charles KWON OhJun)
|
||||
* Email: charleskwonohjun@gmail.com
|
||||
*
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
#include "hbclass.ch"
|
||||
#include "FiveSqlDef.ch"
|
||||
|
||||
CLASS TSqlLexer
|
||||
|
||||
DATA cInput
|
||||
DATA aTokens
|
||||
DATA nLen
|
||||
|
||||
METHOD New( cSQL ) CONSTRUCTOR
|
||||
METHOD Tokenize()
|
||||
METHOD GetTokens()
|
||||
|
||||
ENDCLASS
|
||||
|
||||
METHOD New( cSQL ) CLASS TSqlLexer
|
||||
|
||||
::cInput := cSQL
|
||||
::aTokens := {}
|
||||
::nLen := Len( cSQL )
|
||||
|
||||
RETURN SELF
|
||||
|
||||
METHOD GetTokens() CLASS TSqlLexer
|
||||
RETURN ::aTokens
|
||||
|
||||
METHOD Tokenize() CLASS TSqlLexer
|
||||
|
||||
LOCAL nPos, ch, cToken
|
||||
|
||||
nPos := 1
|
||||
::aTokens := {}
|
||||
|
||||
WHILE nPos <= ::nLen
|
||||
ch := SubStr( ::cInput, nPos, 1 )
|
||||
|
||||
/* Skip whitespace */
|
||||
IF ch == " " .OR. ch == Chr(9) .OR. ch == Chr(10) .OR. ch == Chr(13)
|
||||
nPos++
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* Skip single-line comment: -- ... */
|
||||
IF ch == "-" .AND. nPos < ::nLen .AND. SubStr( ::cInput, nPos + 1, 1 ) == "-"
|
||||
WHILE nPos <= ::nLen .AND. SubStr( ::cInput, nPos, 1 ) != Chr(10)
|
||||
nPos++
|
||||
ENDDO
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* Skip block comment */
|
||||
IF ch == "/" .AND. nPos < ::nLen .AND. SubStr( ::cInput, nPos + 1, 1 ) == "*"
|
||||
nPos += 2
|
||||
WHILE nPos < ::nLen
|
||||
IF SubStr( ::cInput, nPos, 1 ) == "*" .AND. SubStr( ::cInput, nPos + 1, 1 ) == "/"
|
||||
nPos += 2
|
||||
EXIT
|
||||
ENDIF
|
||||
nPos++
|
||||
ENDDO
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* String literal (single-quoted) */
|
||||
IF ch == "'"
|
||||
nPos++
|
||||
cToken := ""
|
||||
WHILE nPos <= ::nLen
|
||||
ch := SubStr( ::cInput, nPos, 1 )
|
||||
IF ch == "'"
|
||||
IF nPos < ::nLen .AND. SubStr( ::cInput, nPos + 1, 1 ) == "'"
|
||||
cToken += "'"
|
||||
nPos += 2
|
||||
ELSE
|
||||
nPos++
|
||||
EXIT
|
||||
ENDIF
|
||||
ELSE
|
||||
cToken += ch
|
||||
nPos++
|
||||
ENDIF
|
||||
ENDDO
|
||||
AAdd( ::aTokens, { TK_TEXT, cToken } )
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* Numeric literal */
|
||||
IF ch >= "0" .AND. ch <= "9"
|
||||
cToken := ""
|
||||
WHILE nPos <= ::nLen
|
||||
ch := SubStr( ::cInput, nPos, 1 )
|
||||
IF ( ch >= "0" .AND. ch <= "9" ) .OR. ch == "."
|
||||
cToken += ch
|
||||
nPos++
|
||||
ELSE
|
||||
EXIT
|
||||
ENDIF
|
||||
ENDDO
|
||||
AAdd( ::aTokens, { TK_NUM, cToken } )
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* Identifier or keyword */
|
||||
IF IsAlpha( ch ) .OR. ch == "_"
|
||||
cToken := ""
|
||||
WHILE nPos <= ::nLen
|
||||
ch := SubStr( ::cInput, nPos, 1 )
|
||||
IF IsAlpha( ch ) .OR. IsDigit( ch ) .OR. ch == "_"
|
||||
cToken += ch
|
||||
nPos++
|
||||
ELSE
|
||||
EXIT
|
||||
ENDIF
|
||||
ENDDO
|
||||
AAdd( ::aTokens, { TK_NAME, Upper( cToken ) } )
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* Bracketed identifier: [column_name] */
|
||||
IF ch == "["
|
||||
nPos++
|
||||
cToken := ""
|
||||
WHILE nPos <= ::nLen .AND. SubStr( ::cInput, nPos, 1 ) != "]"
|
||||
cToken += SubStr( ::cInput, nPos, 1 )
|
||||
nPos++
|
||||
ENDDO
|
||||
IF nPos <= ::nLen
|
||||
nPos++
|
||||
ENDIF
|
||||
AAdd( ::aTokens, { TK_NAME, Upper( cToken ) } )
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* Positional parameter placeholder */
|
||||
IF ch == "?"
|
||||
AAdd( ::aTokens, { TK_QMARK, "?" } )
|
||||
nPos++
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
/* Punctuation and operators */
|
||||
DO CASE
|
||||
CASE ch == ","
|
||||
AAdd( ::aTokens, { TK_COMMA, "," } ) ; nPos++
|
||||
CASE ch == "."
|
||||
AAdd( ::aTokens, { TK_DOT, "." } ) ; nPos++
|
||||
CASE ch == "*"
|
||||
AAdd( ::aTokens, { TK_STAR, "*" } ) ; nPos++
|
||||
CASE ch == "("
|
||||
AAdd( ::aTokens, { TK_LPAR, "(" } ) ; nPos++
|
||||
CASE ch == ")"
|
||||
AAdd( ::aTokens, { TK_RPAR, ")" } ) ; nPos++
|
||||
CASE ch == "+"
|
||||
AAdd( ::aTokens, { TK_PLUS, "+" } ) ; nPos++
|
||||
CASE ch == "-"
|
||||
AAdd( ::aTokens, { TK_MINUS, "-" } ) ; nPos++
|
||||
CASE ch == "/"
|
||||
AAdd( ::aTokens, { TK_SLASH, "/" } ) ; nPos++
|
||||
CASE ch == "|"
|
||||
IF nPos < ::nLen .AND. SubStr( ::cInput, nPos + 1, 1 ) == "|"
|
||||
AAdd( ::aTokens, { TK_PIPES, "||" } ) ; nPos += 2
|
||||
ELSE
|
||||
nPos++
|
||||
ENDIF
|
||||
CASE ch == "="
|
||||
AAdd( ::aTokens, { TK_EQ, "=" } ) ; nPos++
|
||||
CASE ch == "<"
|
||||
IF nPos < ::nLen .AND. SubStr( ::cInput, nPos + 1, 1 ) == "="
|
||||
AAdd( ::aTokens, { TK_LTE, "<=" } ) ; nPos += 2
|
||||
ELSEIF nPos < ::nLen .AND. SubStr( ::cInput, nPos + 1, 1 ) == ">"
|
||||
AAdd( ::aTokens, { TK_NEQ, "<>" } ) ; nPos += 2
|
||||
ELSE
|
||||
AAdd( ::aTokens, { TK_LT, "<" } ) ; nPos++
|
||||
ENDIF
|
||||
CASE ch == ">"
|
||||
IF nPos < ::nLen .AND. SubStr( ::cInput, nPos + 1, 1 ) == "="
|
||||
AAdd( ::aTokens, { TK_GTE, ">=" } ) ; nPos += 2
|
||||
ELSE
|
||||
AAdd( ::aTokens, { TK_GT, ">" } ) ; nPos++
|
||||
ENDIF
|
||||
CASE ch == "!"
|
||||
IF nPos < ::nLen .AND. SubStr( ::cInput, nPos + 1, 1 ) == "="
|
||||
AAdd( ::aTokens, { TK_NEQ, "!=" } ) ; nPos += 2
|
||||
ELSE
|
||||
nPos++
|
||||
ENDIF
|
||||
CASE ch == ";"
|
||||
nPos++
|
||||
OTHERWISE
|
||||
nPos++
|
||||
ENDCASE
|
||||
ENDDO
|
||||
|
||||
/* End-of-input sentinel */
|
||||
AAdd( ::aTokens, { TK_END, "" } )
|
||||
|
||||
RETURN SELF
|
||||
2245
_FiveSql2/src/TSqlParser2.prg
Normal file
2245
_FiveSql2/src/TSqlParser2.prg
Normal file
File diff suppressed because it is too large
Load Diff
1173
_FiveSql2/src/TSqlParser_orig.prg
Normal file
1173
_FiveSql2/src/TSqlParser_orig.prg
Normal file
File diff suppressed because it is too large
Load Diff
166
_FiveSql2/src/TSqlSort.prg
Normal file
166
_FiveSql2/src/TSqlSort.prg
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* TSqlSort.prg — ORDER BY sorting and DISTINCT elimination
|
||||
*
|
||||
* FiveSql — SQL Engine for Harbour DBF/NTX
|
||||
*
|
||||
* Copyright (c) 2025 Charles KWON (Charles KWON OhJun)
|
||||
* Email: charleskwonohjun@gmail.com
|
||||
*
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
#include "hbclass.ch"
|
||||
#include "FiveSqlDef.ch"
|
||||
|
||||
/* Module-level state for the sort comparator callback */
|
||||
STATIC s_aOBCols := {}
|
||||
STATIC s_aOBNames := {}
|
||||
|
||||
CLASS TSqlSort
|
||||
|
||||
METHOD New() CONSTRUCTOR
|
||||
METHOD OrderBy( aRows, aFN, aOB, aTables, aParams )
|
||||
METHOD Distinct( aRows )
|
||||
METHOD RowKey( aR )
|
||||
|
||||
ENDCLASS
|
||||
|
||||
|
||||
METHOD New() CLASS TSqlSort
|
||||
RETURN SELF
|
||||
|
||||
|
||||
METHOD OrderBy( aRows, aFN, aOB, aTables, aParams ) CLASS TSqlSort
|
||||
|
||||
LOCAL i, nCol
|
||||
|
||||
IF Len( aRows ) < 2 .OR. Len( aOB ) == 0
|
||||
RETURN aRows
|
||||
ENDIF
|
||||
|
||||
/* Pre-resolve column indexes */
|
||||
s_aOBCols := {}
|
||||
s_aOBNames := aFN
|
||||
FOR i := 1 TO Len( aOB )
|
||||
nCol := SqlFindColIdx( aOB[ i ][ 1 ], aFN )
|
||||
IF nCol == 0
|
||||
nCol := SqlFindColIdx2( SqlExprName( aOB[ i ][ 1 ] ), aFN )
|
||||
ENDIF
|
||||
AAdd( s_aOBCols, { nCol, aOB[ i ][ 2 ] } )
|
||||
NEXT
|
||||
|
||||
ASort( aRows,,, {|a, b| SqlRowCompare( a, b ) < 0 } )
|
||||
|
||||
RETURN aRows
|
||||
|
||||
|
||||
METHOD Distinct( aRows ) CLASS TSqlSort
|
||||
|
||||
LOCAL aR := {}, i, cKey
|
||||
LOCAL hSeen := { => }
|
||||
|
||||
FOR i := 1 TO Len( aRows )
|
||||
cKey := ::RowKey( aRows[ i ] )
|
||||
IF ! hb_HHasKey( hSeen, cKey )
|
||||
hSeen[ cKey ] := .T.
|
||||
AAdd( aR, aRows[ i ] )
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN aR
|
||||
|
||||
|
||||
METHOD RowKey( aR ) CLASS TSqlSort
|
||||
|
||||
LOCAL c := "", i
|
||||
|
||||
FOR i := 1 TO Len( aR )
|
||||
c += SqlValToStr( aR[ i ] ) + "|"
|
||||
NEXT
|
||||
|
||||
RETURN c
|
||||
|
||||
|
||||
/* Find column index from expression in field name array */
|
||||
FUNCTION SqlFindColIdx( xExpr, aFN )
|
||||
|
||||
LOCAL cN, i
|
||||
|
||||
IF xExpr != NIL .AND. xExpr[ 1 ] == ND_COL
|
||||
cN := Upper( xExpr[ 2 ] )
|
||||
IF "." $ cN
|
||||
cN := SubStr( cN, At( ".", cN ) + 1 )
|
||||
ENDIF
|
||||
FOR i := 1 TO Len( aFN )
|
||||
IF Upper( aFN[ i ] ) == cN
|
||||
RETURN i
|
||||
ENDIF
|
||||
NEXT
|
||||
ENDIF
|
||||
|
||||
RETURN 0
|
||||
|
||||
/* Find column index by name */
|
||||
FUNCTION SqlFindColIdx2( cN, aFN )
|
||||
|
||||
LOCAL i
|
||||
|
||||
cN := Upper( cN )
|
||||
FOR i := 1 TO Len( aFN )
|
||||
IF Upper( aFN[ i ] ) == cN
|
||||
RETURN i
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN 0
|
||||
|
||||
|
||||
/* Multi-key row comparator for ASort */
|
||||
FUNCTION SqlRowCompare( aRowA, aRowB )
|
||||
|
||||
LOCAL i, nCol, cDir, xA, xB, nCmp
|
||||
|
||||
FOR i := 1 TO Len( s_aOBCols )
|
||||
nCol := s_aOBCols[ i ][ 1 ]
|
||||
cDir := s_aOBCols[ i ][ 2 ]
|
||||
|
||||
IF nCol <= 0 .OR. nCol > Len( aRowA ) .OR. nCol > Len( aRowB )
|
||||
LOOP
|
||||
ENDIF
|
||||
|
||||
xA := aRowA[ nCol ]
|
||||
xB := aRowB[ nCol ]
|
||||
|
||||
/* NULLs sort last */
|
||||
IF xA == NIL .AND. xB == NIL
|
||||
LOOP
|
||||
ENDIF
|
||||
IF xA == NIL
|
||||
RETURN iif( cDir == "DESC", -1, 1 )
|
||||
ENDIF
|
||||
IF xB == NIL
|
||||
RETURN iif( cDir == "DESC", 1, -1 )
|
||||
ENDIF
|
||||
|
||||
nCmp := 0
|
||||
IF ValType( xA ) == ValType( xB )
|
||||
IF xA < xB
|
||||
nCmp := -1
|
||||
ELSEIF xA > xB
|
||||
nCmp := 1
|
||||
ENDIF
|
||||
ELSEIF ValType( xA ) == "N" .AND. ValType( xB ) == "C"
|
||||
nCmp := iif( xA < Val( AllTrim( xB ) ), -1, iif( xA > Val( AllTrim( xB ) ), 1, 0 ) )
|
||||
ELSEIF ValType( xA ) == "C" .AND. ValType( xB ) == "N"
|
||||
nCmp := iif( Val( AllTrim( xA ) ) < xB, -1, iif( Val( AllTrim( xA ) ) > xB, 1, 0 ) )
|
||||
ENDIF
|
||||
|
||||
IF nCmp != 0
|
||||
IF cDir == "DESC"
|
||||
RETURN -nCmp
|
||||
ENDIF
|
||||
RETURN nCmp
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
RETURN 0
|
||||
220
_FiveSql2/src/TSqlTxn.prg
Normal file
220
_FiveSql2/src/TSqlTxn.prg
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* TSqlTxn.prg — Transaction manager (BEGIN/COMMIT/ROLLBACK)
|
||||
*
|
||||
* FiveSql — SQL Engine for Harbour DBF/NTX
|
||||
*
|
||||
* Copyright (c) 2025 Charles KWON (Charles KWON OhJun)
|
||||
* Email: charleskwonohjun@gmail.com
|
||||
*
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
#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
|
||||
METHOD Begin()
|
||||
METHOD Commit()
|
||||
METHOD Rollback()
|
||||
METHOD RollbackTo( cName )
|
||||
METHOD SetSavepoint( cName )
|
||||
METHOD LogRecord( cAlias, nRecNo, cAction )
|
||||
METHOD IsActive()
|
||||
|
||||
ENDCLASS
|
||||
|
||||
|
||||
METHOD New() CLASS TSqlTxn
|
||||
RETURN SELF
|
||||
|
||||
|
||||
METHOD IsActive() CLASS TSqlTxn
|
||||
RETURN s_lInTxn
|
||||
|
||||
|
||||
METHOD Begin() CLASS TSqlTxn
|
||||
|
||||
s_aTxnLog := {}
|
||||
s_lInTxn := .T.
|
||||
s_hSavepoints := { => }
|
||||
|
||||
RETURN { { "result" }, { { "Transaction started" } } }
|
||||
|
||||
|
||||
METHOD Commit() CLASS TSqlTxn
|
||||
|
||||
LOCAL nArea
|
||||
|
||||
IF ! s_lInTxn
|
||||
RETURN { { "__error__" }, { { SQL_ERR_TXN, "No active transaction to COMMIT", "" } } }
|
||||
ENDIF
|
||||
|
||||
FOR nArea := 1 TO 250
|
||||
IF ( nArea )->( Used() )
|
||||
dbSelectArea( nArea )
|
||||
dbCommit()
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
s_aTxnLog := {}
|
||||
s_lInTxn := .F.
|
||||
|
||||
RETURN { { "result" }, { { "Transaction committed" } } }
|
||||
|
||||
|
||||
METHOD Rollback() CLASS TSqlTxn
|
||||
|
||||
LOCAL i, j, cAlias, nRecNo, aFldVals, nWA, nSaved
|
||||
LOCAL lOpened
|
||||
|
||||
IF ! s_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 ]
|
||||
|
||||
lOpened := .F.
|
||||
nWA := Select( cAlias )
|
||||
IF nWA == 0
|
||||
BEGIN SEQUENCE
|
||||
dbUseArea( .T., "DBFNTX", Lower( cAlias ) + ".dbf", cAlias, .F., .F. )
|
||||
nWA := Select( cAlias )
|
||||
lOpened := .T.
|
||||
RECOVER
|
||||
nWA := 0
|
||||
END SEQUENCE
|
||||
ENDIF
|
||||
IF nWA > 0
|
||||
dbSelectArea( nWA )
|
||||
dbGoto( nRecNo )
|
||||
IF dbRLock( nRecNo )
|
||||
FOR j := 1 TO Len( aFldVals )
|
||||
FieldPut( j, aFldVals[ j ] )
|
||||
NEXT
|
||||
IF Len( s_aTxnLog[ i ] ) >= 4 .AND. s_aTxnLog[ i ][ 4 ] == "INSERT"
|
||||
dbDelete()
|
||||
ENDIF
|
||||
dbRUnlock( nRecNo )
|
||||
ENDIF
|
||||
dbCommit()
|
||||
IF lOpened
|
||||
dbCloseArea()
|
||||
ENDIF
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
dbSelectArea( nSaved )
|
||||
|
||||
s_aTxnLog := {}
|
||||
s_lInTxn := .F.
|
||||
|
||||
RETURN { { "result" }, { { "Transaction rolled back" } } }
|
||||
|
||||
|
||||
METHOD SetSavepoint( cName ) CLASS TSqlTxn
|
||||
|
||||
IF ! s_lInTxn
|
||||
RETURN { { "__error__" }, { { SQL_ERR_TXN, "No active transaction for SAVEPOINT", "" } } }
|
||||
ENDIF
|
||||
|
||||
IF s_hSavepoints == NIL
|
||||
s_hSavepoints := { => }
|
||||
ENDIF
|
||||
s_hSavepoints[ Upper( cName ) ] := Len( s_aTxnLog )
|
||||
|
||||
RETURN { { "result" }, { { "Savepoint " + cName + " set" } } }
|
||||
|
||||
|
||||
METHOD RollbackTo( cName ) CLASS TSqlTxn
|
||||
|
||||
LOCAL i, j, cAlias, nRecNo, aFldVals, nWA, nSaved, nSpPos
|
||||
LOCAL lOpened
|
||||
|
||||
IF ! s_lInTxn
|
||||
RETURN { { "__error__" }, { { SQL_ERR_TXN, "No active transaction for ROLLBACK TO", "" } } }
|
||||
ENDIF
|
||||
|
||||
IF s_hSavepoints == NIL .OR. ! hb_HHasKey( s_hSavepoints, Upper( cName ) )
|
||||
RETURN { { "__error__" }, { { SQL_ERR_TXN, "Savepoint " + cName + " not found", "" } } }
|
||||
ENDIF
|
||||
|
||||
nSpPos := s_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 ]
|
||||
|
||||
lOpened := .F.
|
||||
nWA := Select( cAlias )
|
||||
IF nWA == 0
|
||||
BEGIN SEQUENCE
|
||||
dbUseArea( .T., "DBFNTX", Lower( cAlias ) + ".dbf", cAlias, .F., .F. )
|
||||
nWA := Select( cAlias )
|
||||
lOpened := .T.
|
||||
RECOVER
|
||||
nWA := 0
|
||||
END SEQUENCE
|
||||
ENDIF
|
||||
IF nWA > 0
|
||||
dbSelectArea( nWA )
|
||||
dbGoto( nRecNo )
|
||||
IF dbRLock( nRecNo )
|
||||
FOR j := 1 TO Len( aFldVals )
|
||||
FieldPut( j, aFldVals[ j ] )
|
||||
NEXT
|
||||
IF Len( s_aTxnLog[ i ] ) >= 4 .AND. s_aTxnLog[ i ][ 4 ] == "INSERT"
|
||||
dbDelete()
|
||||
ENDIF
|
||||
dbRUnlock( nRecNo )
|
||||
ENDIF
|
||||
dbCommit()
|
||||
IF lOpened
|
||||
dbCloseArea()
|
||||
ENDIF
|
||||
ENDIF
|
||||
NEXT
|
||||
|
||||
/* Trim the log back to the savepoint position */
|
||||
ASize( s_aTxnLog, nSpPos )
|
||||
|
||||
dbSelectArea( nSaved )
|
||||
|
||||
RETURN { { "result" }, { { "Rolled back to savepoint " + cName } } }
|
||||
|
||||
|
||||
METHOD LogRecord( cAlias, nRecNo, cAction ) CLASS TSqlTxn
|
||||
|
||||
LOCAL nWA, nSaved, aFldVals := {}, i
|
||||
|
||||
IF ! s_lInTxn
|
||||
RETURN NIL
|
||||
ENDIF
|
||||
|
||||
nSaved := Select()
|
||||
nWA := Select( cAlias )
|
||||
IF nWA > 0
|
||||
dbSelectArea( nWA )
|
||||
dbGoto( nRecNo )
|
||||
FOR i := 1 TO FCount()
|
||||
AAdd( aFldVals, FieldGet( i ) )
|
||||
NEXT
|
||||
AAdd( s_aTxnLog, { cAlias, nRecNo, aFldVals, cAction } )
|
||||
ENDIF
|
||||
dbSelectArea( nSaved )
|
||||
|
||||
RETURN NIL
|
||||
Reference in New Issue
Block a user