001package gudusoft.gsqlparser.parser; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TBaseType; 005import gudusoft.gsqlparser.TCustomLexer; 006import gudusoft.gsqlparser.TCustomParser; 007import gudusoft.gsqlparser.TCustomSqlStatement; 008import gudusoft.gsqlparser.TLexerOracle; 009import gudusoft.gsqlparser.TParserOracleSql; 010import gudusoft.gsqlparser.TParserOraclePLSql; 011import gudusoft.gsqlparser.TSourceToken; 012import gudusoft.gsqlparser.TSourceTokenList; 013import gudusoft.gsqlparser.TStatementList; 014import gudusoft.gsqlparser.TSyntaxError; 015import gudusoft.gsqlparser.EFindSqlStateType; 016import gudusoft.gsqlparser.ESqlPlusCmd; 017import gudusoft.gsqlparser.ETokenType; 018import gudusoft.gsqlparser.ETokenStatus; 019import gudusoft.gsqlparser.ESqlStatementType; 020import gudusoft.gsqlparser.EErrorType; 021import gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement; 022import gudusoft.gsqlparser.stmt.TUnknownSqlStatement; 023import gudusoft.gsqlparser.sqlcmds.ISqlCmds; 024import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory; 025import gudusoft.gsqlparser.compiler.TContext; 026import gudusoft.gsqlparser.sqlenv.TSQLEnv; 027import gudusoft.gsqlparser.compiler.TGlobalScope; 028import gudusoft.gsqlparser.compiler.TFrame; 029import gudusoft.gsqlparser.resolver.TSQLResolver; 030import gudusoft.gsqlparser.TLog; 031import gudusoft.gsqlparser.TGSqlParser; 032import gudusoft.gsqlparser.compiler.TASTEvaluator; 033import java.util.Stack; 034 035import java.io.BufferedReader; 036import java.util.ArrayList; 037import java.util.List; 038 039/** 040 * Oracle database SQL parser implementation. 041 * 042 * <p>This parser handles Oracle-specific SQL syntax including: 043 * <ul> 044 * <li>PL/SQL blocks (procedures, functions, packages, triggers)</li> 045 * <li>SQL*Plus commands (spool, set, show, etc.)</li> 046 * <li>Oracle-specific DML/DDL (MERGE, flashback, etc.)</li> 047 * <li>Oracle analytical functions and extensions</li> 048 * <li>Special token handling (INNER, NOT DEFERRABLE, etc.)</li> 049 * </ul> 050 * 051 * <p><b>Implementation Status:</b> PHASE 3 - IN PROGRESS 052 * <ul> 053 * <li><b>Completed:</b> Oracle classes (TLexerOracle, TParserOracleSql, TParserOraclePLSql) are now PUBLIC</li> 054 * <li><b>Current:</b> Skeleton implementation delegates to legacy TGSqlParser</li> 055 * <li><b>Next:</b> Extract vendor-specific logic from TGSqlParser into this class</li> 056 * <li><b>Goal:</b> Fully self-contained Oracle parser using AbstractSqlParser template</li> 057 * </ul> 058 * 059 * <p><b>Design Notes:</b> 060 * <ul> 061 * <li>Implements {@link SqlParser} directly (will extend {@link AbstractSqlParser} in Phase 4)</li> 062 * <li>Can now directly instantiate: {@link TLexerOracle}, {@link TParserOracleSql}, {@link TParserOraclePLSql}</li> 063 * <li>Uses two parsers: TParserOracleSql (SQL) + TParserOraclePLSql (PL/SQL blocks)</li> 064 * <li>Handles SQL*Plus commands via special tokenization logic</li> 065 * <li>Delimiter character: '/' for PL/SQL blocks, ';' for SQL statements</li> 066 * </ul> 067 * 068 * <p><b>Usage Example:</b> 069 * <pre> 070 * // Get Oracle parser from factory 071 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvoracle); 072 * 073 * // Build context 074 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvoracle) 075 * .sqlText("SELECT * FROM emp WHERE deptno = 10") 076 * .build(); 077 * 078 * // Parse 079 * SqlParseResult result = parser.parse(context); 080 * 081 * // Access statements 082 * TStatementList statements = result.getSqlStatements(); 083 * </pre> 084 * 085 * <p><b>Phase 3 Extraction Roadmap:</b> 086 * <ol> 087 * <li>✅ DONE: Make TLexerOracle, TParserOracleSql, TParserOraclePLSql public</li> 088 * <li>⏳ TODO: Extract tokenization logic (~367 lines from TGSqlParser.dooraclesqltexttotokenlist())</li> 089 * <li>⏳ TODO: Extract raw statement logic (~200 lines from TGSqlParser.dooraclegetrawsqlstatements())</li> 090 * <li>⏳ TODO: Extract parsing orchestration (SQL vs PL/SQL parser selection)</li> 091 * <li>⏳ TODO: Extract helper methods (getanewsourcetoken, getprevsolidtoken, etc.)</li> 092 * <li>⏳ TODO: Extend AbstractSqlParser and use template method pattern fully</li> 093 * <li>⏳ TODO: Remove all delegation to TGSqlParser</li> 094 * </ol> 095 * 096 * <p><b>Key Methods to Extract from TGSqlParser:</b> 097 * <ul> 098 * <li>{@code dooraclesqltexttotokenlist()} - Oracle tokenization with SQL*Plus command detection</li> 099 * <li>{@code dooraclegetrawsqlstatements()} - Oracle raw statement boundaries (handles PL/SQL blocks)</li> 100 * <li>{@code getanewsourcetoken()} - Token iterator from lexer</li> 101 * <li>{@code getprevsolidtoken()} - Navigate token list backwards</li> 102 * <li>{@code IsValidPlaceForDivToSqlplusCmd()} - Slash vs divide operator disambiguation</li> 103 * <li>{@code countLines()} - Multi-line token handling</li> 104 * <li>{@code spaceAtTheEndOfReturnToken()} - SQL*Plus command validation</li> 105 * </ul> 106 * 107 * @see SqlParser 108 * @see AbstractSqlParser 109 * @see TLexerOracle 110 * @see TParserOracleSql 111 * @see TParserOraclePLSql 112 * @since 3.2.0.0 113 */ 114public class OracleSqlParser extends AbstractSqlParser { 115 116 /** 117 * Construct Oracle SQL parser. 118 * <p> 119 * Configures the parser for Oracle database with default delimiters: 120 * <ul> 121 * <li>SQL statements: semicolon (;)</li> 122 * <li>PL/SQL blocks: forward slash (/)</li> 123 * </ul> 124 * <p> 125 * Following the original TGSqlParser pattern, the lexer and parsers are 126 * created once in the constructor and reused for all parsing operations. 127 * This avoids unnecessary object allocation overhead since the parser 128 * is not thread-safe and designed for single-use per instance. 129 */ 130 public OracleSqlParser() { 131 super(EDbVendor.dbvoracle); 132 this.delimiterChar = '/'; // PL/SQL delimiter 133 this.defaultDelimiterStr = ";"; // SQL delimiter 134 135 // Create lexer once - will be reused for all parsing operations 136 // (matches original TGSqlParser constructor pattern at line 1033) 137 this.flexer = new TLexerOracle(); 138 this.flexer.delimiterchar = this.delimiterChar; 139 this.flexer.defaultDelimiterStr = this.defaultDelimiterStr; 140 141 // Set parent's lexer reference for shared tokenization logic 142 this.lexer = this.flexer; 143 144 // Create parsers once - will be reused for all parsing operations 145 // Token list will be set/updated when parsing begins 146 // (matches original TGSqlParser constructor pattern at lines 1036-1040) 147 this.fparser = new TParserOracleSql(null); 148 this.fplsqlparser = new TParserOraclePLSql(null); 149 this.fparser.lexer = this.flexer; 150 this.fplsqlparser.lexer = this.flexer; 151 152 // NOTE: sourcetokenlist and sqlstatements are initialized in AbstractSqlParser constructor 153 } 154 155 // ========== Tokenization State (used during tokenization) ========== 156 // These instance variables are used during the tokenization process 157 // and are set up at the beginning of tokenization 158 159 /** The Oracle lexer used for tokenization */ 160 public TLexerOracle flexer; // Package-accessible for TGSqlParser integration 161 162 // NOTE: sourcetokenlist moved to AbstractSqlParser (inherited) 163 164 /** Optional callback for token processing (can be null) */ 165 private Object tokenHandle; // TTokenCallback interface - keeping as Object for now 166 167 // State variables for tokenization (set during dooraclesqltexttotokenlist()) 168 private boolean continuesqlplusatnewline; 169 private boolean waitingreturnforsemicolon; 170 private boolean waitingreturnforfloatdiv; 171 private boolean isvalidplace; 172 private boolean insqlpluscmd; 173 // Brace nesting inside CREATE JAVA SOURCE ... AS <java code>: the Java 174 // body contains its own semicolons, which must not end the SQL statement. 175 private int javaBraceNesting = 0; 176 177 // ========== Statement Parsing State (used during statement parsing) ========== 178 // These instance variables are used during the statement parsing process 179 180 // NOTE: The following fields moved to AbstractSqlParser (inherited): 181 // - sqlcmds (ISqlCmds) 182 // - sqlstatements (TStatementList) 183 // - parserContext (ParserContext) 184 185 /** Current statement being built */ 186 private TCustomSqlStatement gcurrentsqlstatement; 187 188 /** SQL parser (for regular SQL statements) */ 189 private TParserOracleSql fparser; 190 191 /** PL/SQL parser (for PL/SQL blocks) */ 192 private TParserOraclePLSql fplsqlparser; 193 194 // Note: Global context and frame stack fields inherited from AbstractSqlParser: 195 // - protected TContext globalContext 196 // - protected TSQLEnv sqlEnv 197 // - protected Stack<TFrame> frameStack 198 // - protected TFrame globalFrame 199 200 // ========== Enums for State Machine ========== 201 // These enums are used by the dooraclegetrawsqlstatements state machine 202 203 enum stored_procedure_status {start,is_as,body,bodyend,end, cursor_declare}; 204 enum stored_procedure_type {function,procedure,package_spec,package_body, block_with_begin,block_with_declare, 205 create_trigger,create_library,cursor_in_package_spec,others}; 206 207 static final int stored_procedure_nested_level = 1024; 208 209 // ========== AbstractSqlParser Abstract Methods Implementation ========== 210 211 /** 212 * Return the Oracle lexer instance. 213 * <p> 214 * The lexer is created once in the constructor and reused for all 215 * parsing operations. This method simply returns the existing instance, 216 * matching the original TGSqlParser pattern where the lexer is created 217 * once and reset before each use. 218 * 219 * @param context parser context (not used, lexer already created) 220 * @return the Oracle lexer instance created in constructor 221 */ 222 @Override 223 protected TCustomLexer getLexer(ParserContext context) { 224 // Return existing lexer instance (created in constructor) 225 // No need to create new instance - matches original TGSqlParser pattern 226 return this.flexer; 227 } 228 229 /** 230 * Return the Oracle SQL parser instance with updated token list. 231 * <p> 232 * The parser is created once in the constructor and reused for all 233 * parsing operations. This method updates the token list and returns 234 * the existing instance, matching the original TGSqlParser pattern. 235 * 236 * @param context parser context (not used, parser already created) 237 * @param tokens source token list to parse 238 * @return the Oracle SQL parser instance created in constructor 239 */ 240 @Override 241 protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) { 242 // Update token list for reused parser instance 243 this.fparser.sourcetokenlist = tokens; 244 return this.fparser; 245 } 246 247 /** 248 * Return the Oracle PL/SQL parser instance with updated token list. 249 * <p> 250 * Oracle needs a secondary parser (TParserOraclePLSql) for PL/SQL blocks 251 * (procedures, functions, packages, triggers, anonymous blocks). 252 * <p> 253 * The parser is created once in the constructor and reused for all 254 * parsing operations. This method updates the token list and returns 255 * the existing instance, matching the original TGSqlParser pattern. 256 * 257 * @param context parser context (not used, parser already created) 258 * @param tokens source token list to parse 259 * @return the Oracle PL/SQL parser instance created in constructor 260 */ 261 @Override 262 protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) { 263 // Update token list for reused parser instance 264 this.fplsqlparser.sourcetokenlist = tokens; 265 return this.fplsqlparser; 266 } 267 268 /** 269 * Call Oracle-specific tokenization logic. 270 * <p> 271 * Delegates to dooraclesqltexttotokenlist which handles Oracle's 272 * specific keyword recognition, SQL*Plus commands, forward slash 273 * disambiguation, and token generation. 274 */ 275 @Override 276 protected void tokenizeVendorSql() { 277 dooraclesqltexttotokenlist(); 278 restoreSqlSetStatements(); 279 } 280 281 /** 282 * SET is both a SQL*Plus command (SET PAGESIZE ..., line-terminated) and 283 * the head of the SQL statements SET CONSTRAINT[S] / SET ROLE / 284 * SET TRANSACTION (semicolon-terminated). Tokenization cannot look ahead 285 * (the token list is built incrementally), so every line-leading SET is 286 * first marked sqlpluscmd; a multi-line SET CONSTRAINTS was then truncated 287 * at the newline. This pass runs on the complete token list and restores 288 * the SQL reading when the word after SET is one of the SQL forms: the 289 * SET token gets its saved keyword code back and the rest of its line 290 * loses the insqlpluscmd flag, so the statement splitter routes it through 291 * the command table (sstoraclesetconstraint etc.) and runs to ';'. 292 */ 293 private void restoreSqlSetStatements() { 294 for (int i = 0; i < sourcetokenlist.size(); i++) { 295 TSourceToken st = sourcetokenlist.get(i); 296 if (st.tokencode != TBaseType.sqlpluscmd) continue; 297 if (!st.toString().equalsIgnoreCase("SET")) continue; 298 if (st.prevTokenCode != TBaseType.rrw_set) continue; 299 TSourceToken nx = st.nextSolidToken(); 300 if (nx == null) continue; 301 String w = nx.toString(); 302 if (!(w.equalsIgnoreCase("CONSTRAINT") || w.equalsIgnoreCase("CONSTRAINTS") 303 || w.equalsIgnoreCase("ROLE") || w.equalsIgnoreCase("TRANSACTION"))) continue; 304 st.tokencode = st.prevTokenCode; 305 st.tokentype = ETokenType.ttkeyword; 306 st.insqlpluscmd = false; 307 for (int j = i + 1; j < sourcetokenlist.size(); j++) { 308 TSourceToken t = sourcetokenlist.get(j); 309 if (t.tokencode == TBaseType.lexnewline) break; 310 t.insqlpluscmd = false; 311 } 312 } 313 } 314 315 /** 316 * Post-tokenization: merge ${...} template variable tokens into single IDENT tokens. 317 * Template syntax like ${if(len(X) == 0, "", "...")} is used by BI tools. 318 */ 319 @Override 320 protected void doAfterTokenize(TSourceTokenList tokens) { 321 super.doAfterTokenize(tokens); 322 mergeTemplateVariableTokens(tokens); 323 } 324 325 private void mergeTemplateVariableTokens(TSourceTokenList tokens) { 326 for (int i = 0; i < tokens.size() - 1; i++) { 327 TSourceToken dollar = tokens.get(i); 328 329 // Match either bare '$' (self-char) or '$IDENT' like $P, $X (identifier starting with $) 330 boolean isDollarChar = (dollar.tokencode == '$'); 331 boolean isDollarIdent = (dollar.tokencode == TBaseType.ident 332 && dollar.astext != null && dollar.astext.startsWith("$")); 333 if (!isDollarChar && !isDollarIdent) continue; 334 335 // Find next non-whitespace token — for $IDENT pattern, require immediate '{' (no whitespace) 336 int braceIdx = i + 1; 337 if (isDollarChar) { 338 while (braceIdx < tokens.size() && tokens.get(braceIdx).tokentype == ETokenType.ttwhitespace) { 339 braceIdx++; 340 } 341 } 342 if (braceIdx >= tokens.size() || tokens.get(braceIdx).tokencode != '{') continue; 343 344 // Found ${ pattern — find matching } with depth tracking 345 int depth = 1; 346 int endIdx = braceIdx + 1; 347 boolean isComplex = false; 348 while (endIdx < tokens.size() && depth > 0) { 349 int code = tokens.get(endIdx).tokencode; 350 if (code == '{') depth++; 351 else if (code == '}') depth--; 352 else if (code == '(' || code == ',' || code == '\'' || code == '"') isComplex = true; 353 if (depth > 0) endIdx++; 354 } 355 if (depth != 0) continue; // unclosed 356 357 // Build merged token text 358 StringBuilder sb = new StringBuilder(); 359 for (int j = i; j <= endIdx; j++) { 360 sb.append(tokens.get(j).astext); 361 } 362 363 if (isComplex && isDollarChar) { 364 // Complex template starting with bare $ like ${if(len(X)==0,...)} 365 // These expand to SQL fragments (e.g., AND clauses) at runtime, 366 // so convert to whitespace to let parser skip them entirely. 367 for (int j = i; j <= endIdx; j++) { 368 tokens.get(j).tokentype = ETokenType.ttwhitespace; 369 tokens.get(j).tokencode = TBaseType.lexspace; 370 } 371 } else { 372 // Simple template like ${NAME}, or JasperReports $P{VAR}/$X{IN,COL,PARAM} 373 // These expand to single values/expressions, so merge into IDENT placeholder. 374 dollar.astext = sb.toString(); 375 dollar.tokencode = TBaseType.ident; 376 dollar.tokentype = ETokenType.ttidentifier; 377 // Convert remaining tokens to whitespace so parser skips them even if 378 // tokenstatus is overwritten by statement splitter (tsignoredbygetrawstatement) 379 for (int j = i + 1; j <= endIdx; j++) { 380 tokens.get(j).tokentype = ETokenType.ttwhitespace; 381 tokens.get(j).tokencode = TBaseType.lexspace; 382 } 383 } 384 385 i = endIdx; // skip past merged tokens 386 } 387 } 388 389 /** 390 * Setup Oracle parsers for raw statement extraction. 391 * <p> 392 * Oracle uses dual parsers (SQL + PL/SQL), so we inject sqlcmds and 393 * update token lists for both parsers. 394 */ 395 @Override 396 protected void setupVendorParsersForExtraction() { 397 // Inject sqlcmds into BOTH parsers (SQL + PL/SQL) 398 this.fparser.sqlcmds = this.sqlcmds; 399 this.fplsqlparser.sqlcmds = this.sqlcmds; 400 401 // Update token list for BOTH parsers 402 this.fparser.sourcetokenlist = this.sourcetokenlist; 403 this.fplsqlparser.sourcetokenlist = this.sourcetokenlist; 404 } 405 406 /** 407 * Call Oracle-specific raw statement extraction logic. 408 * <p> 409 * Delegates to dooraclegetrawsqlstatements which handles Oracle's 410 * statement delimiters (semicolon and forward slash). 411 */ 412 @Override 413 protected void extractVendorRawStatements(SqlParseResult.Builder builder) { 414 dooraclegetrawsqlstatements(builder); 415 } 416 417 /** 418 * Perform full parsing of statements with syntax checking. 419 * <p> 420 * This method orchestrates the parsing of all statements by: 421 * <ul> 422 * <li>Using the raw statements passed from AbstractSqlParser.parse()</li> 423 * <li>Initializing SQL and PL/SQL parsers</li> 424 * <li>Creating global context and frame stack</li> 425 * <li>Looping through each raw statement</li> 426 * <li>Calling parsestatement() on each to build AST</li> 427 * <li>Handling error recovery for CREATE TABLE/INDEX</li> 428 * <li>Collecting syntax errors</li> 429 * </ul> 430 * 431 * <p><b>Important:</b> This method does NOT extract raw statements - they are 432 * passed in as a parameter already extracted by {@link #extractRawStatements}. 433 * This eliminates duplicate extraction that was occurring in the old design. 434 * 435 * <p>Extracted from: TGSqlParser.doparse() lines 16903-17026 436 * 437 * @param context parser context 438 * @param parser main SQL parser (TParserOracleSql) 439 * @param secondaryParser PL/SQL parser (TParserOraclePLSql) 440 * @param tokens source token list 441 * @param rawStatements raw statements already extracted (never null) 442 * @return list of fully parsed statements with AST built 443 */ 444 @Override 445 protected TStatementList performParsing(ParserContext context, 446 TCustomParser parser, 447 TCustomParser secondaryParser, 448 TSourceTokenList tokens, 449 TStatementList rawStatements) { 450 // Store references 451 this.fparser = (TParserOracleSql) parser; 452 this.fplsqlparser = (TParserOraclePLSql) secondaryParser; 453 this.sourcetokenlist = tokens; 454 this.parserContext = context; 455 456 // Use the raw statements passed from AbstractSqlParser.parse() 457 // (already extracted - DO NOT re-extract to avoid duplication) 458 this.sqlstatements = rawStatements; 459 460 // Initialize statement parsing infrastructure 461 this.sqlcmds = SqlCmdsFactory.get(vendor); 462 463 // Inject sqlcmds into parsers (required for make_stmt and other methods) 464 this.fparser.sqlcmds = this.sqlcmds; 465 this.fplsqlparser.sqlcmds = this.sqlcmds; 466 467 // Initialize global context for semantic analysis 468 // CRITICAL: When delegated from TGSqlParser, use TGSqlParser's frameStack 469 // so that variables set in statements can be found by other statements 470 if (context != null && context.getGsqlparser() != null) { 471 TGSqlParser gsqlparser = (TGSqlParser) context.getGsqlparser(); 472 this.frameStack = gsqlparser.getFrameStack(); 473 474 // CRITICAL: Set gsqlparser on the NodeFactory - matches TGSqlParser behavior 475 // This is needed for proper AST node creation during parsing 476 // Without this, expression traversal order may differ, causing 477 // dataflow constant ordering issues 478 this.fparser.getNf().setGsqlParser(gsqlparser); 479 this.fplsqlparser.getNf().setGsqlParser(gsqlparser); 480 481 // Create global context if needed 482 this.globalContext = new TContext(); 483 this.sqlEnv = new TSQLEnv(this.vendor) { 484 @Override 485 public void initSQLEnv() { 486 } 487 }; 488 this.globalContext.setSqlEnv(this.sqlEnv, this.sqlstatements); 489 } else { 490 initializeGlobalContext(); 491 } 492 493 // Parse each statement with exception handling for robustness 494 for (int i = 0; i < sqlstatements.size(); i++) { 495 TCustomSqlStatement stmt = sqlstatements.getRawSql(i); 496 497 try { 498 stmt.setFrameStack(frameStack); 499 500 // Parse the statement 501 int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree()); 502 503 // Oracle-specific post-processing (overridden hook method) 504 afterStatementParsed(stmt); 505 506 // Handle error recovery for CREATE TABLE/INDEX 507 boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE; 508 if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) { 509 handleCreateTableErrorRecovery(stmt); 510 } 511 512 // Collect syntax errors 513 if ((parseResult != 0) || (stmt.getErrorCount() > 0)) { 514 copyErrorsFromStatement(stmt); 515 } 516 517 } catch (Exception ex) { 518 // Use inherited exception handler from AbstractSqlParser 519 // This provides consistent error handling across all database parsers 520 handleStatementParsingException(stmt, i, ex); 521 continue; 522 } 523 } 524 525 // Clean up frame stack 526 if (globalFrame != null) { 527 globalFrame.popMeFromStack(frameStack); 528 } 529 530 return this.sqlstatements; 531 } 532 533 // Note: initializeGlobalContext() inherited from AbstractSqlParser 534 535 /** 536 * Override to provide Oracle-specific post-processing after statement parsing. 537 * <p> 538 * For Oracle, we check if the statement is PL/SQL and recursively find syntax 539 * errors in nested PL/SQL statements. 540 */ 541 @Override 542 protected void afterStatementParsed(TCustomSqlStatement stmt) { 543 if (stmt.isoracleplsql()) { 544 findAllSyntaxErrorsInPlsql(stmt); 545 } 546 } 547 548 /** 549 * Perform Oracle-specific semantic analysis using TSQLResolver. 550 * 551 * <p>This includes: 552 * <ul> 553 * <li>Column-to-table resolution</li> 554 * <li>Dataflow analysis</li> 555 * <li>Reference resolution</li> 556 * <li>Scope resolution</li> 557 * </ul> 558 * 559 * @param context the parser context 560 * @param statements the parsed statements 561 */ 562 @Override 563 protected void performSemanticAnalysis(ParserContext context, TStatementList statements) { 564 if (TBaseType.isEnableResolver() && getSyntaxErrors().isEmpty()) { 565 TSQLResolver resolver = new TSQLResolver(globalContext, statements); 566 resolver.resolve(); 567 } 568 } 569 570 /** 571 * Perform Oracle-specific AST interpretation/evaluation using TASTEvaluator. 572 * 573 * <p>This executes simple SQL statements and evaluates expressions 574 * for static analysis and constant folding. 575 * 576 * @param context the parser context 577 * @param statements the parsed statements 578 */ 579 @Override 580 protected void performInterpreter(ParserContext context, TStatementList statements) { 581 if (TBaseType.ENABLE_INTERPRETER && getSyntaxErrors().isEmpty()) { 582 TLog.clearLogs(); 583 TGlobalScope interpreterScope = new TGlobalScope(sqlEnv); 584 TLog.enableInterpreterLogOnly(); 585 TASTEvaluator astEvaluator = new TASTEvaluator(statements, interpreterScope); 586 astEvaluator.eval(); 587 } 588 } 589 590 // ========== Raw Statement Extraction ========== 591 // These methods extract raw SQL statements from tokens without full parsing 592 // Extracted from TGSqlParser.dooraclegetrawsqlstatements() and related methods 593 594 /** 595 * Extract raw Oracle SQL statements from tokenized source. 596 * <p> 597 * This is the main Oracle statement extraction state machine that: 598 * <ul> 599 * <li>Groups tokens into statement boundaries</li> 600 * <li>Identifies statement types (SQL vs PL/SQL, SQL*Plus commands)</li> 601 * <li>Handles nested PL/SQL blocks (procedures, functions, packages, triggers)</li> 602 * <li>Tracks BEGIN/END pairs and other block delimiters</li> 603 * <li>Detects statement terminators (semicolon, forward slash, period)</li> 604 * </ul> 605 * 606 * <p><b>State Machine:</b> Uses 4 main states: 607 * <ul> 608 * <li>{@code stnormal} - Between statements, looking for start of next statement</li> 609 * <li>{@code stsql} - Inside a SQL statement</li> 610 * <li>{@code stsqlplus} - Inside a SQL*Plus command</li> 611 * <li>{@code ststoredprocedure} - Inside a PL/SQL block (procedure/function/package/trigger)</li> 612 * <li>{@code sterror} - Error recovery mode</li> 613 * </ul> 614 * 615 * <p><b>Extracted from:</b> TGSqlParser.dooraclegetrawsqlstatements() (lines 10071-10859) 616 * 617 * <p><b>Design Note:</b> This method now receives a builder to populate with results, 618 * following Option A design where the vendor-specific method focuses on parsing logic 619 * while extractRawStatements() handles result construction. 620 * 621 * @param builder the result builder to populate with statements and error information 622 */ 623 private void dooraclegetrawsqlstatements(SqlParseResult.Builder builder) { 624 int waitingEnds[] = new int[stored_procedure_nested_level]; 625 stored_procedure_type sptype[] = new stored_procedure_type[stored_procedure_nested_level]; 626 stored_procedure_status procedure_status[] = new stored_procedure_status[stored_procedure_nested_level]; 627 boolean endBySlashOnly = true; 628 int nestedProcedures = 0, nestedParenthesis = 0; 629 // Flag for CREATE MLE MODULE with AS clause - terminates with / not ; 630 boolean mleModuleWithAs = false; 631 // Flag for WITH FUNCTION/PROCEDURE - track BEGIN/END nesting to handle embedded semicolons 632 boolean withPlsqlDefinition = false; 633 int withPlsqlBeginEndNesting = 0; 634 boolean withPlsqlFoundSelect = false; // True when SELECT has been found after WITH FUNCTION 635 // Track whether the current CTE statement's main SELECT has been found 636 // (i.e., the SELECT after WITH name AS (...) at paren level 0) 637 boolean cteMainSelectFound = false; 638 639 if (TBaseType.assigned(sqlstatements)) sqlstatements.clear(); 640 if (!TBaseType.assigned(sourcetokenlist)) { 641 // No tokens available - populate builder with error and return 642 builder.errorCode(1); 643 builder.errorMessage("No source token list available"); 644 builder.sqlStatements(new TStatementList()); 645 return; 646 } 647 648 gcurrentsqlstatement = null; 649 EFindSqlStateType gst = EFindSqlStateType.stnormal; 650 TSourceToken lcprevsolidtoken = null, ast = null; 651 652 // Main tokenization loop 653 for (int i = 0; i < sourcetokenlist.size(); i++) { 654 655 if ((ast != null) && (ast.issolidtoken())) 656 lcprevsolidtoken = ast; 657 658 ast = sourcetokenlist.get(i); 659 sourcetokenlist.curpos = i; 660 661 // Token-specific keyword transformations for Oracle 662 performRawStatementTokenTransformations(ast); 663 664 // State machine processing 665 switch (gst) { 666 case sterror: { 667 if (ast.tokentype == ETokenType.ttsemicolon) { 668 appendToken(gcurrentsqlstatement, ast); 669 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 670 gst = EFindSqlStateType.stnormal; 671 } else { 672 appendToken(gcurrentsqlstatement, ast); 673 } 674 break; 675 } //sterror 676 677 case stnormal: { 678 if ((ast.tokencode == TBaseType.cmtdoublehyphen) 679 || (ast.tokencode == TBaseType.cmtslashstar) 680 || (ast.tokencode == TBaseType.lexspace) 681 || (ast.tokencode == TBaseType.lexnewline) 682 || (ast.tokentype == ETokenType.ttsemicolon)) { 683 if (gcurrentsqlstatement != null) { 684 appendToken(gcurrentsqlstatement, ast); 685 } 686 687 if ((lcprevsolidtoken != null) && (ast.tokentype == ETokenType.ttsemicolon)) { 688 if (lcprevsolidtoken.tokentype == ETokenType.ttsemicolon) { 689 // ;;;; continuous semicolon, treat it as comment 690 ast.tokentype = ETokenType.ttsimplecomment; 691 ast.tokencode = TBaseType.cmtdoublehyphen; 692 } 693 } 694 695 continue; 696 } 697 698 if (ast.tokencode == TBaseType.sqlpluscmd) { 699 gst = EFindSqlStateType.stsqlplus; 700 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 701 appendToken(gcurrentsqlstatement, ast); 702 continue; 703 } 704 705 // find a token to start sql or plsql mode 706 gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement); 707 708 if (gcurrentsqlstatement != null) { 709 if (gcurrentsqlstatement.isoracleplsql()) { 710 nestedProcedures = 0; 711 gst = EFindSqlStateType.ststoredprocedure; 712 appendToken(gcurrentsqlstatement, ast); 713 714 switch (gcurrentsqlstatement.sqlstatementtype) { 715 case sstplsql_createprocedure: 716 sptype[nestedProcedures] = stored_procedure_type.procedure; 717 break; 718 case sstplsql_createfunction: 719 sptype[nestedProcedures] = stored_procedure_type.function; 720 break; 721 case sstplsql_createpackage: 722 sptype[nestedProcedures] = stored_procedure_type.package_spec; 723 if (ast.searchToken(TBaseType.rrw_body, 5) != null) { 724 sptype[nestedProcedures] = stored_procedure_type.package_body; 725 } 726 break; 727 case sst_plsql_block: 728 sptype[nestedProcedures] = stored_procedure_type.block_with_declare; 729 if (ast.tokencode == TBaseType.rrw_begin) { 730 sptype[nestedProcedures] = stored_procedure_type.block_with_begin; 731 } 732 break; 733 case sstplsql_createtrigger: 734 sptype[nestedProcedures] = stored_procedure_type.create_trigger; 735 break; 736 case sstoraclecreatelibrary: 737 sptype[nestedProcedures] = stored_procedure_type.create_library; 738 break; 739 case sstplsql_createtype_placeholder: 740 gst = EFindSqlStateType.stsql; 741 break; 742 default: 743 sptype[nestedProcedures] = stored_procedure_type.others; 744 break; 745 } 746 747 if (sptype[0] == stored_procedure_type.block_with_declare) { 748 endBySlashOnly = false; 749 procedure_status[0] = stored_procedure_status.is_as; 750 } else if (sptype[0] == stored_procedure_type.block_with_begin) { 751 endBySlashOnly = false; 752 procedure_status[0] = stored_procedure_status.body; 753 } else if (sptype[0] == stored_procedure_type.procedure) { 754 endBySlashOnly = false; 755 procedure_status[0] = stored_procedure_status.start; 756 } else if (sptype[0] == stored_procedure_type.function) { 757 endBySlashOnly = false; 758 procedure_status[0] = stored_procedure_status.start; 759 } else if (sptype[0] == stored_procedure_type.package_spec) { 760 endBySlashOnly = false; 761 procedure_status[0] = stored_procedure_status.start; 762 } else if (sptype[0] == stored_procedure_type.package_body) { 763 endBySlashOnly = false; 764 procedure_status[0] = stored_procedure_status.start; 765 } else if (sptype[0] == stored_procedure_type.create_trigger) { 766 endBySlashOnly = false; 767 procedure_status[0] = stored_procedure_status.start; 768 } else if (sptype[0] == stored_procedure_type.create_library) { 769 endBySlashOnly = false; 770 procedure_status[0] = stored_procedure_status.bodyend; 771 } else { 772 endBySlashOnly = true; 773 procedure_status[0] = stored_procedure_status.bodyend; 774 } 775 776 if ((ast.tokencode == TBaseType.rrw_begin) 777 || (ast.tokencode == TBaseType.rrw_package) 778 || (ast.searchToken(TBaseType.rrw_package, 4) != null)) { 779 waitingEnds[nestedProcedures] = 1; 780 } 781 } else { 782 gst = EFindSqlStateType.stsql; 783 appendToken(gcurrentsqlstatement, ast); 784 nestedParenthesis = 0; 785 // Check if this is CREATE MLE MODULE with AS clause (JavaScript code) 786 // If AS is found after LANGUAGE JAVASCRIPT, it terminates with / not ; 787 if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstoraclecreatemlemodule) { 788 // Look ahead to see if there's an AS keyword 789 TSourceToken asToken = ast.searchToken(TBaseType.rrw_as, 10); 790 mleModuleWithAs = (asToken != null); 791 } else { 792 mleModuleWithAs = false; 793 } 794 795 // Check if this is WITH FUNCTION/PROCEDURE (Oracle 12c inline PL/SQL) 796 // Need to track BEGIN/END nesting to handle embedded semicolons 797 if (ast.tokencode == TBaseType.rrw_with && gcurrentsqlstatement.isctequery) { 798 // Look ahead for FUNCTION or PROCEDURE keyword 799 TSourceToken nextSolid = ast.nextSolidToken(); 800 if (nextSolid != null && (nextSolid.tokencode == TBaseType.rrw_function 801 || nextSolid.tokencode == TBaseType.rrw_procedure)) { 802 withPlsqlDefinition = true; 803 withPlsqlBeginEndNesting = 0; 804 } 805 } 806 } 807 } else { 808 //error token found 809 this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo) 810 , "Error when tokenize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist)); 811 812 ast.tokentype = ETokenType.tttokenlizererrortoken; 813 gst = EFindSqlStateType.sterror; 814 815 gcurrentsqlstatement = new TUnknownSqlStatement(vendor); 816 gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid; 817 appendToken(gcurrentsqlstatement, ast); 818 } 819 820 break; 821 } // stnormal 822 823 case stsqlplus: { 824 if (ast.insqlpluscmd) { 825 appendToken(gcurrentsqlstatement, ast); 826 } else { 827 gst = EFindSqlStateType.stnormal; //this token must be newline, 828 appendToken(gcurrentsqlstatement, ast); // so add it here 829 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 830 } 831 832 break; 833 }//case stsqlplus 834 835 case stsql: { 836 // For WITH FUNCTION/PROCEDURE, track BEGIN/END nesting and when SELECT is found 837 if (withPlsqlDefinition) { 838 if (ast.tokencode == TBaseType.rrw_begin) { 839 withPlsqlBeginEndNesting++; 840 } else if (ast.tokencode == TBaseType.rrw_end) { 841 withPlsqlBeginEndNesting--; 842 if (withPlsqlBeginEndNesting < 0) withPlsqlBeginEndNesting = 0; 843 } else if (ast.tokencode == TBaseType.rrw_select && withPlsqlBeginEndNesting == 0) { 844 // Found SELECT after all function definitions are done 845 withPlsqlFoundSelect = true; 846 } 847 } 848 849 // For CREATE MLE MODULE with AS clause, don't terminate on semicolon 850 // The JavaScript code may contain semicolons; wait for / to terminate 851 // For WITH FUNCTION/PROCEDURE, don't terminate on semicolon until SELECT is found 852 // (the semicolons in function body and after END are part of the function definition) 853 // CREATE JAVA SOURCE ... AS <java code>: semicolons inside 854 // the Java class body (brace depth > 0) are Java statement 855 // terminators, not SQL statement terminators. 856 if ((gcurrentsqlstatement != null) 857 && (gcurrentsqlstatement.sqlstatementtype == gudusoft.gsqlparser.ESqlStatementType.sstoraclecreatejava)) { 858 if ("{".equals(ast.toString())) { 859 javaBraceNesting++; 860 } else if ("}".equals(ast.toString()) && (javaBraceNesting > 0)) { 861 javaBraceNesting--; 862 } 863 } 864 865 boolean skipSemicolonTermination = mleModuleWithAs || (javaBraceNesting > 0) || (withPlsqlDefinition && !withPlsqlFoundSelect); 866 if (ast.tokentype == ETokenType.ttsemicolon && !skipSemicolonTermination) { 867 gst = EFindSqlStateType.stnormal; 868 appendToken(gcurrentsqlstatement, ast); 869 gcurrentsqlstatement.semicolonended = ast; 870 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 871 mleModuleWithAs = false; // Reset flag 872 javaBraceNesting = 0; 873 withPlsqlDefinition = false; // Reset WITH FUNCTION flag 874 withPlsqlBeginEndNesting = 0; 875 cteMainSelectFound = false; 876 withPlsqlFoundSelect = false; 877 continue; 878 } 879 880 if (sourcetokenlist.sqlplusaftercurtoken()) //most probably is / cmd 881 { 882 gst = EFindSqlStateType.stnormal; 883 appendToken(gcurrentsqlstatement, ast); 884 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 885 mleModuleWithAs = false; // Reset flag 886 javaBraceNesting = 0; 887 continue; 888 } 889 890 if (ast.tokencode == '(') nestedParenthesis++; 891 if (ast.tokencode == ')') { 892 nestedParenthesis--; 893 if (nestedParenthesis < 0) nestedParenthesis = 0; 894 } 895 896 Boolean findNewStmt = false; 897 TCustomSqlStatement lcStmt = null; 898 // Check for new statement: CREATE TABLE (original), or SELECT inside a non-CTE SELECT 899 boolean shouldCheckNewStmt = false; 900 if ((nestedParenthesis == 0) && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatetable)) { 901 shouldCheckNewStmt = true; 902 } else if ((nestedParenthesis == 0) && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstselect) 903 && (ast.tokencode == TBaseType.rrw_select || ast.tokencode == TBaseType.rrw_with)) { 904 // Check if current statement is a CTE (starts with WITH) 905 boolean isCteContext = false; 906 for (int si = 0; si < gcurrentsqlstatement.sourcetokenlist.size(); si++) { 907 TSourceToken st = gcurrentsqlstatement.sourcetokenlist.get(si); 908 if (st.tokentype == ETokenType.ttwhitespace || st.tokentype == ETokenType.ttreturn 909 || st.tokencode == TBaseType.cmtdoublehyphen || st.tokencode == TBaseType.cmtslashstar) { 910 continue; 911 } 912 if (st.tokencode == TBaseType.rrw_with) { 913 isCteContext = true; 914 } 915 break; 916 } 917 // Don't split if previous token makes this SELECT part of current statement: 918 // - Set operators: UNION, INTERSECT, MINUS, EXCEPT, ALL 919 // - Left paren: (SELECT ...) — SELECT is main query of parenthesized expr 920 boolean suppressSplit = false; 921 if (ast.tokencode == TBaseType.rrw_select && lcprevsolidtoken != null) { 922 int prevCode = lcprevsolidtoken.tokencode; 923 if (prevCode == TBaseType.rrw_union || prevCode == TBaseType.rrw_intersect 924 || prevCode == TBaseType.rrw_minus || prevCode == TBaseType.rrw_except 925 || prevCode == TBaseType.rrw_all 926 || prevCode == '(') { 927 suppressSplit = true; 928 } 929 } 930 if (suppressSplit) { 931 // SELECT is part of current statement — don't split 932 } else if (!isCteContext) { 933 // Non-CTE SELECT: any SELECT/WITH at paren level 0 starts a new statement 934 shouldCheckNewStmt = true; 935 } else if (cteMainSelectFound) { 936 // CTE context: main SELECT already consumed, so this SELECT/WITH 937 // at paren level 0 is a new statement 938 shouldCheckNewStmt = true; 939 } else if (ast.tokencode == TBaseType.rrw_select) { 940 // CTE context: this is the main SELECT after WITH name AS (...) 941 cteMainSelectFound = true; 942 // Don't split — this SELECT is part of the CTE statement 943 } 944 // If ast is WITH and main SELECT not yet found, it could be another 945 // CTE definition (WITH a AS (...), b AS (...)) — don't split 946 } 947 if (shouldCheckNewStmt) { 948 // For SELECT-after-SELECT/WITH splitting, use stnormal so issql can detect CTE starts. 949 // For CREATE TABLE, preserve original stsql state to avoid false positives 950 // (e.g., INSERT/DELETE keywords in blockchain table clauses). 951 EFindSqlStateType issqlState = (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstselect) 952 ? EFindSqlStateType.stnormal : gst; 953 lcStmt = sqlcmds.issql(ast, issqlState, gcurrentsqlstatement); 954 if (lcStmt != null) { 955 findNewStmt = true; 956 if (lcStmt.sqlstatementtype == ESqlStatementType.sstselect) { 957 TSourceToken prevst = ast.prevSolidToken(); 958 if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatetable) { 959 // For CREATE TABLE, suppress split when SELECT follows AS/(/): AS (SELECT ...) 960 if ((prevst.tokencode == TBaseType.rrw_as) || (prevst.tokencode == '(') || (prevst.tokencode == ')')) { 961 findNewStmt = false; 962 } 963 } 964 // For SELECT-after-SELECT/WITH splitting at paren level 0, 965 // no suppression needed — the new SELECT/WITH is a new statement 966 } 967 } 968 } 969 970 if (findNewStmt) { 971 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 972 gcurrentsqlstatement = lcStmt; 973 cteMainSelectFound = false; // Reset for new statement 974 nestedParenthesis = 0; // Reset paren tracking for new statement 975 appendToken(gcurrentsqlstatement, ast); 976 continue; 977 } else 978 appendToken(gcurrentsqlstatement, ast); 979 980 break; 981 }//case stsql 982 983 case ststoredprocedure: { 984 985 if (procedure_status[nestedProcedures] != stored_procedure_status.bodyend) { 986 appendToken(gcurrentsqlstatement, ast); 987 } 988 989 switch (procedure_status[nestedProcedures]) { 990 case cursor_declare: 991 if (ast.tokencode == ';') { 992 nestedProcedures--; 993 if (nestedProcedures < 0) { 994 nestedProcedures = 0; 995 } 996 } 997 break; 998 case start: 999 if ((ast.tokencode == TBaseType.rrw_as) || (ast.tokencode == TBaseType.rrw_is)) { 1000 if (sptype[nestedProcedures] != stored_procedure_type.create_trigger) { 1001 if ((sptype[0] == stored_procedure_type.package_spec) && (nestedProcedures > 0)) { 1002 // when it's a package specification, only top level accept as/is 1003 } else { 1004 procedure_status[nestedProcedures] = stored_procedure_status.is_as; 1005 if (ast.searchToken("language", 1) != null) { 1006 if (nestedProcedures == 0) { 1007 gst = EFindSqlStateType.stsql; 1008 } else { 1009 procedure_status[nestedProcedures] = stored_procedure_status.body; 1010 nestedProcedures--; 1011 } 1012 } 1013 } 1014 } 1015 } else if (ast.tokencode == TBaseType.rrw_begin) { 1016 if (sptype[nestedProcedures] == stored_procedure_type.create_trigger) { 1017 waitingEnds[nestedProcedures]++; 1018 } 1019 if (nestedProcedures > 0) { 1020 nestedProcedures--; 1021 } 1022 procedure_status[nestedProcedures] = stored_procedure_status.body; 1023 } else if (ast.tokencode == TBaseType.rrw_end) { 1024 if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures - 1] == 1) 1025 && ((sptype[nestedProcedures - 1] == stored_procedure_type.package_body) 1026 || (sptype[nestedProcedures - 1] == stored_procedure_type.package_spec))) { 1027 nestedProcedures--; 1028 procedure_status[nestedProcedures] = stored_procedure_status.bodyend; 1029 } 1030 } else if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) { 1031 if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures] == 0) 1032 && (procedure_status[nestedProcedures - 1] == stored_procedure_status.is_as)) { 1033 nestedProcedures--; 1034 nestedProcedures++; 1035 waitingEnds[nestedProcedures] = 0; 1036 procedure_status[nestedProcedures] = stored_procedure_status.start; 1037 } 1038 } else if (ast.tokencode == TBaseType.rrw_oracle_cursor) { 1039 if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures] == 0) 1040 && (procedure_status[nestedProcedures - 1] == stored_procedure_status.is_as)) { 1041 nestedProcedures--; 1042 nestedProcedures++; 1043 waitingEnds[nestedProcedures] = 0; 1044 procedure_status[nestedProcedures] = stored_procedure_status.cursor_declare; 1045 } 1046 } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger) && (ast.tokencode == TBaseType.rrw_declare)) { 1047 procedure_status[nestedProcedures] = stored_procedure_status.is_as; 1048 } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger) 1049 && (ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) { 1050 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 1051 gst = EFindSqlStateType.stnormal; 1052 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1053 1054 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 1055 appendToken(gcurrentsqlstatement, ast); 1056 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1057 } else if (sptype[nestedProcedures] == stored_procedure_type.create_trigger) { 1058 if (ast.tokencode == TBaseType.rrw_trigger) { 1059 TSourceToken compoundSt = ast.searchToken(TBaseType.rrw_oracle_compound, -1); 1060 if (compoundSt != null) { 1061 procedure_status[nestedProcedures] = stored_procedure_status.body; 1062 waitingEnds[nestedProcedures]++; 1063 } 1064 } 1065 } else if ((sptype[nestedProcedures] == stored_procedure_type.function) 1066 && (ast.tokencode == TBaseType.rrw_teradata_using)) { 1067 if ((ast.searchToken("aggregate", -1) != null) || (ast.searchToken("pipelined", -1) != null)) { 1068 if (nestedProcedures == 0) { 1069 gst = EFindSqlStateType.stsql; 1070 } else { 1071 procedure_status[nestedProcedures] = stored_procedure_status.body; 1072 nestedProcedures--; 1073 } 1074 } 1075 } 1076 break; 1077 case is_as: 1078 if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) { 1079 nestedProcedures++; 1080 if (nestedProcedures > stored_procedure_nested_level - 1) { 1081 gst = EFindSqlStateType.sterror; 1082 nestedProcedures--; 1083 } else { 1084 waitingEnds[nestedProcedures] = 0; 1085 procedure_status[nestedProcedures] = stored_procedure_status.start; 1086 } 1087 } else if (ast.tokencode == TBaseType.rrw_begin) { 1088 if ((nestedProcedures == 0) && 1089 ((sptype[nestedProcedures] == stored_procedure_type.package_body) 1090 || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) { 1091 // top level package begin already counted 1092 } else { 1093 waitingEnds[nestedProcedures]++; 1094 } 1095 procedure_status[nestedProcedures] = stored_procedure_status.body; 1096 } else if (ast.tokencode == TBaseType.rrw_end) { 1097 if ((nestedProcedures == 0) && (waitingEnds[nestedProcedures] == 1) 1098 && ((sptype[nestedProcedures] == stored_procedure_type.package_body) 1099 || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) { 1100 procedure_status[nestedProcedures] = stored_procedure_status.bodyend; 1101 waitingEnds[nestedProcedures]--; 1102 } else { 1103 waitingEnds[nestedProcedures]--; 1104 } 1105 } else if (ast.tokencode == TBaseType.rrw_case) { 1106 if (ast.searchToken(';', 1) == null) { 1107 waitingEnds[nestedProcedures]++; 1108 } 1109 } 1110 break; 1111 case body: 1112 if (ast.tokencode == TBaseType.rrw_begin) { 1113 waitingEnds[nestedProcedures]++; 1114 } else if (ast.tokencode == TBaseType.rrw_if) { 1115 if (ast.searchToken(';', 2) == null) { 1116 waitingEnds[nestedProcedures]++; 1117 } 1118 } else if (ast.tokencode == TBaseType.rrw_case) { 1119 if (ast.searchToken(';', 2) == null) { 1120 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 1121 waitingEnds[nestedProcedures]++; 1122 } 1123 } 1124 } else if (ast.tokencode == TBaseType.rrw_loop) { 1125 if (!((ast.searchToken(TBaseType.rrw_end, -1) != null) 1126 && (ast.searchToken(';', 2) != null))) { 1127 waitingEnds[nestedProcedures]++; 1128 } 1129 } else if (ast.tokencode == TBaseType.rrw_end) { 1130 waitingEnds[nestedProcedures]--; 1131 if (waitingEnds[nestedProcedures] == 0) { 1132 if (nestedProcedures == 0) { 1133 procedure_status[nestedProcedures] = stored_procedure_status.bodyend; 1134 } else { 1135 nestedProcedures--; 1136 procedure_status[nestedProcedures] = stored_procedure_status.is_as; 1137 } 1138 } 1139 } else if ((waitingEnds[nestedProcedures] == 0) 1140 && (ast.tokentype == ETokenType.ttslash) 1141 && (ast.tokencode == TBaseType.sqlpluscmd)) { 1142 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 1143 gst = EFindSqlStateType.stnormal; 1144 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1145 1146 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 1147 appendToken(gcurrentsqlstatement, ast); 1148 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1149 } 1150 break; 1151 case bodyend: 1152 if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) { 1153 // TPlsqlStatementParse(asqlstatement).TerminatorToken := ast; 1154 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 1155 gst = EFindSqlStateType.stnormal; 1156 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1157 1158 //make / a sqlplus cmd 1159 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 1160 appendToken(gcurrentsqlstatement, ast); 1161 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1162 } else if ((ast.tokentype == ETokenType.ttperiod) && (sourcetokenlist.returnaftercurtoken(false)) && (sourcetokenlist.returnbeforecurtoken(false))) { 1163 // single dot at a seperate line 1164 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 1165 gst = EFindSqlStateType.stnormal; 1166 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1167 1168 //make ttperiod a sqlplus cmd 1169 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 1170 appendToken(gcurrentsqlstatement, ast); 1171 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1172 } else if ((ast.searchToken(TBaseType.rrw_package, 1) != null) && (!endBySlashOnly)) { 1173 appendToken(gcurrentsqlstatement, ast); 1174 gst = EFindSqlStateType.stnormal; 1175 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1176 } else if ((ast.searchToken(TBaseType.rrw_procedure, 1) != null) && (!endBySlashOnly)) { 1177 appendToken(gcurrentsqlstatement, ast); 1178 gst = EFindSqlStateType.stnormal; 1179 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1180 } else if ((ast.searchToken(TBaseType.rrw_function, 1) != null) && (!endBySlashOnly)) { 1181 appendToken(gcurrentsqlstatement, ast); 1182 gst = EFindSqlStateType.stnormal; 1183 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1184 } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) 1185 && ((ast.searchToken(TBaseType.rrw_package, 4) != null) || (ast.searchToken(TBaseType.rrw_package, 5) != null)) 1186 && (!endBySlashOnly)) { 1187 appendToken(gcurrentsqlstatement, ast); 1188 gst = EFindSqlStateType.stnormal; 1189 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1190 } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) 1191 && ((ast.searchToken(TBaseType.rrw_procedure, 4) != null) 1192 || (ast.searchToken(TBaseType.rrw_function, 4) != null) 1193 || (ast.searchToken(TBaseType.rrw_view, 4) != null) 1194 || (ast.searchToken(TBaseType.rrw_oracle_synonym, 4) != null) 1195 || (ast.searchToken(TBaseType.rrw_trigger, 4) != null)) 1196 && (!endBySlashOnly)) { 1197 appendToken(gcurrentsqlstatement, ast); 1198 gst = EFindSqlStateType.stnormal; 1199 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1200 } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) && (ast.searchToken(TBaseType.rrw_library, 4) != null) && (!endBySlashOnly)) { 1201 appendToken(gcurrentsqlstatement, ast); 1202 gst = EFindSqlStateType.stnormal; 1203 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1204 } else if ((ast.searchToken(TBaseType.rrw_alter, 1) != null) && (ast.searchToken(TBaseType.rrw_trigger, 2) != null) && (!endBySlashOnly)) { 1205 appendToken(gcurrentsqlstatement, ast); 1206 gst = EFindSqlStateType.stnormal; 1207 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1208 } else if ((ast.searchToken(TBaseType.rrw_select, 1) != null) && (!endBySlashOnly)) { 1209 appendToken(gcurrentsqlstatement, ast); 1210 gst = EFindSqlStateType.stnormal; 1211 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1212 } else if ((ast.searchToken(TBaseType.rrw_call, 1) != null) && (!endBySlashOnly)) { 1213 appendToken(gcurrentsqlstatement, ast); 1214 gst = EFindSqlStateType.stnormal; 1215 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1216 } else if ((ast.searchToken(TBaseType.rrw_commit, 1) != null) && (!endBySlashOnly)) { 1217 appendToken(gcurrentsqlstatement, ast); 1218 gst = EFindSqlStateType.stnormal; 1219 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1220 } else if ((ast.searchToken(TBaseType.rrw_declare, 1) != null) && (!endBySlashOnly)) { 1221 appendToken(gcurrentsqlstatement, ast); 1222 gst = EFindSqlStateType.stnormal; 1223 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1224 } else if ((ast.searchToken(TBaseType.rrw_grant, 1) != null) 1225 && (ast.searchToken(TBaseType.rrw_execute, 2) != null) && (!endBySlashOnly)) { 1226 appendToken(gcurrentsqlstatement, ast); 1227 gst = EFindSqlStateType.stnormal; 1228 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1229 } else if ((ast.searchToken(TBaseType.rrw_alter, 1) != null) 1230 && (ast.searchToken(TBaseType.rrw_table, 2) != null) && (!endBySlashOnly)) { 1231 appendToken(gcurrentsqlstatement, ast); 1232 gst = EFindSqlStateType.stnormal; 1233 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder); 1234 } else { 1235 appendToken(gcurrentsqlstatement, ast); 1236 } 1237 break; 1238 case end: 1239 break; 1240 default: 1241 break; 1242 } 1243 1244 if (ast.tokencode == TBaseType.sqlpluscmd) { 1245 int m = flexer.getkeywordvalue(ast.getAstext()); 1246 if (m != 0) { 1247 ast.tokencode = m; 1248 } else if (ast.tokentype == ETokenType.ttslash) { 1249 ast.tokencode = '/'; 1250 } else { 1251 ast.tokencode = TBaseType.ident; 1252 } 1253 } 1254 1255 final int wrapped_keyword_max_pos = 20; 1256 if ((ast.tokencode == TBaseType.rrw_wrapped) 1257 && (ast.posinlist - gcurrentsqlstatement.sourcetokenlist.get(0).posinlist < wrapped_keyword_max_pos)) { 1258 if (gcurrentsqlstatement instanceof gudusoft.gsqlparser.stmt.TCommonStoredProcedureSqlStatement) { 1259 ((gudusoft.gsqlparser.stmt.TCommonStoredProcedureSqlStatement) gcurrentsqlstatement).setWrapped(true); 1260 } 1261 1262 if (gcurrentsqlstatement instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage) { 1263 if (ast.prevSolidToken() != null) { 1264 ((gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage) gcurrentsqlstatement) 1265 .setPackageName(fparser.getNf().createObjectNameWithPart(ast.prevSolidToken())); 1266 } 1267 } 1268 } 1269 1270 break; 1271 } //ststoredprocedure 1272 1273 } //switch 1274 }//for 1275 1276 //last statement 1277 if ((gcurrentsqlstatement != null) && 1278 ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) || 1279 (gst == EFindSqlStateType.sterror))) { 1280 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, true, builder); 1281 } 1282 1283 // Populate builder with results 1284 builder.sqlStatements(this.sqlstatements); 1285 builder.syntaxErrors(syntaxErrors instanceof ArrayList ? 1286 (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors)); 1287 builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size()); 1288 builder.errorMessage(syntaxErrors.isEmpty() ? "" : 1289 String.format("Raw extraction completed with %d error(s)", syntaxErrors.size())); 1290 } 1291 1292 /** 1293 * Handle token transformations during raw statement extraction. 1294 * <p> 1295 * This performs Oracle-specific keyword disambiguation that must happen 1296 * before statement boundary detection. Examples: 1297 * <ul> 1298 * <li>RETURN after WHERE → treat as identifier</li> 1299 * <li>VALUE after BY → mark as value_after_by</li> 1300 * <li>NEW → treat as identifier or constructor based on context</li> 1301 * <li>And many more Oracle-specific cases</li> 1302 * </ul> 1303 * 1304 * @param ast current token being processed 1305 */ 1306 private void performRawStatementTokenTransformations(TSourceToken ast) { 1307 // This method contains the keyword transformation logic from dooraclegetrawsqlstatements 1308 // It's been extracted to keep the main method more readable 1309 1310 if (ast.tokencode == TBaseType.rrw_return) { 1311 TSourceToken stMatch = ast.searchToken(TBaseType.rrw_where, 1); 1312 if (stMatch != null) { 1313 ast.tokencode = TBaseType.ident; 1314 } 1315 } else if (ast.tokencode == TBaseType.rrw_value_oracle) { 1316 TSourceToken stBy = ast.searchToken(TBaseType.rrw_by, -1); 1317 if (stBy != null) { 1318 ast.tokencode = TBaseType.rrw_value_after_by; 1319 } 1320 } else if (ast.tokencode == TBaseType.rrw_new_oracle) { 1321 TSourceToken stRightParen = ast.searchToken(')', -1); 1322 if (stRightParen != null) { 1323 ast.tokencode = TBaseType.ident; 1324 } 1325 TSourceToken stDot = ast.searchToken('.', 1); 1326 if (stDot != null) { 1327 ast.tokencode = TBaseType.ident; 1328 } 1329 1330 TSourceToken stNext = ast.searchTokenAfterObjectName(); 1331 stDot = ast.searchToken('.', 1); 1332 if ((stDot == null) && (stNext != null) && (stNext.tokencode == '(')) { 1333 ast.tokencode = TBaseType.rrw_oracle_new_constructor; 1334 } 1335 } else if (ast.tokencode == TBaseType.rrw_chr_oracle) { 1336 TSourceToken stLeftParen = ast.searchToken('(', 1); 1337 if (stLeftParen == null) { 1338 ast.tokencode = TBaseType.ident; 1339 } 1340 } else if (ast.tokencode == TBaseType.rrw_log_oracle) { 1341 TSourceToken stNext = ast.searchToken(TBaseType.rrw_errors_oracle, 1); 1342 TSourceToken stPrev = ast.searchToken(TBaseType.rrw_view, -1); 1343 if (stPrev == null) { 1344 stPrev = ast.searchToken(TBaseType.rrw_oracle_supplemental, -1); 1345 } 1346 if ((stNext == null) && (stPrev == null)) { 1347 ast.tokencode = TBaseType.ident; 1348 } 1349 } else if (ast.tokencode == TBaseType.rrw_delete) { 1350 TSourceToken stPrev = ast.searchToken('.', -1); 1351 if (stPrev != null) { 1352 ast.tokencode = TBaseType.ident; 1353 } 1354 } else if (ast.tokencode == TBaseType.rrw_partition) { 1355 TSourceToken stPrev = ast.searchToken(TBaseType.rrw_add, -1); 1356 if (stPrev != null) { 1357 stPrev.tokencode = TBaseType.rrw_add_p; 1358 } 1359 } else if (ast.tokencode == TBaseType.rrw_oracle_column) { 1360 TSourceToken stPrev = ast.searchToken(TBaseType.rrw_oracle_modify, -1); 1361 if (stPrev != null) { 1362 ast.tokencode = TBaseType.rrw_oracle_column_after_modify; 1363 } 1364 } else if (ast.tokencode == TBaseType.rrw_oracle_apply) { 1365 TSourceToken stPrev = ast.searchToken(TBaseType.rrw_outer, -1); 1366 if (stPrev != null) { 1367 stPrev.tokencode = TBaseType.ORACLE_OUTER2; 1368 } 1369 } else if (ast.tokencode == TBaseType.rrw_oracle_subpartition) { 1370 TSourceToken stNext = ast.searchToken("(", 2); 1371 if (stNext != null) { 1372 TSourceToken st1 = ast.nextSolidToken(); 1373 if (st1.toString().equalsIgnoreCase("template")) { 1374 // don't change, keep as RW_SUBPARTITION 1375 } else { 1376 ast.tokencode = TBaseType.rrw_oracle_subpartition_tablesample; 1377 } 1378 } 1379 } else if (ast.tokencode == TBaseType.rrw_primary) { 1380 TSourceToken stNext = ast.searchToken("key", 1); 1381 if (stNext == null) { 1382 ast.tokencode = TBaseType.ident; 1383 } 1384 } else if (ast.tokencode == TBaseType.rrw_oracle_offset) { 1385 TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_row, 2); 1386 if (stNext == null) { 1387 stNext = ast.searchToken(TBaseType.rrw_oracle_rows, 2); 1388 } 1389 if (stNext == null) { 1390 stNext = searchRowAfterOffsetExpression(ast); 1391 } 1392 if (stNext != null) { 1393 ast.tokencode = TBaseType.rrw_oracle_offset_row; 1394 } 1395 } else if (ast.tokencode == TBaseType.rrw_translate) { 1396 TSourceToken stNext = ast.searchToken("(", 2); 1397 if (stNext == null) { 1398 ast.tokencode = TBaseType.ident; 1399 } 1400 } else if (ast.tokencode == TBaseType.rrw_constraint) { 1401 TSourceToken stNext = ast.nextSolidToken(); 1402 if (stNext == null) { 1403 ast.tokencode = TBaseType.ident; 1404 } else { 1405 if (stNext.tokencode != TBaseType.ident) { 1406 ast.tokencode = TBaseType.ident; 1407 } 1408 } 1409 } else if (ast.tokencode == TBaseType.rrw_oracle_without) { 1410 TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_count, 1); 1411 if (stNext != null) { 1412 ast.tokencode = TBaseType.rrw_oracle_without_before_count; 1413 } 1414 } else if (ast.tokencode == TBaseType.rrw_bulk) { 1415 TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_collect, 1); 1416 if (stNext == null) { 1417 ast.tokencode = TBaseType.ident; 1418 } 1419 } else if (ast.tokencode == TBaseType.rrw_oracle_model) { 1420 TSourceToken stNext = ast.nextSolidToken(); 1421 if (stNext != null) { 1422 switch (stNext.toString().toUpperCase()) { 1423 case "RETURN": 1424 case "REFERENCE": 1425 case "IGNORE": 1426 case "KEEP": 1427 case "UNIQUE": 1428 case "PARTITION": 1429 case "DIMENSION": 1430 case "MEASURES": 1431 case "RULES": 1432 ast.tokencode = TBaseType.rrw_oracle_model_in_model_clause; 1433 break; 1434 default: 1435 ; 1436 } 1437 } 1438 } 1439 } 1440 1441 /** 1442 * Look for the ROW/ROWS keyword terminating an OFFSET clause whose value is 1443 * a multi-token arithmetic expression, e.g. <code>OFFSET 1 * 10 ROWS</code> 1444 * (Mantis 4614). The single-token case is handled by the caller via 1445 * {@link TSourceToken#searchToken(int, int)}; this scan only accepts a 1446 * strictly alternating operand/operator token run (with balanced 1447 * parentheses), so a bare <code>OFFSET</code> used as a column name is not 1448 * rewritten. 1449 * 1450 * @param offsetToken the OFFSET keyword token 1451 * @return the terminating ROW/ROWS token, or null if the following tokens 1452 * do not form an arithmetic expression ended by ROW/ROWS 1453 */ 1454 private TSourceToken searchRowAfterOffsetExpression(TSourceToken offsetToken) { 1455 // generous cap: only bounds runaway scans on token runs that never hit 1456 // ROW/ROWS; any realistic offset expression is far shorter (a tight cap 1457 // would reject valid long expressions before the grammar sees them) 1458 final int maxTokens = 512; 1459 TSourceToken t = offsetToken.nextSolidToken(); 1460 int parenDepth = 0; 1461 boolean expectOperand = true; 1462 int steps = 0; 1463 while (t != null && steps++ < maxTokens) { 1464 String text = t.toString(); 1465 if (expectOperand) { 1466 if (text.equals("(")) { 1467 parenDepth++; 1468 } else if (text.equals("+") || text.equals("-")) { 1469 // unary sign, keep expecting an operand 1470 } else if (t.tokencode == TBaseType.iconst 1471 || t.tokencode == TBaseType.fconst 1472 || t.tokencode == TBaseType.ident 1473 || text.startsWith(":")) { 1474 expectOperand = false; 1475 } else { 1476 return null; 1477 } 1478 } else { 1479 if (parenDepth == 0 1480 && (t.tokencode == TBaseType.rrw_oracle_row || t.tokencode == TBaseType.rrw_oracle_rows)) { 1481 return t; 1482 } 1483 if (text.equals(")")) { 1484 if (--parenDepth < 0) { 1485 return null; 1486 } 1487 } else if (text.equals("+") || text.equals("-") || text.equals("*") || text.equals("/")) { 1488 expectOperand = true; 1489 } else { 1490 return null; 1491 } 1492 } 1493 t = t.nextSolidToken(); 1494 } 1495 return null; 1496 } 1497 1498 private void appendToken(TCustomSqlStatement statement, TSourceToken token) { 1499 if (statement == null || token == null) { 1500 return; 1501 } 1502 token.stmt = statement; 1503 statement.sourcetokenlist.add(token); 1504 } 1505 1506 // ========== Error Handling and Recovery ========== 1507 1508 /** 1509 * Find all syntax errors in PL/SQL statements recursively. 1510 * Extracted from TGSqlParser.findAllSyntaxErrorsInPlsql(). 1511 */ 1512 private void findAllSyntaxErrorsInPlsql(TCustomSqlStatement psql) { 1513 if (psql.getErrorCount() > 0) { 1514 copyErrorsFromStatement(psql); 1515 } 1516 1517 for (int k = 0; k < psql.getStatements().size(); k++) { 1518 findAllSyntaxErrorsInPlsql(psql.getStatements().get(k)); 1519 } 1520 } 1521 1522 /** 1523 * Handle error recovery for CREATE TABLE/INDEX statements. 1524 * Oracle allows table properties that may not be fully parsed. 1525 * This method marks unparseable properties as SQL*Plus commands to skip them. 1526 * 1527 * <p>Extracted from TGSqlParser.doparse() lines 16916-16971 1528 */ 1529 private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) { 1530 if (((stmt.sqlstatementtype == ESqlStatementType.sstcreatetable) || 1531 (stmt.sqlstatementtype == ESqlStatementType.sstcreateindex)) && 1532 (!TBaseType.c_createTableStrictParsing)) { 1533 1534 // Find the closing parenthesis of table definition 1535 int nested = 0; 1536 boolean isIgnore = false, isFoundIgnoreToken = false; 1537 TSourceToken firstIgnoreToken = null; 1538 1539 for (int k = 0; k < stmt.sourcetokenlist.size(); k++) { 1540 TSourceToken st = stmt.sourcetokenlist.get(k); 1541 1542 if (isIgnore) { 1543 if (st.issolidtoken() && (st.tokencode != ';')) { 1544 isFoundIgnoreToken = true; 1545 if (firstIgnoreToken == null) { 1546 firstIgnoreToken = st; 1547 } 1548 } 1549 if (st.tokencode != ';') { 1550 st.tokencode = TBaseType.sqlpluscmd; 1551 } 1552 continue; 1553 } 1554 1555 if (st.tokencode == (int) ')') { 1556 nested--; 1557 if (nested == 0) { 1558 // Check if next token is "AS ( SELECT" 1559 boolean isSelect = false; 1560 TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1); 1561 if (st1 != null) { 1562 TSourceToken st2 = st.searchToken((int) '(', 2); 1563 if (st2 != null) { 1564 TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3); 1565 isSelect = (st3 != null); 1566 } 1567 } 1568 if (!isSelect) isIgnore = true; 1569 } 1570 } 1571 1572 if ((st.tokencode == (int) '(') || (st.tokencode == TBaseType.left_parenthesis_2)) { 1573 nested++; 1574 } 1575 } 1576 1577 // Verify it's a valid Oracle table property 1578 if ((firstIgnoreToken != null) && 1579 (!TBaseType.searchOracleTablePros(firstIgnoreToken.toString()))) { 1580 // Not a valid property, keep the error 1581 isFoundIgnoreToken = false; 1582 } 1583 1584 // Retry parsing if we found ignoreable properties 1585 if (isFoundIgnoreToken) { 1586 stmt.clearError(); 1587 stmt.parsestatement(null, false); 1588 } 1589 } 1590 } 1591 1592 /** 1593 * Copy syntax errors from a statement to our error list. 1594 * Extracted from TGSqlParser.copyerrormsg(). 1595 */ 1596 1597 @Override 1598 public String toString() { 1599 return "OracleSqlParser{vendor=" + vendor + "}"; 1600 } 1601 1602 // ========== Main Oracle Tokenization ========== 1603 // Core tokenization logic extracted from TGSqlParser.dooraclesqltexttotokenlist() 1604 1605 /** 1606 * Perform Oracle-specific tokenization with SQL*Plus command detection. 1607 * <p> 1608 * This method implements Oracle's complex tokenization rules including: 1609 * <ul> 1610 * <li>SQL*Plus command detection (SPOOL, SET, START, etc.)</li> 1611 * <li>Forward slash disambiguation (division vs PL/SQL delimiter)</li> 1612 * <li>Oracle-specific keyword transformations (INNER, TYPE, FULL, etc.)</li> 1613 * <li>Context-dependent token code modifications</li> 1614 * </ul> 1615 * 1616 * <p><b>State Machine:</b> Uses 5 boolean flags to track tokenization state: 1617 * <ul> 1618 * <li>{@code insqlpluscmd} - Currently inside SQL*Plus command</li> 1619 * <li>{@code isvalidplace} - Valid place to start SQL*Plus command</li> 1620 * <li>{@code waitingreturnforfloatdiv} - Slash seen, waiting for newline</li> 1621 * <li>{@code waitingreturnforsemicolon} - Semicolon seen, waiting for newline</li> 1622 * <li>{@code continuesqlplusatnewline} - SQL*Plus command continues to next line</li> 1623 * </ul> 1624 * 1625 * <p><b>Extracted from:</b> TGSqlParser.dooraclesqltexttotokenlist() (lines 3931-4298) 1626 * 1627 * @throws RuntimeException if tokenization fails 1628 */ 1629 private void dooraclesqltexttotokenlist() { 1630 // Initialize state machine for SQL*Plus command detection 1631 insqlpluscmd = false; 1632 javaBraceNesting = 0; 1633 isvalidplace = true; 1634 waitingreturnforfloatdiv = false; 1635 waitingreturnforsemicolon = false; 1636 continuesqlplusatnewline = false; 1637 1638 ESqlPlusCmd currentCmdType = ESqlPlusCmd.spcUnknown; 1639 1640 TSourceToken lct = null, prevst = null; 1641 1642 TSourceToken asourcetoken, lcprevst; 1643 int yychar; 1644 1645 asourcetoken = getanewsourcetoken(); 1646 if (asourcetoken == null) return; 1647 yychar = asourcetoken.tokencode; 1648 1649 while (yychar > 0) { 1650 sourcetokenlist.add(asourcetoken); 1651 1652 switch (yychar) { 1653 case TBaseType.cmtdoublehyphen: 1654 case TBaseType.cmtslashstar: 1655 case TBaseType.lexspace: { 1656 if (insqlpluscmd) { 1657 asourcetoken.insqlpluscmd = true; 1658 } 1659 break; 1660 } 1661 1662 case TBaseType.lexnewline: { 1663 if (insqlpluscmd) { 1664 insqlpluscmd = false; 1665 isvalidplace = true; 1666 1667 if (continuesqlplusatnewline) { 1668 insqlpluscmd = true; 1669 isvalidplace = false; 1670 asourcetoken.insqlpluscmd = true; 1671 } 1672 1673 if (!insqlpluscmd) { 1674 currentCmdType = ESqlPlusCmd.spcUnknown; 1675 } 1676 } 1677 1678 if (waitingreturnforsemicolon) { 1679 isvalidplace = true; 1680 } 1681 1682 if (waitingreturnforfloatdiv) { 1683 isvalidplace = true; 1684 lct.tokencode = TBaseType.sqlpluscmd; 1685 if (lct.tokentype != ETokenType.ttslash) { 1686 lct.tokentype = ETokenType.ttsqlpluscmd; 1687 } 1688 } 1689 1690 if (countLines(asourcetoken.toString()) > 1) { 1691 // There is a line after select, so spool is the right place to start a sqlplus command 1692 isvalidplace = true; 1693 } 1694 1695 flexer.insqlpluscmd = insqlpluscmd; 1696 break; 1697 } 1698 1699 default: { 1700 // Solid token 1701 // Save semicolon flag before clearing: slash after semicolon on 1702 // the same line (e.g. "END; /") should be a SQL*Plus delimiter, 1703 // not division. 1704 boolean prevWasSemicolon = waitingreturnforsemicolon; 1705 continuesqlplusatnewline = false; 1706 waitingreturnforsemicolon = false; 1707 waitingreturnforfloatdiv = false; 1708 1709 if (insqlpluscmd) { 1710 asourcetoken.insqlpluscmd = true; 1711 if (asourcetoken.toString().equalsIgnoreCase("-")) { 1712 continuesqlplusatnewline = true; 1713 } 1714 } else { 1715 if (asourcetoken.tokentype == ETokenType.ttsemicolon) { 1716 waitingreturnforsemicolon = true; 1717 } 1718 1719 if ((asourcetoken.tokentype == ETokenType.ttslash) 1720 && (isvalidplace || prevWasSemicolon || (isValidPlaceForDivToSqlplusCmd(sourcetokenlist, asourcetoken.posinlist)))) { 1721 lct = asourcetoken; 1722 waitingreturnforfloatdiv = true; 1723 } 1724 1725 currentCmdType = TSqlplusCmdStatement.searchCmd(asourcetoken.toString(), asourcetoken.nextToken()); 1726 if (currentCmdType != ESqlPlusCmd.spcUnknown) { 1727 if (isvalidplace) { 1728 TSourceToken lnbreak = null; 1729 boolean aRealSqlplusCmd = true; 1730 if (sourcetokenlist.curpos > 0) { 1731 lnbreak = sourcetokenlist.get(sourcetokenlist.curpos - 1); 1732 aRealSqlplusCmd = !spaceAtTheEndOfReturnToken(lnbreak.toString()); 1733 } 1734 1735 if (aRealSqlplusCmd) { 1736 asourcetoken.prevTokenCode = asourcetoken.tokencode; 1737 asourcetoken.tokencode = TBaseType.sqlpluscmd; 1738 if (asourcetoken.tokentype != ETokenType.ttslash) { 1739 asourcetoken.tokentype = ETokenType.ttsqlpluscmd; 1740 } 1741 insqlpluscmd = true; 1742 flexer.insqlpluscmd = insqlpluscmd; 1743 } 1744 } else if ((asourcetoken.tokencode == TBaseType.rrw_connect) && (sourcetokenlist.returnbeforecurtoken(true))) { 1745 asourcetoken.tokencode = TBaseType.sqlpluscmd; 1746 if (asourcetoken.tokentype != ETokenType.ttslash) { 1747 asourcetoken.tokentype = ETokenType.ttsqlpluscmd; 1748 } 1749 insqlpluscmd = true; 1750 flexer.insqlpluscmd = insqlpluscmd; 1751 } else if (sourcetokenlist.returnbeforecurtoken(true)) { 1752 TSourceToken lnbreak = sourcetokenlist.get(sourcetokenlist.curpos - 1); 1753 1754 if ((countLines(lnbreak.toString()) > 1) && (!spaceAtTheEndOfReturnToken(lnbreak.toString()))) { 1755 asourcetoken.tokencode = TBaseType.sqlpluscmd; 1756 if (asourcetoken.tokentype != ETokenType.ttslash) { 1757 asourcetoken.tokentype = ETokenType.ttsqlpluscmd; 1758 } 1759 insqlpluscmd = true; 1760 flexer.insqlpluscmd = insqlpluscmd; 1761 } 1762 } 1763 } 1764 } 1765 1766 isvalidplace = false; 1767 1768 // Oracle-specific keyword handling (inline to match legacy behavior) 1769 if (prevst != null) { 1770 if (prevst.tokencode == TBaseType.rrw_inner) { 1771 if (asourcetoken.tokencode != flexer.getkeywordvalue("JOIN")) { 1772 prevst.tokencode = TBaseType.ident; 1773 } 1774 } else if ((prevst.tokencode == TBaseType.rrw_not) 1775 && (asourcetoken.tokencode == flexer.getkeywordvalue("DEFERRABLE"))) { 1776 prevst.tokencode = flexer.getkeywordvalue("NOT_DEFERRABLE"); 1777 } 1778 } 1779 1780 if (asourcetoken.tokencode == TBaseType.rrw_inner) { 1781 prevst = asourcetoken; 1782 } else if (asourcetoken.tokencode == TBaseType.rrw_not) { 1783 prevst = asourcetoken; 1784 } else { 1785 prevst = null; 1786 } 1787 1788 // Oracle keyword transformations that rely on prev token state 1789 if ((asourcetoken.tokencode == flexer.getkeywordvalue("DIRECT_LOAD")) 1790 || (asourcetoken.tokencode == flexer.getkeywordvalue("ALL"))) { 1791 lcprevst = getprevsolidtoken(asourcetoken); 1792 if (lcprevst != null) { 1793 if (lcprevst.tokencode == TBaseType.rrw_for) 1794 lcprevst.tokencode = TBaseType.rw_for1; 1795 } 1796 } else if (asourcetoken.tokencode == TBaseType.rrw_dense_rank) { 1797 TSourceToken stKeep = asourcetoken.searchToken(TBaseType.rrw_keep, -2); 1798 if (stKeep != null) { 1799 stKeep.tokencode = TBaseType.rrw_keep_before_dense_rank; 1800 } 1801 } else if (asourcetoken.tokencode == TBaseType.rrw_full) { 1802 TSourceToken stMatch = asourcetoken.searchToken(TBaseType.rrw_match, -1); 1803 if (stMatch != null) { 1804 asourcetoken.tokencode = TBaseType.RW_FULL2; 1805 } 1806 } else if (asourcetoken.tokencode == TBaseType.rrw_join) { 1807 TSourceToken stFull = asourcetoken.searchToken(TBaseType.rrw_full, -1); 1808 if (stFull != null) { 1809 stFull.tokencode = TBaseType.RW_FULL2; 1810 } else { 1811 TSourceToken stNatural = asourcetoken.searchToken(TBaseType.rrw_natural, -4); 1812 if (stNatural != null) { 1813 stNatural.tokencode = TBaseType.RW_NATURAL2; 1814 } 1815 } 1816 } else if (asourcetoken.tokencode == TBaseType.rrw_outer) { 1817 TSourceToken stFull = asourcetoken.searchToken(TBaseType.rrw_full, -1); 1818 if (stFull != null) { 1819 stFull.tokencode = TBaseType.RW_FULL2; 1820 } 1821 } else if (asourcetoken.tokencode == TBaseType.rrw_is) { 1822 TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2); 1823 if (stType != null) { 1824 stType.tokencode = TBaseType.rrw_type2; 1825 } 1826 } else if (asourcetoken.tokencode == TBaseType.rrw_as) { 1827 TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2); 1828 if (stType != null) { 1829 stType.tokencode = TBaseType.rrw_type2; 1830 } 1831 } else if (asourcetoken.tokencode == TBaseType.rrw_oid) { 1832 TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2); 1833 if (stType != null) { 1834 stType.tokencode = TBaseType.rrw_type2; 1835 } 1836 } else if (asourcetoken.tokencode == TBaseType.rrw_type) { 1837 TSourceToken stPrev; 1838 stPrev = asourcetoken.searchToken(TBaseType.rrw_drop, -1); 1839 if (stPrev != null) { 1840 asourcetoken.tokencode = TBaseType.rrw_type2; 1841 } 1842 if (asourcetoken.tokencode == TBaseType.rrw_type) { 1843 stPrev = asourcetoken.searchToken(TBaseType.rrw_of, -1); 1844 if (stPrev != null) { 1845 asourcetoken.tokencode = TBaseType.rrw_type2; 1846 } 1847 } 1848 if (asourcetoken.tokencode == TBaseType.rrw_type) { 1849 stPrev = asourcetoken.searchToken(TBaseType.rrw_create, -1); 1850 if (stPrev != null) { 1851 asourcetoken.tokencode = TBaseType.rrw_type2; 1852 } 1853 } 1854 if (asourcetoken.tokencode == TBaseType.rrw_type) { 1855 stPrev = asourcetoken.searchToken(TBaseType.rrw_replace, -1); 1856 if (stPrev != null) { 1857 asourcetoken.tokencode = TBaseType.rrw_type2; 1858 } 1859 } 1860 if (asourcetoken.tokencode == TBaseType.rrw_type) { 1861 stPrev = asourcetoken.searchToken('%', -1); 1862 if (stPrev != null) { 1863 asourcetoken.tokencode = TBaseType.rrw_type2; 1864 } 1865 } 1866 } else if ((asourcetoken.tokencode == TBaseType.rrw_by) || (asourcetoken.tokencode == TBaseType.rrw_to)) { 1867 lcprevst = getprevsolidtoken(asourcetoken); 1868 if (lcprevst != null) { 1869 if ((lcprevst.tokencode == TBaseType.sqlpluscmd) && (lcprevst.toString().equalsIgnoreCase("connect"))) { 1870 lcprevst.tokencode = TBaseType.rrw_connect; 1871 lcprevst.tokentype = ETokenType.ttkeyword; 1872 flexer.insqlpluscmd = false; 1873 1874 continuesqlplusatnewline = false; 1875 waitingreturnforsemicolon = false; 1876 waitingreturnforfloatdiv = false; 1877 isvalidplace = false; 1878 insqlpluscmd = false; 1879 } 1880 } 1881 } else if (asourcetoken.tokencode == TBaseType.rrw_with) { 1882 lcprevst = getprevsolidtoken(asourcetoken); 1883 if (lcprevst != null) { 1884 if ((lcprevst.tokencode == TBaseType.sqlpluscmd) && (lcprevst.toString().equalsIgnoreCase("start"))) { 1885 lcprevst.tokencode = TBaseType.rrw_start; 1886 lcprevst.tokentype = ETokenType.ttkeyword; 1887 flexer.insqlpluscmd = false; 1888 1889 continuesqlplusatnewline = false; 1890 waitingreturnforsemicolon = false; 1891 waitingreturnforfloatdiv = false; 1892 isvalidplace = false; 1893 insqlpluscmd = false; 1894 } 1895 } 1896 } else if (asourcetoken.tokencode == TBaseType.rrw_set) { 1897 lcprevst = getprevsolidtoken(asourcetoken); 1898 if (lcprevst != null) { 1899 if (lcprevst.getAstext().equalsIgnoreCase("a")) { 1900 TSourceToken lcpp = getprevsolidtoken(lcprevst); 1901 if (lcpp != null) { 1902 if ((lcpp.tokencode == TBaseType.rrw_not) || (lcpp.tokencode == TBaseType.rrw_is)) { 1903 lcprevst.tokencode = TBaseType.rrw_oracle_a_in_aset; 1904 asourcetoken.tokencode = TBaseType.rrw_oracle_set_in_aset; 1905 } 1906 } 1907 } 1908 } 1909 } 1910 1911 break; 1912 } 1913 } 1914 1915 // Get next token 1916 asourcetoken = getanewsourcetoken(); 1917 if (asourcetoken != null) { 1918 yychar = asourcetoken.tokencode; 1919 1920 // Handle special case: dot after SQL*Plus commands 1921 if ((asourcetoken.tokencode == '.') && (getprevsolidtoken(asourcetoken) != null) 1922 && ((currentCmdType == ESqlPlusCmd.spcAppend) 1923 || (currentCmdType == ESqlPlusCmd.spcChange) || (currentCmdType == ESqlPlusCmd.spcInput) 1924 || (currentCmdType == ESqlPlusCmd.spcList) || (currentCmdType == ESqlPlusCmd.spcRun))) { 1925 // a.ent_rp_usr_id is not a real sqlplus command 1926 TSourceToken lcprevst2 = getprevsolidtoken(asourcetoken); 1927 lcprevst2.insqlpluscmd = false; 1928 if (lcprevst2.prevTokenCode != 0) { 1929 lcprevst2.tokencode = lcprevst2.prevTokenCode; 1930 } else { 1931 lcprevst2.tokencode = TBaseType.ident; 1932 } 1933 1934 flexer.insqlpluscmd = false; 1935 continuesqlplusatnewline = false; 1936 waitingreturnforsemicolon = false; 1937 waitingreturnforfloatdiv = false; 1938 isvalidplace = false; 1939 insqlpluscmd = false; 1940 } 1941 } else { 1942 yychar = 0; 1943 1944 if (waitingreturnforfloatdiv) { 1945 // / at the end of line treat as sqlplus command 1946 lct.tokencode = TBaseType.sqlpluscmd; 1947 if (lct.tokentype != ETokenType.ttslash) { 1948 lct.tokentype = ETokenType.ttsqlpluscmd; 1949 } 1950 } 1951 } 1952 1953 if ((yychar == 0) && (prevst != null)) { 1954 if (prevst.tokencode == TBaseType.rrw_inner) { 1955 prevst.tokencode = TBaseType.ident; 1956 } 1957 } 1958 } 1959 } 1960 1961 // ========== Helper Methods for Tokenization ========== 1962 // These methods support Oracle-specific tokenization logic 1963 1964 /** 1965 * Count number of newlines in a string. 1966 * 1967 * @param s string to analyze 1968 * @return number of line breaks (LF or CR) 1969 */ 1970 private int countLines(String s) { 1971 int pos = 0, lf = 0, cr = 0; 1972 1973 while (pos < s.length()) { 1974 if (s.charAt(pos) == '\r') { 1975 cr++; 1976 pos++; 1977 continue; 1978 } 1979 if (s.charAt(pos) == '\n') { 1980 lf++; 1981 pos++; 1982 continue; 1983 } 1984 1985 if (s.charAt(pos) == ' ') { 1986 pos++; 1987 continue; 1988 } 1989 break; 1990 } 1991 1992 if (lf >= cr) return lf; 1993 else return cr; 1994 } 1995 1996 /** 1997 * Check if return token ends with space or tab. 1998 * 1999 * @param s token text 2000 * @return true if ends with space/tab 2001 */ 2002 private boolean spaceAtTheEndOfReturnToken(String s) { 2003 if (s == null) return false; 2004 if (s.length() == 0) return false; 2005 2006 return ((s.charAt(s.length() - 1) == ' ') || (s.charAt(s.length() - 1) == '\t')); 2007 } 2008 2009 /** 2010 * Determine if forward slash should be treated as SQL*Plus command delimiter. 2011 * <p> 2012 * Oracle uses '/' as both division operator and SQL*Plus block delimiter. 2013 * This method disambiguates by checking if the '/' appears at the beginning 2014 * of a line (after a return token without trailing whitespace). 2015 * 2016 * @param pstlist token list 2017 * @param pPos position of '/' token 2018 * @return true if '/' should be SQL*Plus command 2019 */ 2020 private boolean isValidPlaceForDivToSqlplusCmd(TSourceTokenList pstlist, int pPos) { 2021 boolean ret = false; 2022 2023 if ((pPos <= 0) || (pPos > pstlist.size() - 1)) return ret; 2024 2025 // Token directly before div must be ttreturn without space appending it 2026 gudusoft.gsqlparser.TSourceToken lcst = pstlist.get(pPos - 1); 2027 if (lcst.tokentype != gudusoft.gsqlparser.ETokenType.ttreturn) { 2028 return ret; 2029 } 2030 2031 if (!(lcst.getAstext().charAt(lcst.getAstext().length() - 1) == ' ')) { 2032 ret = true; 2033 } 2034 2035 return ret; 2036 } 2037 2038 /** 2039 * Get previous non-whitespace token. 2040 * 2041 * @param ptoken current token 2042 * @return previous solid token, or null 2043 */ 2044 private gudusoft.gsqlparser.TSourceToken getprevsolidtoken(gudusoft.gsqlparser.TSourceToken ptoken) { 2045 gudusoft.gsqlparser.TSourceToken ret = null; 2046 TSourceTokenList lctokenlist = ptoken.container; 2047 2048 if (lctokenlist != null) { 2049 if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) { 2050 if (!( 2051 (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttwhitespace) 2052 || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttreturn) 2053 || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttsimplecomment) 2054 || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttbracketedcomment) 2055 )) { 2056 ret = lctokenlist.get(ptoken.posinlist - 1); 2057 } else { 2058 ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false); 2059 } 2060 } 2061 } 2062 return ret; 2063 } 2064}