diff --git a/_FiveSql2/Makefile b/_FiveSql2/Makefile index 12608e1..edb822f 100644 --- a/_FiveSql2/Makefile +++ b/_FiveSql2/Makefile @@ -68,12 +68,6 @@ test_challenge: $(OUTDIR) test_extreme: $(OUTDIR) $(HB) $(TESTDIR)/test_sql_extreme.prg $(SOURCES) -o$(OUTDIR)/test_extreme $(HBFLAGS) -test_cmp: $(OUTDIR) - $(HB) $(TESTDIR)/test_parser_cmp.prg $(SRCDIR)/TSqlParser2.prg $(SRCDIR)/TSqlLexer.prg $(SRCDIR)/TSqlExpr.prg $(SRCDIR)/TSqlFunc.prg $(SRCDIR)/FiveSqlDef.ch -o$(OUTDIR)/test_cmp $(HBFLAGS) - -bench: $(OUTDIR) - $(HB) $(TESTDIR)/bench_parser.prg $(SRCDIR)/TSqlParser2.prg $(SRCDIR)/TSqlLexer.prg $(SRCDIR)/TSqlExpr.prg $(SRCDIR)/TSqlFunc.prg $(SRCDIR)/FiveSqlDef.ch -o$(OUTDIR)/bench_parser $(HBFLAGS) - # ============================================================================ # Run tests # ============================================================================ diff --git a/_FiveSql2/src/TSqlDDL.prg b/_FiveSql2/src/TSqlDDL.prg index dce000b..a73912b 100644 --- a/_FiveSql2/src/TSqlDDL.prg +++ b/_FiveSql2/src/TSqlDDL.prg @@ -176,8 +176,7 @@ METHOD CreateTable( aTokens, nPos ) CLASS TSqlDDL LOOP ENDIF - /* CHECK constraint (table-level) — inline parens extraction - * because Five does not support pass-by-reference (@nPos) */ + /* CHECK constraint (table-level) — inline parens extraction. */ IF ::DDL_IsKW( aTokens, nPos, "CHECK" ) nPos++ cCheckExpr := "" diff --git a/_FiveSql2/src/TSqlIndex.prg b/_FiveSql2/src/TSqlIndex.prg index c83556b..cb4fdab 100644 --- a/_FiveSql2/src/TSqlIndex.prg +++ b/_FiveSql2/src/TSqlIndex.prg @@ -30,17 +30,11 @@ CLASS TSqlIndex METHOD ListTags( nWA ) METHOD FindBestTag( nWA, cField ) METHOD FindCompoundTag( nWA, aFields ) - METHOD ApplyScope( nWA, cField, xLow, xHigh ) - METHOD ClearScope( nWA ) - METHOD ApplySeek( nWA, cField, xValue ) METHOD BuildKey( nWA, xValue ) METHOD MatchOrderByTag( nWA, aOrderBy, aFieldNames ) METHOD TryIndexScan( nWA, xWhere, xFullWhere, aTables, aParams, aRE, aRows ) METHOD TryIndexJoinScan( nWA, xWhere, aTables, aParams, aRE, aRows, aJoins ) - METHOD IndexInfo( nWA ) METHOD BuildKeyExpr( nWA, cField ) - METHOD CreateTempIndex( nWA, cField ) - METHOD DropTempIndex( nWA, cTempFile ) METHOD ExtractStrWidth( cExpr ) METHOD TryCompoundSeek( nWA, xWhere, xFullWhere, aTables, aParams, aRE, aRows ) METHOD ExtractEqPairs( xW, aTables, aParams, aOut ) @@ -344,95 +338,6 @@ METHOD FindCompoundTag( nWA, aFields ) CLASS TSqlIndex RETURN nBestTag -/* - * Apply scope boundaries for range queries. - * - * CDX advantage: Can switch to the best tag for this field from among - * multiple available tags, then set scope on that specific tag. - * After the query, the original tag order is restored. - * - * NTX limitation: ordScope() applies to the currently active index. - * FindBestTag must select the right .ntx file first. Only one - * scope can be active at a time across all attached NTX files. - */ -METHOD ApplyScope( nWA, cField, xLow, xHigh ) CLASS TSqlIndex - - LOCAL nSaved, nTag, cRDD - LOCAL xScopeLow, xScopeHigh, nPrevTag - - nSaved := Select() - dbSelectArea( nWA ) - - cRDD := Upper( rddName() ) - - /* Save current tag so we can restore it after CDX tag switch */ - nPrevTag := ordSetFocus() - - nTag := ::FindBestTag( nWA, cField ) - IF nTag == 0 - /* No matching index — restore previous tag and fall back to full scan */ - IF nPrevTag > 0 - ordSetFocus( nPrevTag ) - ENDIF - dbSelectArea( nSaved ) - RETURN .F. - ENDIF - - xScopeLow := ::BuildKey( nWA, xLow ) - xScopeHigh := ::BuildKey( nWA, xHigh ) - - IF cRDD == "DBFCDX" - /* CDX: tag is already set by FindBestTag. ordScope applies to active tag. - * Multiple tags can have independent scopes in CDX. */ - ordScope( 0, xScopeLow ) - ordScope( 1, xScopeHigh ) - ELSE - /* NTX: only one active index at a time. Scope applies globally. */ - ordScope( 0, xScopeLow ) - ordScope( 1, xScopeHigh ) - ENDIF - - dbSelectArea( nSaved ) - -RETURN .T. - - -METHOD ClearScope( nWA ) CLASS TSqlIndex - - LOCAL nSaved - - nSaved := Select() - dbSelectArea( nWA ) - - ordScope( 0, NIL ) - ordScope( 1, NIL ) - - dbSelectArea( nSaved ) - -RETURN NIL - - -METHOD ApplySeek( nWA, cField, xValue ) CLASS TSqlIndex - - LOCAL nSaved, nTag, xSeekKey, lFound - - nSaved := Select() - dbSelectArea( nWA ) - - nTag := ::FindBestTag( nWA, cField ) - IF nTag == 0 - dbSelectArea( nSaved ) - RETURN .F. - ENDIF - - xSeekKey := ::BuildKey( nWA, xValue ) - lFound := dbSeek( xSeekKey, .T. ) - - dbSelectArea( nSaved ) - -RETURN lFound - - METHOD BuildKey( nWA, xValue ) CLASS TSqlIndex LOCAL cExpr, nSaved, nWidth @@ -714,34 +619,6 @@ METHOD TryIndexJoinScan( nWA, xWhere, aTables, aParams, aRE, aRows, aJoins ) CLA RETURN .F. -METHOD IndexInfo( nWA ) CLASS TSqlIndex - - LOCAL aTags, i, cInfo - - aTags := ::ListTags( nWA ) - - cInfo := "RDD: " + ::DetectRDD( nWA ) + ; - " Orders: " + hb_ntos( Len( aTags ) ) + Chr(10) - - FOR i := 1 TO Len( aTags ) - cInfo += " [" + hb_ntos( i ) + "] " + ; - PadR( aTags[ i ][ 1 ], 12 ) + ; - " Key: " + aTags[ i ][ 2 ] - IF ! Empty( aTags[ i ][ 3 ] ) - cInfo += " FOR: " + aTags[ i ][ 3 ] - ENDIF - IF aTags[ i ][ 4 ] - cInfo += " UNIQUE" - ENDIF - IF aTags[ i ][ 5 ] - cInfo += " DESC" - ENDIF - cInfo += Chr(10) - NEXT - -RETURN cInfo - - METHOD BuildKeyExpr( nWA, cField ) CLASS TSqlIndex LOCAL nSaved, nFPos, cType, nLen @@ -774,66 +651,6 @@ METHOD BuildKeyExpr( nWA, cField ) CLASS TSqlIndex RETURN cField -METHOD CreateTempIndex( nWA, cField ) CLASS TSqlIndex - - LOCAL nSaved, cTable, cTempFile, cExpr, cRDD - LOCAL lOk := .F. - - nSaved := Select() - dbSelectArea( nWA ) - - cRDD := rddName() - cTable := Lower( AllTrim( Alias() ) ) - cField := Upper( AllTrim( cField ) ) - - cExpr := ::BuildKeyExpr( nWA, cField ) - - IF Upper( cRDD ) == "DBFCDX" - cTempFile := "__tmp_" + cTable + ".cdx" - BEGIN SEQUENCE - ordCreate( cTempFile, "__TMP_" + cField, cExpr ) - lOk := .T. - RECOVER - END SEQUENCE - ELSE - cTempFile := "__tmp_" + cTable + "_" + Lower( cField ) + ".ntx" - BEGIN SEQUENCE - dbCreateIndex( cTempFile, cExpr ) - lOk := .T. - RECOVER - END SEQUENCE - ENDIF - - dbSelectArea( nSaved ) - - IF lOk - RETURN cTempFile - ENDIF - -RETURN "" - - -METHOD DropTempIndex( nWA, cTempFile ) CLASS TSqlIndex - - LOCAL nSaved - - IF Empty( cTempFile ) - RETURN NIL - ENDIF - - nSaved := Select() - dbSelectArea( nWA ) - - ordSetFocus( 0 ) - dbClearIndex() - - dbSelectArea( nSaved ) - - FErase( cTempFile ) - -RETURN NIL - - METHOD ExtractStrWidth( cExpr ) CLASS TSqlIndex LOCAL nComma, nParen, cWidth diff --git a/_FiveSql2/src/TSqlParser_orig.prg b/_FiveSql2/src/TSqlParser_orig.prg deleted file mode 100644 index 9c68526..0000000 --- a/_FiveSql2/src/TSqlParser_orig.prg +++ /dev/null @@ -1,1173 +0,0 @@ -/* - * TSqlParser.prg — Recursive descent SQL parser - * - * 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 TSqlParser - - DATA aTokens - DATA nPos - DATA aParams - - METHOD New( aTokens, aParams ) CONSTRUCTOR - METHOD Parse() - METHOD ParseSelect() - METHOD ParseInsert() - METHOD ParseUpdate() - METHOD ParseDelete() - METHOD ParseExpr() - METHOD ParseOr() - METHOD ParseAnd() - METHOD ParseNot() - METHOD ParseCompare() - METHOD ParseAdd() - METHOD ParseMul() - METHOD ParseUnary() - METHOD ParsePrimary() - METHOD ParseSubquery() - METHOD ParseColumnList() - METHOD ParseFrom() - METHOD ParseOrderBy() - METHOD ParseExprList() - METHOD TType( n ) - METHOD TVal( n ) - METHOD IsKW( n, c ) - METHOD EatKW( c ) - METHOD IsFromKW( cVal ) - METHOD ParseWindow( cFuncName, aFuncArgs ) - METHOD ParseMerge() - -ENDCLASS - - -METHOD New( aTokens, aParams ) CLASS TSqlParser - - ::aTokens := aTokens - ::nPos := 1 - ::aParams := iif( aParams == NIL, {}, aParams ) - -RETURN SELF - - -/* Token type at position n */ -METHOD TType( n ) CLASS TSqlParser - - IF n > 0 .AND. n <= Len( ::aTokens ) - RETURN ::aTokens[ n ][ TK_TYPE ] - ENDIF - -RETURN TK_END - -/* Token value at position n */ -METHOD TVal( n ) CLASS TSqlParser - - IF n > 0 .AND. n <= Len( ::aTokens ) - RETURN ::aTokens[ n ][ TK_VALUE ] - ENDIF - -RETURN "" - -/* Check whether token at position n is a keyword matching c */ -METHOD IsKW( n, c ) CLASS TSqlParser -RETURN ::TType( n ) == TK_NAME .AND. ::TVal( n ) == c - -/* Consume keyword c at current position; advance and return .T. on match */ -METHOD EatKW( c ) CLASS TSqlParser - - IF ::IsKW( ::nPos, c ) - ::nPos++ - RETURN .T. - ENDIF - -RETURN .F. - -/* Test whether a value is a SQL clause keyword that terminates a FROM list */ -METHOD IsFromKW( cVal ) CLASS TSqlParser - - LOCAL aKW := { "WHERE", "ORDER", "GROUP", "HAVING", "JOIN", "LEFT", ; - "RIGHT", "INNER", "ON", "OUTER", "CROSS", "FULL", ; - "SET", "VALUES", "LIMIT", "TOP", "UNION", ; - "INTERSECT", "EXCEPT", "WITH" } - -RETURN AScan( aKW, {|x| x == cVal } ) > 0 - - -/* Top-level statement dispatcher */ -METHOD Parse() CLASS TSqlParser - - LOCAL cType, h - LOCAL aCTE, cName, xSub, lRecursive - - IF Len( ::aTokens ) < 2 - RETURN NIL - ENDIF - - cType := ::TVal( ::nPos ) - - /* WITH (Common Table Expression), including RECURSIVE */ - IF cType == "WITH" - ::nPos++ - aCTE := {} - lRecursive := .F. - WHILE .T. - /* Detect RECURSIVE keyword */ - IF ::IsKW( ::nPos, "RECURSIVE" ) - lRecursive := .T. - ::nPos++ - ENDIF - cName := ::TVal( ::nPos ) - ::nPos++ - ::EatKW( "AS" ) - xSub := ::ParseSubquery() - AAdd( aCTE, { cName, xSub } ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - /* Parse the main SELECT statement after WITH */ - ::EatKW( "SELECT" ) - h := ::ParseSelect() - IF h != NIL - h[ "cte" ] := aCTE - h[ "cte_recursive" ] := lRecursive - ENDIF - RETURN h - ENDIF - - DO CASE - CASE cType == "SELECT" - ::nPos++ - RETURN ::ParseSelect() - - CASE cType == "INSERT" - ::nPos++ - RETURN ::ParseInsert() - - CASE cType == "UPDATE" - ::nPos++ - RETURN ::ParseUpdate() - - CASE cType == "DELETE" - ::nPos++ - RETURN ::ParseDelete() - - CASE cType == "CREATE" - h := { => } - ::nPos++ - h[ "type" ] := "CREATE" - h[ "tokens" ] := ::aTokens - h[ "pos" ] := ::nPos - RETURN h - - CASE cType == "DROP" - h := { => } - ::nPos++ - h[ "type" ] := "DROP" - h[ "tokens" ] := ::aTokens - h[ "pos" ] := ::nPos - RETURN h - - CASE cType == "SET" - h := { => } - ::nPos++ - IF ::IsKW( ::nPos, "COLLATION" ) - ::nPos++ - ::EatKW( "TO" ) - h[ "type" ] := "SET_COLLATION" - h[ "value" ] := ::TVal( ::nPos ) - ::nPos++ - ELSE - h[ "type" ] := "SET" - h[ "tokens" ] := ::aTokens - h[ "pos" ] := ::nPos - ENDIF - RETURN h - - CASE cType == "ALTER" - h := { => } - ::nPos++ - h[ "type" ] := "ALTER" - h[ "tokens" ] := ::aTokens - h[ "pos" ] := ::nPos - RETURN h - - CASE cType == "BEGIN" - h := { => } - h[ "type" ] := "BEGIN" - RETURN h - - CASE cType == "COMMIT" - h := { => } - h[ "type" ] := "COMMIT" - RETURN h - - CASE cType == "ROLLBACK" - h := { => } - ::nPos++ - IF ::IsKW( ::nPos, "TO" ) - ::nPos++ - /* ROLLBACK TO [SAVEPOINT] name */ - IF ::IsKW( ::nPos, "SAVEPOINT" ) - ::nPos++ - ENDIF - h[ "type" ] := "ROLLBACK_TO" - h[ "savepoint" ] := ::TVal( ::nPos ) - ::nPos++ - ELSE - h[ "type" ] := "ROLLBACK" - ENDIF - RETURN h - - CASE cType == "SAVEPOINT" - h := { => } - ::nPos++ - h[ "type" ] := "SAVEPOINT" - h[ "name" ] := ::TVal( ::nPos ) - ::nPos++ - RETURN h - - CASE cType == "TRUNCATE" - h := { => } - ::nPos++ - ::EatKW( "TABLE" ) - h[ "type" ] := "TRUNCATE" - h[ "table" ] := ::TVal( ::nPos ) - ::nPos++ - RETURN h - - CASE cType == "MERGE" - RETURN ::ParseMerge() - - ENDCASE - -RETURN NIL - - -/* Parse SELECT statement */ -METHOD ParseSelect() CLASS TSqlParser - - LOCAL h := { => } - LOCAL nTop := 0, lDistinct := .F. - LOCAL aCols, aTables := {}, aJoins := {} - LOCAL xWhere := NIL, aGroupBy := {}, xHaving := NIL, aOrderBy := {} - LOCAL nLimit := 0, nOffset := 0 - LOCAL hUnion := NIL - LOCAL lAll - - h[ "type" ] := "SELECT" - - /* DISTINCT */ - IF ::IsKW( ::nPos, "DISTINCT" ) - lDistinct := .T. - ::nPos++ - ENDIF - h[ "distinct" ] := lDistinct - - /* TOP n */ - IF ::IsKW( ::nPos, "TOP" ) - ::nPos++ - IF ::TType( ::nPos ) == TK_NUM - nTop := Val( ::TVal( ::nPos ) ) - ::nPos++ - ENDIF - ENDIF - h[ "top" ] := nTop - - /* Column list */ - aCols := ::ParseColumnList() - h[ "columns" ] := aCols - - /* FROM */ - IF ::IsKW( ::nPos, "FROM" ) - ::nPos++ - ::ParseFrom( @aTables, @aJoins ) - ENDIF - h[ "tables" ] := aTables - h[ "joins" ] := aJoins - - /* WHERE */ - IF ::IsKW( ::nPos, "WHERE" ) - ::nPos++ - xWhere := ::ParseExpr() - ENDIF - h[ "where" ] := xWhere - - /* GROUP BY */ - IF ::IsKW( ::nPos, "GROUP" ) - ::nPos++ - ::EatKW( "BY" ) - aGroupBy := ::ParseExprList() - ENDIF - h[ "group_by" ] := aGroupBy - - /* HAVING */ - IF ::IsKW( ::nPos, "HAVING" ) - ::nPos++ - xHaving := ::ParseExpr() - ENDIF - h[ "having" ] := xHaving - - /* ORDER BY */ - IF ::IsKW( ::nPos, "ORDER" ) - ::nPos++ - ::EatKW( "BY" ) - aOrderBy := ::ParseOrderBy() - ENDIF - h[ "order_by" ] := aOrderBy - - /* LIMIT / OFFSET */ - IF ::IsKW( ::nPos, "LIMIT" ) - ::nPos++ - IF ::TType( ::nPos ) == TK_NUM - nLimit := Val( ::TVal( ::nPos ) ) - ::nPos++ - ENDIF - IF ::IsKW( ::nPos, "OFFSET" ) - ::nPos++ - IF ::TType( ::nPos ) == TK_NUM - nOffset := Val( ::TVal( ::nPos ) ) - ::nPos++ - ENDIF - ENDIF - ENDIF - h[ "limit" ] := nLimit - h[ "offset" ] := nOffset - - /* UNION / UNION ALL / INTERSECT / EXCEPT */ - IF ::IsKW( ::nPos, "UNION" ) - ::nPos++ - lAll := .F. - IF ::IsKW( ::nPos, "ALL" ) - lAll := .T. - ::nPos++ - ENDIF - ::EatKW( "SELECT" ) - hUnion := ::ParseSelect() - IF hUnion != NIL - hUnion[ "union_all" ] := lAll - ENDIF - ELSEIF ::IsKW( ::nPos, "INTERSECT" ) - ::nPos++ - ::EatKW( "SELECT" ) - hUnion := ::ParseSelect() - IF hUnion != NIL - hUnion[ "set_op" ] := "INTERSECT" - ENDIF - ELSEIF ::IsKW( ::nPos, "EXCEPT" ) - ::nPos++ - ::EatKW( "SELECT" ) - hUnion := ::ParseSelect() - IF hUnion != NIL - hUnion[ "set_op" ] := "EXCEPT" - ENDIF - ENDIF - h[ "union" ] := hUnion - -RETURN h - - -/* Parse column list */ -METHOD ParseColumnList() CLASS TSqlParser - - LOCAL aCols := {}, xExpr, cAlias - - DO WHILE .T. - xExpr := ::ParseExpr() - cAlias := "" - IF ::IsKW( ::nPos, "AS" ) - ::nPos++ - cAlias := ::TVal( ::nPos ) - ::nPos++ - ENDIF - AAdd( aCols, { xExpr, cAlias } ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - -RETURN aCols - - -/* Parse expression list (GROUP BY) */ -METHOD ParseExprList() CLASS TSqlParser - - LOCAL aList := {} - - DO WHILE ::TType( ::nPos ) == TK_NAME .AND. ; - ! ::IsKW( ::nPos, "HAVING" ) .AND. ! ::IsKW( ::nPos, "ORDER" ) .AND. ; - ! ::IsKW( ::nPos, "LIMIT" ) .AND. ! ::IsKW( ::nPos, "UNION" ) .AND. ; - ! ::IsKW( ::nPos, "INTERSECT" ) .AND. ! ::IsKW( ::nPos, "EXCEPT" ) - AAdd( aList, ::ParseExpr() ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - -RETURN aList - - -/* Parse FROM clause */ -METHOD ParseFrom( aTables, aJoins ) CLASS TSqlParser - - LOCAL cTable, cAlias, cJoinType, xOnCond, xSubQ - - /* Derived table: FROM (subquery) [AS] alias */ - IF ::TType( ::nPos ) == TK_LPAR .AND. ::IsKW( ::nPos + 1, "SELECT" ) - xSubQ := ::ParseSubquery() - cAlias := "" - IF ::IsKW( ::nPos, "AS" ) - ::nPos++ - ENDIF - IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) ) - cAlias := ::TVal( ::nPos ) - ::nPos++ - ENDIF - IF Empty( cAlias ) - cAlias := "__DRV1" - ENDIF - AAdd( aTables, { "__SUBQUERY__", cAlias, xSubQ } ) - ELSE - /* Primary table */ - cTable := ::TVal( ::nPos ) - ::nPos++ - cAlias := "" - IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) ) - cAlias := ::TVal( ::nPos ) - ::nPos++ - ENDIF - AAdd( aTables, { cTable, cAlias, "" } ) - ENDIF - - /* Additional comma-separated tables */ - DO WHILE ::TType( ::nPos ) == TK_COMMA - ::nPos++ - cTable := ::TVal( ::nPos ) ; ::nPos++ - cAlias := "" - IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) ) - cAlias := ::TVal( ::nPos ) ; ::nPos++ - ENDIF - AAdd( aTables, { cTable, cAlias, "" } ) - ENDDO - - /* Explicit JOIN clauses */ - DO WHILE ::IsKW( ::nPos, "JOIN" ) .OR. ::IsKW( ::nPos, "LEFT" ) .OR. ; - ::IsKW( ::nPos, "RIGHT" ) .OR. ::IsKW( ::nPos, "INNER" ) .OR. ; - ::IsKW( ::nPos, "CROSS" ) .OR. ::IsKW( ::nPos, "FULL" ) - cJoinType := ::TVal( ::nPos ) - ::nPos++ - IF ::IsKW( ::nPos, "OUTER" ) - ::nPos++ - ENDIF - IF ::IsKW( ::nPos, "JOIN" ) - ::nPos++ - ENDIF - - cTable := ::TVal( ::nPos ) ; ::nPos++ - cAlias := "" - IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) ) - cAlias := ::TVal( ::nPos ) ; ::nPos++ - ENDIF - AAdd( aTables, { cTable, cAlias, "" } ) - - xOnCond := NIL - IF ::IsKW( ::nPos, "ON" ) - ::nPos++ - xOnCond := ::ParseExpr() - ENDIF - - AAdd( aJoins, { cJoinType, cTable, cAlias, xOnCond } ) - ENDDO - -RETURN NIL - - -/* Parse ORDER BY clause */ -METHOD ParseOrderBy() CLASS TSqlParser - - LOCAL aOrder := {}, xExpr, cDir - - DO WHILE ::TType( ::nPos ) != TK_END .AND. ; - ! ::IsKW( ::nPos, "LIMIT" ) .AND. ! ::IsKW( ::nPos, "UNION" ) .AND. ; - ! ::IsKW( ::nPos, "INTERSECT" ) .AND. ! ::IsKW( ::nPos, "EXCEPT" ) - xExpr := ::ParseExpr() - cDir := "ASC" - IF ::IsKW( ::nPos, "ASC" ) - ::nPos++ - ELSEIF ::IsKW( ::nPos, "DESC" ) - cDir := "DESC" - ::nPos++ - ENDIF - AAdd( aOrder, { xExpr, cDir } ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - -RETURN aOrder - - -/* Parse INSERT INTO */ -METHOD ParseInsert() CLASS TSqlParser - - LOCAL h := { => }, cTable, aFields := {}, aValues := {}, xE - - h[ "type" ] := "INSERT" - ::EatKW( "INTO" ) - cTable := ::TVal( ::nPos ) ; ::nPos++ - h[ "table" ] := cTable - - /* Optional column list */ - IF ::TType( ::nPos ) == TK_LPAR - ::nPos++ - DO WHILE ::TType( ::nPos ) == TK_NAME - AAdd( aFields, ::TVal( ::nPos ) ) ; ::nPos++ - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - IF ::TType( ::nPos ) == TK_RPAR - ::nPos++ - ENDIF - ENDIF - h[ "fields" ] := aFields - - /* VALUES clause */ - IF ::IsKW( ::nPos, "VALUES" ) - ::nPos++ - IF ::TType( ::nPos ) == TK_LPAR - ::nPos++ - DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END - xE := ::ParseExpr() - AAdd( aValues, xE ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ENDIF - ENDDO - IF ::TType( ::nPos ) == TK_RPAR - ::nPos++ - ENDIF - ENDIF - ENDIF - h[ "values" ] := aValues - -RETURN h - - -/* Parse UPDATE */ -METHOD ParseUpdate() CLASS TSqlParser - - LOCAL h := { => }, cTable, aSet := {}, cCol, xVal - - h[ "type" ] := "UPDATE" - cTable := ::TVal( ::nPos ) ; ::nPos++ - h[ "table" ] := cTable - - IF ::IsKW( ::nPos, "SET" ) - ::nPos++ - DO WHILE ::TType( ::nPos ) == TK_NAME - cCol := ::TVal( ::nPos ) ; ::nPos++ - IF ::TType( ::nPos ) == TK_EQ - ::nPos++ - ENDIF - xVal := ::ParseExpr() - AAdd( aSet, { cCol, xVal } ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - ENDIF - h[ "set" ] := aSet - - h[ "where" ] := NIL - IF ::IsKW( ::nPos, "WHERE" ) - ::nPos++ - h[ "where" ] := ::ParseExpr() - ENDIF - -RETURN h - - -/* Parse DELETE FROM */ -METHOD ParseDelete() CLASS TSqlParser - - LOCAL h := { => } - - h[ "type" ] := "DELETE" - ::EatKW( "FROM" ) - h[ "table" ] := ::TVal( ::nPos ) ; ::nPos++ - - h[ "where" ] := NIL - IF ::IsKW( ::nPos, "WHERE" ) - ::nPos++ - h[ "where" ] := ::ParseExpr() - ENDIF - -RETURN h - - -/* Parse expression entry point */ -METHOD ParseExpr() CLASS TSqlParser -RETURN ::ParseOr() - - -/* Parse OR-level */ -METHOD ParseOr() CLASS TSqlParser - - LOCAL xL, xN - - xL := ::ParseAnd() - DO WHILE ::IsKW( ::nPos, "OR" ) - ::nPos++ - xN := ::ParseAnd() - xL := SqlNode( ND_BIN, "OR", xL, xN, NIL ) - ENDDO - -RETURN xL - - -/* Parse AND-level */ -METHOD ParseAnd() CLASS TSqlParser - - LOCAL xL, xN - - xL := ::ParseNot() - DO WHILE ::IsKW( ::nPos, "AND" ) - ::nPos++ - xN := ::ParseNot() - xL := SqlNode( ND_BIN, "AND", xL, xN, NIL ) - ENDDO - -RETURN xL - - -/* Parse NOT prefix */ -METHOD ParseNot() CLASS TSqlParser - - LOCAL xE - - IF ::IsKW( ::nPos, "NOT" ) - ::nPos++ - xE := ::ParseCompare() - RETURN SqlNode( ND_UNI, "NOT", xE, NIL, NIL ) - ENDIF - -RETURN ::ParseCompare() - - -/* Parse comparison-level */ -METHOD ParseCompare() CLASS TSqlParser - - LOCAL xL, xR, cOp, xLow, xHigh - LOCAL aList - LOCAL lNotIn, lNotBet - - xL := ::ParseAdd() - - /* Standard comparison operators */ - IF ::TType( ::nPos ) == TK_EQ .OR. ::TType( ::nPos ) == TK_NEQ .OR. ; - ::TType( ::nPos ) == TK_LT .OR. ::TType( ::nPos ) == TK_GT .OR. ; - ::TType( ::nPos ) == TK_LTE .OR. ::TType( ::nPos ) == TK_GTE - cOp := ::TVal( ::nPos ) - ::nPos++ - IF ::TType( ::nPos ) == TK_LPAR .AND. ::IsKW( ::nPos + 1, "SELECT" ) - xR := ::ParseSubquery() - ELSE - xR := ::ParseAdd() - ENDIF - RETURN SqlNode( ND_BIN, cOp, xL, xR, NIL ) - ENDIF - - /* LIKE [ESCAPE] */ - IF ::IsKW( ::nPos, "LIKE" ) - ::nPos++ - xR := ::ParseAdd() - IF ::IsKW( ::nPos, "ESCAPE" ) - ::nPos++ - RETURN SqlNode( ND_BIN, "LIKE", xL, xR, ::ParseAdd() ) - ENDIF - RETURN SqlNode( ND_BIN, "LIKE", xL, xR, NIL ) - ENDIF - - /* NOT LIKE [ESCAPE] */ - IF ::IsKW( ::nPos, "NOT" ) .AND. ::IsKW( ::nPos + 1, "LIKE" ) - ::nPos += 2 - xR := ::ParseAdd() - IF ::IsKW( ::nPos, "ESCAPE" ) - ::nPos++ - RETURN SqlNode( ND_UNI, "NOT", SqlNode( ND_BIN, "LIKE", xL, xR, ::ParseAdd() ), NIL, NIL ) - ENDIF - RETURN SqlNode( ND_UNI, "NOT", SqlNode( ND_BIN, "LIKE", xL, xR, NIL ), NIL, NIL ) - ENDIF - - /* IN / NOT IN */ - IF ::IsKW( ::nPos, "IN" ) .OR. ; - ( ::IsKW( ::nPos, "NOT" ) .AND. ::IsKW( ::nPos + 1, "IN" ) ) - lNotIn := .F. - IF ::IsKW( ::nPos, "NOT" ) - lNotIn := .T. - ::nPos++ - ENDIF - ::nPos++ - IF ::TType( ::nPos ) == TK_LPAR - IF ::IsKW( ::nPos + 1, "SELECT" ) - xR := ::ParseSubquery() - ELSE - ::nPos++ - aList := {} - DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END - AAdd( aList, ::ParseExpr() ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ENDIF - ENDDO - IF ::TType( ::nPos ) == TK_RPAR - ::nPos++ - ENDIF - xR := SqlNode( ND_LIST, aList, NIL, NIL, NIL ) - ENDIF - ENDIF - xL := SqlNode( ND_BIN, "IN", xL, xR, NIL ) - IF lNotIn - xL := SqlNode( ND_UNI, "NOT", xL, NIL, NIL ) - ENDIF - RETURN xL - ENDIF - - /* BETWEEN / NOT BETWEEN */ - IF ::IsKW( ::nPos, "BETWEEN" ) .OR. ; - ( ::IsKW( ::nPos, "NOT" ) .AND. ::IsKW( ::nPos + 1, "BETWEEN" ) ) - lNotBet := .F. - IF ::IsKW( ::nPos, "NOT" ) - lNotBet := .T. - ::nPos++ - ENDIF - ::nPos++ - xLow := ::ParseAdd() - ::EatKW( "AND" ) - xHigh := ::ParseAdd() - xL := SqlNode( ND_RANGE, "BETWEEN", xL, xLow, xHigh ) - IF lNotBet - xL := SqlNode( ND_UNI, "NOT", xL, NIL, NIL ) - ENDIF - RETURN xL - ENDIF - - /* IS [NOT] NULL */ - IF ::IsKW( ::nPos, "IS" ) - ::nPos++ - IF ::IsKW( ::nPos, "NOT" ) - ::nPos++ - ::EatKW( "NULL" ) - RETURN SqlNode( ND_BIN, "IS NOT NULL", xL, NIL, NIL ) - ENDIF - ::EatKW( "NULL" ) - RETURN SqlNode( ND_BIN, "IS NULL", xL, NIL, NIL ) - ENDIF - - /* COLLATE keyword (skip) */ - IF ::IsKW( ::nPos, "COLLATE" ) - ::nPos++ - IF ::TType( ::nPos ) == TK_NAME - ::nPos++ - ENDIF - ENDIF - -RETURN xL - - -/* Parse addition / subtraction / string concatenation */ -METHOD ParseAdd() CLASS TSqlParser - - LOCAL xL, cOp, xR - - xL := ::ParseMul() - DO WHILE ::TType( ::nPos ) == TK_PLUS .OR. ::TType( ::nPos ) == TK_MINUS .OR. ::TType( ::nPos ) == TK_PIPES - cOp := ::TVal( ::nPos ) - ::nPos++ - xR := ::ParseMul() - xL := SqlNode( ND_BIN, cOp, xL, xR, NIL ) - ENDDO - -RETURN xL - - -/* Parse multiplication / division */ -METHOD ParseMul() CLASS TSqlParser - - LOCAL xL, cOp, xR - - xL := ::ParseUnary() - DO WHILE ::TType( ::nPos ) == TK_STAR .OR. ::TType( ::nPos ) == TK_SLASH - cOp := ::TVal( ::nPos ) - ::nPos++ - xR := ::ParseUnary() - xL := SqlNode( ND_BIN, cOp, xL, xR, NIL ) - ENDDO - -RETURN xL - - -/* Parse unary minus */ -METHOD ParseUnary() CLASS TSqlParser - - LOCAL xE - - IF ::TType( ::nPos ) == TK_MINUS - ::nPos++ - xE := ::ParsePrimary() - RETURN SqlNode( ND_UNI, "-", xE, NIL, NIL ) - ENDIF - -RETURN ::ParsePrimary() - - -/* Parse primary expressions */ -METHOD ParsePrimary() CLASS TSqlParser - - LOCAL cVal, cName, xE, aArgs, aCases, xElse, xCond, xThen - - /* NULL literal */ - IF ::IsKW( ::nPos, "NULL" ) - ::nPos++ - RETURN SqlNode( ND_NIL, NIL, NIL, NIL, NIL ) - ENDIF - - /* Numeric literal */ - IF ::TType( ::nPos ) == TK_NUM - cVal := ::TVal( ::nPos ) - ::nPos++ - RETURN SqlNode( ND_LIT, Val( cVal ), NIL, NIL, NIL ) - ENDIF - - /* String literal */ - IF ::TType( ::nPos ) == TK_TEXT - cVal := ::TVal( ::nPos ) - ::nPos++ - RETURN SqlNode( ND_LIT, cVal, NIL, NIL, NIL ) - ENDIF - - /* Positional parameter */ - IF ::TType( ::nPos ) == TK_QMARK - ::nPos++ - RETURN SqlNode( ND_PAR, NIL, NIL, NIL, NIL ) - ENDIF - - /* Parenthesized expression or scalar subquery */ - IF ::TType( ::nPos ) == TK_LPAR - IF ::IsKW( ::nPos + 1, "SELECT" ) - RETURN ::ParseSubquery() - ENDIF - ::nPos++ - xE := ::ParseExpr() - IF ::TType( ::nPos ) == TK_RPAR - ::nPos++ - ENDIF - RETURN xE - ENDIF - - /* EXISTS (subquery) */ - IF ::IsKW( ::nPos, "EXISTS" ) - ::nPos++ - xE := ::ParseSubquery() - RETURN SqlNode( ND_FN, "EXISTS", { xE }, NIL, NIL ) - ENDIF - - /* CASE WHEN ... THEN ... [ELSE ...] END */ - IF ::IsKW( ::nPos, "CASE" ) - ::nPos++ - aCases := {} - xElse := NIL - DO WHILE ::IsKW( ::nPos, "WHEN" ) - ::nPos++ - xCond := ::ParseExpr() - ::EatKW( "THEN" ) - xThen := ::ParseExpr() - AAdd( aCases, { xCond, xThen } ) - ENDDO - IF ::IsKW( ::nPos, "ELSE" ) - ::nPos++ - xElse := ::ParseExpr() - ENDIF - ::EatKW( "END" ) - RETURN SqlNode( ND_CASE, aCases, xElse, NIL, NIL ) - ENDIF - - /* TIMESTAMP literal */ - IF ::IsKW( ::nPos, "TIMESTAMP" ) - IF ::TType( ::nPos + 1 ) == TK_TEXT - ::nPos++ - cVal := ::TVal( ::nPos ) - ::nPos++ - RETURN SqlNode( ND_FN, "TIMESTAMP", ; - { SqlNode( ND_LIT, cVal, NIL, NIL, NIL ) }, NIL, NIL ) - ENDIF - ENDIF - - /* Wildcard star */ - IF ::TType( ::nPos ) == TK_STAR - ::nPos++ - RETURN SqlNode( ND_COL, "*", NIL, NIL, NIL ) - ENDIF - - /* Identifier: column reference or function call */ - IF ::TType( ::nPos ) == TK_NAME - cName := ::TVal( ::nPos ) - ::nPos++ - - /* Qualified column: table.column */ - IF ::TType( ::nPos ) == TK_DOT - ::nPos++ - cName += "." + ::TVal( ::nPos ) - ::nPos++ - ENDIF - - /* Function call: name( args ) */ - IF ::TType( ::nPos ) == TK_LPAR - ::nPos++ - aArgs := {} - IF ::TType( ::nPos ) == TK_STAR - AAdd( aArgs, SqlNode( ND_COL, "*", NIL, NIL, NIL ) ) - ::nPos++ - ELSEIF ::TType( ::nPos ) != TK_RPAR - AAdd( aArgs, ::ParseExpr() ) - DO WHILE ::TType( ::nPos ) == TK_COMMA - ::nPos++ - AAdd( aArgs, ::ParseExpr() ) - ENDDO - ENDIF - IF ::TType( ::nPos ) == TK_RPAR - ::nPos++ - ENDIF - /* Window function: func(...) OVER (...) */ - IF ::IsKW( ::nPos, "OVER" ) - RETURN ::ParseWindow( cName, aArgs ) - ENDIF - RETURN SqlNode( ND_FN, cName, aArgs, NIL, NIL ) - ENDIF - - RETURN SqlNode( ND_COL, cName, NIL, NIL, NIL ) - ENDIF - - /* Fallback: skip unrecognized token */ - ::nPos++ - -RETURN SqlNode( ND_NIL, NIL, NIL, NIL, NIL ) - - -/* Parse a parenthesized subquery */ -METHOD ParseSubquery() CLASS TSqlParser - - LOCAL nDepth := 0, aSubTokens := {}, oSub, aParsed - - IF ::TType( ::nPos ) == TK_LPAR - ::nPos++ - ENDIF - - DO WHILE ::nPos <= Len( ::aTokens ) - IF ::TType( ::nPos ) == TK_LPAR - nDepth++ - ELSEIF ::TType( ::nPos ) == TK_RPAR - IF nDepth == 0 - ::nPos++ - EXIT - ENDIF - nDepth-- - ENDIF - AAdd( aSubTokens, ::aTokens[ ::nPos ] ) - ::nPos++ - ENDDO - AAdd( aSubTokens, { TK_END, "" } ) - - oSub := TSqlParser():New( aSubTokens, ::aParams ) - aParsed := oSub:Parse() - -RETURN SqlNode( ND_SUB, aParsed, NIL, NIL, NIL ) - - -/* Parse OVER(...) for window functions */ -METHOD ParseWindow( cFuncName, aFuncArgs ) CLASS TSqlParser - - LOCAL aPartBy := {}, aOrdBy := {}, xExpr, cDir - - ::nPos++ /* eat OVER */ - - IF ::TType( ::nPos ) == TK_LPAR - ::nPos++ /* eat ( */ - - /* PARTITION BY */ - IF ::IsKW( ::nPos, "PARTITION" ) - ::nPos++ - ::EatKW( "BY" ) - DO WHILE ::TType( ::nPos ) == TK_NAME .AND. ; - ! ::IsKW( ::nPos, "ORDER" ) .AND. ; - ::TType( ::nPos ) != TK_RPAR .AND. ; - ::TType( ::nPos ) != TK_END - AAdd( aPartBy, ::ParseExpr() ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - ENDIF - - /* ORDER BY */ - IF ::IsKW( ::nPos, "ORDER" ) - ::nPos++ - ::EatKW( "BY" ) - DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END - xExpr := ::ParseExpr() - cDir := "ASC" - IF ::IsKW( ::nPos, "ASC" ) - ::nPos++ - ELSEIF ::IsKW( ::nPos, "DESC" ) - cDir := "DESC" - ::nPos++ - ENDIF - AAdd( aOrdBy, { xExpr, cDir } ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - ENDIF - - IF ::TType( ::nPos ) == TK_RPAR - ::nPos++ /* eat ) */ - ENDIF - ENDIF - -RETURN SqlNode( ND_WINDOW, cFuncName, aFuncArgs, aPartBy, aOrdBy ) - - -/* Parse MERGE INTO ... USING ... ON ... WHEN MATCHED/NOT MATCHED */ -METHOD ParseMerge() CLASS TSqlParser - - LOCAL h := { => }, cTarget, cSource, cSrcAlias - LOCAL xOnCond, aUpdSet := {}, aInsFlds := {}, aInsVals := {} - LOCAL cCol, xVal - LOCAL lHasMatched := .F., lHasNotMatched := .F. - - h[ "type" ] := "MERGE" - ::nPos++ /* eat MERGE */ - ::EatKW( "INTO" ) - cTarget := ::TVal( ::nPos ) - ::nPos++ - h[ "target" ] := cTarget - - ::EatKW( "USING" ) - cSource := ::TVal( ::nPos ) - ::nPos++ - cSrcAlias := "" - IF ::IsKW( ::nPos, "AS" ) - ::nPos++ - ENDIF - IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsKW( ::nPos, "ON" ) - cSrcAlias := ::TVal( ::nPos ) - ::nPos++ - ENDIF - h[ "source" ] := cSource - h[ "source_alias" ] := cSrcAlias - - ::EatKW( "ON" ) - xOnCond := ::ParseExpr() - h[ "on" ] := xOnCond - - /* WHEN MATCHED THEN UPDATE SET ... */ - DO WHILE ::IsKW( ::nPos, "WHEN" ) - ::nPos++ /* eat WHEN */ - IF ::IsKW( ::nPos, "MATCHED" ) - ::nPos++ /* eat MATCHED */ - ::EatKW( "THEN" ) - ::EatKW( "UPDATE" ) - ::EatKW( "SET" ) - lHasMatched := .T. - DO WHILE ::TType( ::nPos ) == TK_NAME - cCol := ::TVal( ::nPos ) - ::nPos++ - /* skip table.col format */ - IF ::TType( ::nPos ) == TK_DOT - ::nPos++ - cCol := ::TVal( ::nPos ) - ::nPos++ - ENDIF - IF ::TType( ::nPos ) == TK_EQ - ::nPos++ - ENDIF - xVal := ::ParseExpr() - AAdd( aUpdSet, { cCol, xVal } ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - ELSEIF ::IsKW( ::nPos, "NOT" ) - ::nPos++ /* eat NOT */ - ::EatKW( "MATCHED" ) - ::EatKW( "THEN" ) - ::EatKW( "INSERT" ) - lHasNotMatched := .T. - /* Optional column list */ - IF ::TType( ::nPos ) == TK_LPAR - ::nPos++ - DO WHILE ::TType( ::nPos ) == TK_NAME - AAdd( aInsFlds, ::TVal( ::nPos ) ) - ::nPos++ - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ELSE - EXIT - ENDIF - ENDDO - IF ::TType( ::nPos ) == TK_RPAR - ::nPos++ - ENDIF - ENDIF - ::EatKW( "VALUES" ) - IF ::TType( ::nPos ) == TK_LPAR - ::nPos++ - DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END - AAdd( aInsVals, ::ParseExpr() ) - IF ::TType( ::nPos ) == TK_COMMA - ::nPos++ - ENDIF - ENDDO - IF ::TType( ::nPos ) == TK_RPAR - ::nPos++ - ENDIF - ENDIF - ELSE - EXIT - ENDIF - ENDDO - - h[ "has_matched" ] := lHasMatched - h[ "update_set" ] := aUpdSet - h[ "has_not_matched" ] := lHasNotMatched - h[ "insert_fields" ] := aInsFlds - h[ "insert_values" ] := aInsVals - -RETURN h diff --git a/_FiveSql2/test/bench_parser.prg b/_FiveSql2/test/bench_parser.prg deleted file mode 100644 index eba82f9..0000000 --- a/_FiveSql2/test/bench_parser.prg +++ /dev/null @@ -1,82 +0,0 @@ -/* - * bench_parser.prg — Parser performance benchmark - * Compares TSqlParser (original) vs TSqlParser2 (Pratt) - * - * Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun) - * All rights reserved. - */ - -#include "FiveSqlDef.ch" -#include "hbclass.ch" - -#define ITERS 10000 - -PROCEDURE Main() - - LOCAL aSQL, i, j, t0, t1, oLex, aTokens, oP, h - LOCAL nT1, nT2, aAllTokens - - aSQL := { ; - "SELECT name, salary FROM employees WHERE salary > 5000", ; - "SELECT e.name, o.product FROM employees e JOIN orders o ON e.id = o.emp_id WHERE o.amount > 100", ; - "SELECT dept, COUNT(*) AS cnt, AVG(salary) AS avg_sal FROM employees GROUP BY dept HAVING AVG(salary) > 5000 ORDER BY avg_sal DESC", ; - "SELECT name FROM employees WHERE id IN (SELECT emp_id FROM orders WHERE amount > 500)", ; - "SELECT name, salary, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn FROM employees", ; - "SELECT name, CASE WHEN salary > 7000 THEN 'High' WHEN salary > 5000 THEN 'Mid' ELSE 'Low' END AS tier FROM employees WHERE dept = 'Engineering' OR (salary > 6000 AND mgr_id IS NOT NULL)", ; - "INSERT INTO employees (id, name, dept, salary) VALUES (99, 'Test', 'QA', 5000)", ; - "UPDATE employees SET salary = salary * 1.1 + 500 WHERE dept = 'Sales' AND salary BETWEEN 4000 AND 6000" ; - } - - ? "================================================================" - ? " Parser Benchmark: TSqlParser vs TSqlParser2 (Pratt)" - ? " Queries: " + hb_ntos( Len( aSQL ) ) + " Iterations: " + hb_ntos( ITERS ) - ? "================================================================" - ? - - /* Pre-tokenize all queries */ - aAllTokens := {} - FOR i := 1 TO Len( aSQL ) - oLex := TSqlLexer():New( aSQL[ i ] ) - oLex:Tokenize() - AAdd( aAllTokens, oLex:GetTokens() ) - NEXT - - /* Benchmark TSqlParser (original) */ - t0 := hb_MilliSeconds() - FOR j := 1 TO ITERS - FOR i := 1 TO Len( aSQL ) - oP := TSqlParser():New( aAllTokens[ i ], {} ) - h := oP:Parse() - NEXT - NEXT - t1 := hb_MilliSeconds() - nT1 := t1 - t0 - ? " TSqlParser (original) : " + Str( nT1, 8 ) + " ms (" + ; - Str( nT1 * 1000 / ( ITERS * Len( aSQL ) ), 6, 1 ) + " us/parse)" - - /* Benchmark TSqlParser2 (Pratt) */ - t0 := hb_MilliSeconds() - FOR j := 1 TO ITERS - FOR i := 1 TO Len( aSQL ) - oP := TSqlParser2():New( aAllTokens[ i ], {} ) - h := oP:Parse() - NEXT - NEXT - t1 := hb_MilliSeconds() - nT2 := t1 - t0 - ? " TSqlParser2 (Pratt) : " + Str( nT2, 8 ) + " ms (" + ; - Str( nT2 * 1000 / ( ITERS * Len( aSQL ) ), 6, 1 ) + " us/parse)" - - ? - IF nT2 > 0 .AND. nT1 > 0 - IF nT1 > nT2 - ? " Pratt is " + Str( nT1 / nT2, 5, 2 ) + "x faster" - ELSEIF nT2 > nT1 - ? " Original is " + Str( nT2 / nT1, 5, 2 ) + "x faster" - ELSE - ? " Same speed" - ENDIF - ENDIF - ? "================================================================" - -RETURN diff --git a/_FiveSql2/test/test_parser_cmp.prg b/_FiveSql2/test/test_parser_cmp.prg deleted file mode 100644 index f384280..0000000 --- a/_FiveSql2/test/test_parser_cmp.prg +++ /dev/null @@ -1,98 +0,0 @@ -/* - * test_parser_cmp.prg — Compare AST output: TSqlParser vs TSqlParser2 (Pratt) - * - * Runs identical SQL through both parsers and verifies the AST hashes - * are structurally identical. - * - * Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun) - * All rights reserved. - */ - -#include "FiveSqlDef.ch" -#include "hbclass.ch" - -STATIC s_nPass := 0 -STATIC s_nFail := 0 - -PROCEDURE Main() - - ? "================================================================" - ? " TSqlParser vs TSqlParser2 (Pratt) — AST Comparison" - ? "================================================================" - ? - - Cmp( "Simple SELECT", "SELECT name, salary FROM employees WHERE salary > 5000" ) - Cmp( "SELECT *", "SELECT * FROM employees" ) - Cmp( "JOIN", "SELECT e.name, o.product FROM employees e JOIN orders o ON e.id = o.emp_id" ) - Cmp( "LEFT JOIN", "SELECT e.name, o.product FROM employees e LEFT JOIN orders o ON e.id = o.emp_id" ) - Cmp( "GROUP BY", "SELECT dept, COUNT(*) AS cnt FROM employees GROUP BY dept" ) - Cmp( "HAVING", "SELECT dept, AVG(salary) AS avg_sal FROM employees GROUP BY dept HAVING AVG(salary) > 6000" ) - Cmp( "ORDER BY DESC", "SELECT name, salary FROM employees ORDER BY salary DESC, name ASC" ) - Cmp( "DISTINCT + TOP", "SELECT DISTINCT TOP 5 dept FROM employees" ) - Cmp( "Subquery IN", "SELECT name FROM employees WHERE id IN (SELECT emp_id FROM orders)" ) - Cmp( "Subquery NOT IN", "SELECT name FROM employees WHERE id NOT IN (1, 2, 3)" ) - Cmp( "BETWEEN", "SELECT name FROM employees WHERE salary BETWEEN 5000 AND 8000" ) - Cmp( "NOT BETWEEN", "SELECT name FROM employees WHERE salary NOT BETWEEN 1000 AND 2000" ) - Cmp( "LIKE ESCAPE", "SELECT name FROM products WHERE name LIKE '%10!%%' ESCAPE '!'" ) - Cmp( "IS NULL", "SELECT name FROM employees WHERE mgr_id IS NULL" ) - Cmp( "IS NOT NULL", "SELECT name FROM employees WHERE mgr_id IS NOT NULL" ) - Cmp( "OR + AND", "SELECT * FROM employees WHERE dept = 'Sales' OR (salary > 7000 AND mgr_id = 0)" ) - Cmp( "NOT", "SELECT * FROM employees WHERE NOT (salary < 5000)" ) - Cmp( "Arithmetic", "SELECT name, salary * 12 + 1000 AS annual FROM employees" ) - Cmp( "Unary minus", "SELECT -salary FROM employees" ) - Cmp( "String concat", "SELECT name || ' - ' || dept FROM employees" ) - Cmp( "CASE WHEN", "SELECT name, CASE WHEN salary > 7000 THEN 'High' WHEN salary > 5000 THEN 'Mid' ELSE 'Low' END AS tier FROM employees" ) - Cmp( "Nested function", "SELECT UPPER(SUBSTR(name, 1, 3)) FROM employees" ) - Cmp( "COUNT(*)", "SELECT COUNT(*) FROM employees" ) - Cmp( "Window ROW_NUMBER", "SELECT name, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn FROM employees" ) - Cmp( "Window PARTITION", "SELECT name, RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk FROM employees" ) - Cmp( "UNION ALL", "SELECT name FROM employees WHERE dept = 'Sales' UNION ALL SELECT name FROM employees WHERE dept = 'HR'" ) - Cmp( "INSERT", "INSERT INTO employees (id, name) VALUES (99, 'Test')" ) - Cmp( "UPDATE", "UPDATE employees SET salary = 9999 WHERE id = 1" ) - Cmp( "DELETE", "DELETE FROM employees WHERE id = 99" ) - Cmp( "CTE simple", "WITH top_e AS (SELECT name FROM employees WHERE salary > 7000) SELECT * FROM top_e" ) - - ? - ? "================================================================" - ? " Pass: " + hb_ntos( s_nPass ) + "/" + hb_ntos( s_nPass + s_nFail ) - IF s_nFail == 0 - ? " ALL AST OUTPUTS IDENTICAL" - ENDIF - ? "================================================================" - -RETURN - - -STATIC FUNCTION Cmp( cLabel, cSQL ) - - LOCAL oLex, aTokens, oP1, oP2, h1, h2 - LOCAL cAST1, cAST2 - - /* Tokenize once */ - oLex := TSqlLexer():New( cSQL ) - oLex:Tokenize() - aTokens := oLex:GetTokens() - - /* Parse with TSqlParser (original) */ - oP1 := TSqlParser():New( AClone( aTokens ), {} ) - h1 := oP1:Parse() - - /* Parse with TSqlParser2 (Pratt) */ - oP2 := TSqlParser2():New( AClone( aTokens ), {} ) - h2 := oP2:Parse() - - /* Serialize both ASTs for comparison */ - cAST1 := hb_ValToExp( h1 ) - cAST2 := hb_ValToExp( h2 ) - - IF cAST1 == cAST2 - s_nPass++ - ? " PASS: " + cLabel - ELSE - s_nFail++ - ? " FAIL: " + cLabel - ? " P1: " + Left( cAST1, 120 ) - ? " P2: " + Left( cAST2, 120 ) - ENDIF - -RETURN NIL diff --git a/hbrt/class.go b/hbrt/class.go index e876547..fdcdd09 100644 --- a/hbrt/class.go +++ b/hbrt/class.go @@ -298,7 +298,14 @@ func (t *Thread) tryBinaryOp(op int) bool { if !a.IsObject() { return false } - cls := GetClass(a.AsArray().Class) + // AsArray can return nil if the ptr field is unset despite an object + // tag — a corrupted Value that would otherwise crash at `.Class`. + // Guard defensively; correct construction paths never hit this. + arr := a.AsArray() + if arr == nil { + return false + } + cls := GetClass(arr.Class) if cls == nil || cls.Operators[op] == nil { return false }