Files
five/_FiveSql2/src/TSqlExpr.prg
CharlesKWON f4ed42556b checkpoint: season-wide bug fix campaign + infra
Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2
SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved
as a single checkpoint before refactoring the parser to delegate xBase
command translation to the preprocessor.

Highlights:

FiveSql2 engine (_FiveSql2/src/)
- prefix-glob index attach -> explicit convention (<table>_pk.ntx,
  <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop
- DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt)
- COUNT(DISTINCT col) parsed + aggregated via hSeen hash
- UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent)
- DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT)
- Derived table FROM (SELECT...) + JOIN right-side derived
- Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect
- LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs)
- DATE literal round-trip validation (Feb 29 non-leap rejected)
- CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists
- AlterTable type dispatcher comma-wrapped (1-char type "A" no longer
  matches CHARACTER)

Compiler / runtime
- gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity)
- gengo split: emit_block.go, emit_stmt.go, folding.go extracted
- parser/stmtreg.go nudges
- hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*),
  windows debug stubs collapsed
- thread/vm/value/class/pcinterp tightening from panic traces

RDD layer (hbrdd/)
- dbf: null bitmap support (null.go + null_test.go), mmap split
  (mmap_posix.go / mmap_windows.go), byte-level numeric parse
- ntx/cdx: windows mmap parity
- workarea + mem RDD: cross-area state-bleed fixes

RTL (hbrtl/)
- errorlog rewrite with platform-specific FD (errorlog_fd_unix /
  errorlog_fd_other)
- sqlscan, sqlhelpers, indexrtl, datetime extensions

Gates green at checkpoint:
- go test ./...        : PASS
- FiveSql2 SQL:1999    : 43/43
- Harbour compat       : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:26:25 +09:00

473 lines
13 KiB
Plaintext

