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>
473 lines
13 KiB
Plaintext
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
|