/*
* 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"
/* SqlExprHasAgg is implemented in Go (hbrtl/sqlexpr.go) — registered
* as SQLEXPRHASAGG. The prior PRG recursive walker has been removed
* to avoid a name collision with the RTL symbol; behavior is
* byte-for-byte identical. See docs/RTL-Go-Native-Migration.md. */
/* SqlExtractWindow: walk xE, find every ND_WINDOW node, replace it
* in-place with a synthetic ND_COL pointing at a generated alias,
* and append {windowExpr, alias} to aWindows for the caller to
* register as hidden SELECT columns. Used by RunSelect so a wrapped
* window function (`SUM(x) OVER () + 100`) can flow through the
* usual ApplyWindowFunctions path: the inner ND_WINDOW becomes a
* hidden top-level ND_WINDOW column, projection evaluates the outer
* expression as ND_BIN(ND_COL("__win_..."), 100), and the trim at
* RunSelect's tail strips the hidden column back off the result.
*
* Returns the (possibly mutated) xE. cPrefix scopes alias names per
* SELECT column so two wrappers don't collide. */
FUNCTION SqlExtractWindow( xE, aWindows, cPrefix )
LOCAL i, cAlias, xNew
IF xE == NIL .OR. ValType( xE ) != "A" .OR. Len( xE ) < 1
RETURN xE
ENDIF
IF xE[ 1 ] == ND_WINDOW
cAlias := cPrefix + "_" + AllTrim( hb_NToS( Len( aWindows ) + 1 ) ) + "__"
AAdd( aWindows, { AClone( xE ), cAlias } )
RETURN { ND_COL, cAlias, NIL, NIL, NIL }
ENDIF
IF xE[ 1 ] == ND_BIN
xE[ 3 ] := SqlExtractWindow( xE[ 3 ], aWindows, cPrefix )
xE[ 4 ] := SqlExtractWindow( xE[ 4 ], aWindows, cPrefix )
RETURN xE
ENDIF
IF xE[ 1 ] == ND_UNI
xE[ 3 ] := SqlExtractWindow( xE[ 3 ], aWindows, cPrefix )
RETURN xE
ENDIF
IF xE[ 1 ] == ND_FN
IF ValType( xE[ 3 ] ) == "A"
FOR i := 1 TO Len( xE[ 3 ] )
xE[ 3 ][ i ] := SqlExtractWindow( xE[ 3 ][ i ], aWindows, cPrefix )
NEXT
ENDIF
RETURN xE
ENDIF
IF xE[ 1 ] == ND_CASE
IF ValType( xE[ 2 ] ) == "A"
FOR i := 1 TO Len( xE[ 2 ] )
xE[ 2 ][ i ][ 1 ] := SqlExtractWindow( xE[ 2 ][ i ][ 1 ], aWindows, cPrefix )
xE[ 2 ][ i ][ 2 ] := SqlExtractWindow( xE[ 2 ][ i ][ 2 ], aWindows, cPrefix )
NEXT
ENDIF
IF Len( xE ) >= 3 .AND. xE[ 3 ] != NIL
xE[ 3 ] := SqlExtractWindow( xE[ 3 ], aWindows, cPrefix )
ENDIF
RETURN xE
ENDIF
RETURN xE
/* SqlIsAggName is implemented in Go (hbrtl/sqlhelpers.go) — registered
* as SQLISAGGNAME. Former PRG body:
* RETURN ( "," + c + "," ) $ ( "," + AGG_FUNCTIONS + "," )
* Removed to avoid the symbol collision; Go version uses a hash lookup
* against aggFuncSet (same AGG_FUNCTIONS list) and skips the substring
* scan + string allocations that were ~9% of GROUP BY CPU. */
/* 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, aFnArgs
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
/* Date arithmetic: Date + N → Date (N days later). N + Date
* is symmetric. Without these branches Date operands collapse
* to 0 via SqlCoerceNum and the result is just the integer
* offset. Mirrors EvalExpr's same-named fix. */
IF ValType( xL ) == "D" .AND. ValType( xR ) == "N"
RETURN xL + xR
ENDIF
IF ValType( xL ) == "N" .AND. ValType( xR ) == "D"
RETURN xR + xL
ENDIF
RETURN SqlCoerceNum( xL ) + SqlCoerceNum( xR )
ENDIF
IF cOp == "-"
IF ValType( xL ) == "D" .AND. ValType( xR ) == "N"
RETURN xL - xR
ENDIF
IF ValType( xL ) == "D" .AND. ValType( xR ) == "D"
RETURN xL - xR
ENDIF
RETURN SqlCoerceNum( xL ) - SqlCoerceNum( xR )
ENDIF
IF cOp == "*"
RETURN SqlCoerceNum( xL ) * SqlCoerceNum( xR )
ENDIF
IF cOp == "/"
IF SqlCoerceNum( xR ) != 0
RETURN SqlCoerceNum( xL ) / SqlCoerceNum( xR )
ENDIF
RETURN 0
ENDIF
IF cOp == "||"
RETURN SqlCoerceStr( xL ) + SqlCoerceStr( 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
/* Evaluate ALL arguments, not just the first */
aFnArgs := {}
FOR i := 1 TO Len( xExpr[ 3 ] )
AAdd( aFnArgs, SqlEvalRowExpr( xExpr[ 3 ][ i ], aFN, aRow ) )
NEXT
RETURN SqlEvalFunc( xExpr[ 2 ], aFnArgs )
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 nodes from an expression tree.
* Returns an array of the original ND_COL AST nodes so callers can
* re-emit them with their qualified names preserved — e.g. FetchRow
* needs "o.qty" / "p.price" rather than the bare "qty" / "price" so
* it can route through FindWA to the right workarea in JOIN contexts.
*/
FUNCTION SqlCollectColExprs( 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, xE )
ENDIF
CASE xE[ 1 ] == ND_BIN
SqlCollectColExprs( xE[ 3 ], aCols )
SqlCollectColExprs( xE[ 4 ], aCols )
CASE xE[ 1 ] == ND_UNI
SqlCollectColExprs( xE[ 3 ], aCols )
CASE xE[ 1 ] == ND_FN
IF ValType( xE[ 3 ] ) == "A"
FOR i := 1 TO Len( xE[ 3 ] )
SqlCollectColExprs( xE[ 3 ][ i ], aCols )
NEXT
ENDIF
CASE xE[ 1 ] == ND_CASE
IF ValType( xE[ 2 ] ) == "A"
FOR i := 1 TO Len( xE[ 2 ] )
SqlCollectColExprs( xE[ 2 ][ i ][ 1 ], aCols )
SqlCollectColExprs( xE[ 2 ][ i ][ 2 ], aCols )
NEXT
ENDIF
IF xE[ 3 ] != NIL
SqlCollectColExprs( xE[ 3 ], aCols )
ENDIF
ENDCASE
RETURN aCols
/* 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