001package gudusoft.gsqlparser.parser; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.EErrorType; 005import gudusoft.gsqlparser.ESqlStatementType; 006import gudusoft.gsqlparser.ETokenStatus; 007import gudusoft.gsqlparser.ETokenType; 008import gudusoft.gsqlparser.TrialInputGuard; 009import gudusoft.gsqlparser.TBaseType; 010import gudusoft.gsqlparser.TCustomLexer; 011import gudusoft.gsqlparser.TCustomParser; 012import gudusoft.gsqlparser.TCustomSqlStatement; 013import gudusoft.gsqlparser.TSourceToken; 014import gudusoft.gsqlparser.TSourceTokenList; 015import gudusoft.gsqlparser.TStatementList; 016import gudusoft.gsqlparser.TSyntaxError; 017import gudusoft.gsqlparser.compiler.TContext; 018import gudusoft.gsqlparser.compiler.TFrame; 019import gudusoft.gsqlparser.compiler.TGlobalScope; 020import gudusoft.gsqlparser.sqlcmds.ISqlCmds; 021import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory; 022import gudusoft.gsqlparser.sqlenv.TSQLEnv; 023import gudusoft.gsqlparser.stmt.TRoutine; 024 025import java.io.ByteArrayInputStream; 026import java.io.BufferedInputStream; 027import java.io.BufferedReader; 028import java.io.FileInputStream; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.InputStreamReader; 032import java.io.StringReader; 033import java.nio.charset.Charset; 034import java.util.ArrayList; 035import java.util.List; 036 037/** 038 * Abstract base class providing common logic and template methods for SQL parsing. 039 * 040 * <p>This class implements the <b>Template Method Pattern</b>, defining the skeleton 041 * of the parsing algorithm while allowing subclasses to override specific steps. 042 * It provides default implementations for common operations and hooks for 043 * vendor-specific customization. 044 * 045 * <p><b>Design Pattern:</b> Template Method 046 * <ul> 047 * <li><b>Template Methods:</b> {@link #parse(ParserContext)}, {@link #tokenize(ParserContext)}</li> 048 * <li><b>Abstract Methods:</b> Must be implemented by subclasses</li> 049 * <li><b>Hook Methods:</b> Optional overrides for customization</li> 050 * </ul> 051 * 052 * <p><b>Parsing Algorithm (Template Method):</b> 053 * <ol> 054 * <li>Get lexer ({@link #getLexer(ParserContext)})</li> 055 * <li>Tokenize SQL ({@link #performTokenization(ParserContext, TCustomLexer)})</li> 056 * <li>Process tokens ({@link #processTokensBeforeParse(ParserContext, TSourceTokenList)})</li> 057 * <li>Get parser(s) ({@link #getParser(ParserContext, TSourceTokenList)})</li> 058 * <li>Parse SQL ({@link #performParsing(ParserContext, TCustomParser, TCustomParser, TSourceTokenList)})</li> 059 * <li>Semantic analysis ({@link #performSemanticAnalysis(ParserContext, TStatementList)})</li> 060 * </ol> 061 * 062 * <p><b>Subclass Responsibilities:</b> 063 * <pre> 064 * public class OracleSqlParser extends AbstractSqlParser { 065 * public OracleSqlParser() { 066 * super(EDbVendor.dbvoracle); 067 * this.delimiterChar = '/'; 068 * } 069 * 070 * // Must implement abstract methods 071 * protected TCustomLexer getLexer(ParserContext context) { 072 * return new TLexerOracle(); 073 * } 074 * 075 * protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) { 076 * return new TParserOracleSql(tokens); 077 * } 078 * 079 * // ... other abstract methods 080 * 081 * // Optionally override hook methods 082 * protected void processTokensBeforeParse(ParserContext context, TSourceTokenList tokens) { 083 * // Oracle-specific token processing 084 * } 085 * } 086 * </pre> 087 * 088 * @see SqlParser 089 * @see ParserContext 090 * @see SqlParseResult 091 * @since 3.2.0.0 092 */ 093public abstract class AbstractSqlParser implements SqlParser { 094 095 protected final EDbVendor vendor; 096 protected char delimiterChar = ';'; 097 protected String defaultDelimiterStr = ";"; 098 099 // Syntax errors collected during parsing 100 protected List<TSyntaxError> syntaxErrors = new ArrayList<>(); 101 102 // ========== Core Parsing Components (Reused Across Parse Operations) ========== 103 104 /** 105 * Token list container - created once in constructor, cleared before each parse. 106 * <p>This follows the component reuse pattern to avoid allocation overhead. 107 */ 108 protected TSourceTokenList sourcetokenlist; 109 110 /** 111 * Statement list container - created once in constructor, cleared before each extraction. 112 * <p>This follows the component reuse pattern to avoid allocation overhead. 113 */ 114 protected TStatementList sqlstatements; 115 116 /** 117 * Current parser context for the ongoing parse operation. 118 * <p>Set at the beginning of each parse operation, contains input SQL and options. 119 */ 120 protected ParserContext parserContext; 121 122 /** 123 * SQL command resolver for identifying statement types (SELECT, INSERT, etc.). 124 * <p>Initialized lazily using SqlCmdsFactory.get(vendor) - vendor-specific implementation. 125 */ 126 protected ISqlCmds sqlcmds; 127 128 /** 129 * Token handler callback for processing tokens as they are created. 130 * <p>Optional callback that gets invoked for each token created during tokenization. 131 */ 132 private gudusoft.gsqlparser.ITokenHandle tokenHandle = null; 133 134 /** 135 * The lexer instance used for tokenization. 136 * <p>Subclasses should set this field in their constructor to their specific lexer instance. 137 * This allows common tokenization logic in AbstractSqlParser to access the lexer generically. 138 */ 139 protected TCustomLexer lexer = null; 140 141 // ========== Semantic Analysis Infrastructure ========== 142 143 /** 144 * Global context for semantic analysis. 145 * <p>Created during performParsing phase, contains SQL environment and statement references. 146 */ 147 protected TContext globalContext; 148 149 /** 150 * SQL environment for semantic analysis. 151 * <p>Vendor-specific environment configuration, used by resolver and semantic analysis. 152 */ 153 protected TSQLEnv sqlEnv; 154 155 /** 156 * Frame stack for scope management during parsing. 157 * <p>Used to track nested scopes (global, statement, block-level) during parsing. 158 */ 159 protected java.util.Stack<TFrame> frameStack; 160 161 /** 162 * Global frame pushed to frame stack during parsing. 163 * <p>Represents the outermost scope, must be popped after parsing completes. 164 */ 165 protected TFrame globalFrame; 166 167 protected static class PreparedSqlReader { 168 private final BufferedReader reader; 169 private final String charset; 170 171 protected PreparedSqlReader(BufferedReader reader, String charset) { 172 this.reader = reader; 173 this.charset = charset; 174 } 175 176 public BufferedReader getReader() { 177 return reader; 178 } 179 180 public String getCharset() { 181 return charset; 182 } 183 } 184 185 /** 186 * Construct parser for given database vendor. 187 * 188 * @param vendor the database vendor 189 */ 190 protected AbstractSqlParser(EDbVendor vendor) { 191 if (vendor == null) { 192 throw new IllegalArgumentException("vendor cannot be null"); 193 } 194 this.vendor = vendor; 195 196 // Initialize reusable containers (cleared before each use) 197 this.sourcetokenlist = new TSourceTokenList(); 198 this.sqlstatements = new TStatementList(); 199 200 // Note: parserContext is set at the beginning of each parse operation 201 // Note: sqlcmds is initialized lazily when first needed 202 } 203 204 @Override 205 public EDbVendor getVendor() { 206 return vendor; 207 } 208 209 /** 210 * Set an event handler which will be fired when a new source token is created by the lexer during tokenization. 211 * 212 * @param tokenHandle the event handler to process the new created source token 213 */ 214 public void setTokenHandle(gudusoft.gsqlparser.ITokenHandle tokenHandle) { 215 this.tokenHandle = tokenHandle; 216 } 217 218 /** 219 * Template method for full parsing. 220 * 221 * <p>This method defines the skeleton of the parsing algorithm. 222 * Subclasses should NOT override this method; instead, they should 223 * override the abstract methods and hook methods called by this template. 224 * 225 * <p><b>Algorithm:</b> 226 * <ol> 227 * <li>Create lexer</li> 228 * <li>Tokenize (time tracked)</li> 229 * <li>Process tokens (vendor-specific preprocessing)</li> 230 * <li>Create parser(s)</li> 231 * <li>Parse (time tracked)</li> 232 * <li>Semantic analysis (time tracked)</li> 233 * <li>Interpreter (time tracked)</li> 234 * </ol> 235 * 236 * @param context immutable context with all inputs 237 * @return immutable result with all outputs 238 */ 239 @Override 240 public final SqlParseResult parse(ParserContext context) { 241 // Clear syntax errors from previous parse 242 syntaxErrors.clear(); 243 244 try { 245 // Step 1: Get raw statements (internally calls tokenize() and extractRawStatements()) 246 SqlParseResult rawResult = getrawsqlstatements(context); 247 248 if (rawResult.getErrorCode() != 0) { 249 return rawResult; 250 } 251 252 // Get tokens, lexer, and RAW STATEMENTS from raw result 253 TSourceTokenList tokens = rawResult.getSourceTokenList(); 254 TCustomLexer lexer = rawResult.getLexer(); 255 TStatementList rawStatements = rawResult.getSqlStatements(); 256 257 // Step 2: Get parser(s) 258 TCustomParser parser = getParser(context, tokens); 259 TCustomParser secondaryParser = getSecondaryParser(context, tokens); 260 261 // Step 3: Full parsing (build AST for each raw statement) 262 SqlParseResult.Builder resultBuilder = new SqlParseResult.Builder(); 263 resultBuilder.lexer(lexer); 264 resultBuilder.sourceTokenList(tokens); 265 resultBuilder.tokenizationTimeMs(rawResult.getTokenizationTimeMs()); 266 resultBuilder.parser(parser); 267 268 String initialCatalog = sqlEnv == null ? null : sqlEnv.getDefaultCatalogName(); 269 String initialSchema = sqlEnv == null ? null : sqlEnv.getDefaultSchemaName(); 270 long parseStart = System.currentTimeMillis(); 271 // Pass raw statements to performParsing - it will build AST for each statement 272 TStatementList statements = performParsing(context, parser, secondaryParser, tokens, rawStatements); 273 if (statements == null) { 274 statements = new TStatementList(); 275 } 276 resultBuilder.sqlStatements(statements); 277 resultBuilder.parsingTimeMs(System.currentTimeMillis() - parseStart); 278 279 // Step 4: Semantic analysis 280 if (!context.isOnlyNeedRawParseTree()) { 281 long semanticStart = System.currentTimeMillis(); 282 String parsedCatalog = sqlEnv == null ? null : sqlEnv.getDefaultCatalogName(); 283 String parsedSchema = sqlEnv == null ? null : sqlEnv.getDefaultSchemaName(); 284 boolean replayDefaults = TBaseType.isEnableResolver2() && sqlEnv != null; 285 if (replayDefaults) { 286 sqlEnv.setDefaultCatalogName(initialCatalog); 287 sqlEnv.setDefaultSchemaName(initialSchema); 288 } 289 try { 290 performSemanticAnalysis(context, statements); 291 } finally { 292 if (replayDefaults) { 293 sqlEnv.setDefaultCatalogName(parsedCatalog); 294 sqlEnv.setDefaultSchemaName(parsedSchema); 295 } 296 } 297 resultBuilder.semanticAnalysisTimeMs(System.currentTimeMillis() - semanticStart); 298 } 299 300 // Step 5: Interpreter 301 if (!context.isOnlyNeedRawParseTree() && syntaxErrors.isEmpty()) { 302 long interpreterStart = System.currentTimeMillis(); 303 performInterpreter(context, statements); 304 resultBuilder.interpreterTimeMs(System.currentTimeMillis() - interpreterStart); 305 } 306 307 resultBuilder.syntaxErrors(syntaxErrors instanceof ArrayList ? 308 (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors)); 309 resultBuilder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size()); 310 resultBuilder.errorMessage(syntaxErrors.isEmpty() ? "" : 311 String.format("Parsing completed with %d error(s)", syntaxErrors.size())); 312 313 return resultBuilder.build(); 314 } catch (Exception e) { 315 e.printStackTrace(); 316 SqlParseResult.Builder resultBuilder = new SqlParseResult.Builder(); 317 resultBuilder.errorCode(1); 318 String errorMsg = "Parsing failed: " + e.getMessage(); 319 resultBuilder.errorMessage(errorMsg); 320 System.out.println(errorMsg+"File:\t"+context.getSqlFilename()); 321 if (context.isDumpResolverLog()) { 322 e.printStackTrace(); 323 } 324 return resultBuilder.build(); 325 } 326 } 327 328 /** 329 * Template method for tokenization only (without full parsing). 330 * 331 * <p>This method is used by {@code getrawsqlstatements()} which only 332 * needs tokenization and raw statement extraction, without detailed 333 * syntax checking or semantic analysis. 334 * 335 * <p><b>Algorithm:</b> 336 * <ol> 337 * <li>Get lexer</li> 338 * <li>Tokenize (time tracked)</li> 339 * <li>Extract raw statements (no parsing)</li> 340 * </ol> 341 * 342 * @param context immutable context with all inputs 343 * @return immutable result with tokens and raw statements 344 */ 345 @Override 346 public final SqlParseResult tokenize(ParserContext context) { 347 SqlParseResult.Builder resultBuilder = new SqlParseResult.Builder(); 348 349 try { 350 // Step 1: Get lexer (vendor-specific instance, may be cached) 351 TCustomLexer lexer = getLexer(context); 352 if (lexer == null) { 353 throw new IllegalStateException("getLexer() returned null"); 354 } 355 resultBuilder.lexer(lexer); 356 357 // Step 2: Perform tokenization. prepareSqlReader() measures the 358 // input at the byte source and records a verdict BEFORE any token 359 // is produced, so a refused input never yields tokens at all. 360 long tokenStart = System.currentTimeMillis(); 361 trialRefusalMessage = null; 362 TSourceTokenList tokens = performTokenization(context, lexer); 363 if (tokens == null) { 364 throw new IllegalStateException("performTokenization() returned null"); 365 } 366 367 // Trial size verdict, recorded at the byte source by prepareSqlReader 368 // before a single character was decoded. Reaching here with a 369 // refusal means tokenization ran over an intentionally empty reader, 370 // so there is nothing to retract -- but clear anyway and report the 371 // canonical message. Always null in full builds. 372 if (trialRefusalMessage != null) { 373 String refusal = trialRefusalMessage; 374 trialRefusalMessage = null; 375 tokens.clear(); 376 syntaxErrors.clear(); 377 resultBuilder.errorCode(-1); 378 resultBuilder.errorMessage(refusal); 379 return resultBuilder.build(); 380 } 381 382 383 // Step 3: Post-tokenization processing (CRITICAL for correct behavior) 384 // These steps must run after tokenization to prepare tokens for parsing 385 386 // Step 3a: Post-tokenization normalization 387 doAfterTokenize(tokens); 388 389 // Step 3b: Reset token chain (CRITICAL FIX) 390 // Links all tokens via getNextTokenInChain() - required for TObjectName.toString() 391 TBaseType.resetTokenChain(tokens, 0); 392 393 // Step 3c: Process tokens using token table 394 // Vendor-specific token code adjustments (e.g., BigQuery/Snowflake DO keyword handling) 395 processTokensInTokenTable(context, lexer, tokens); 396 397 // Step 3d: Pre-parse token processing 398 // Pre-parse preprocessing (e.g., Snowflake duplicate semicolon removal) 399 processTokensBeforeParse(context, tokens); 400 401 // Step 3e: Invoke the public token-list hook after all built-in 402 // normalization, matching the legacy TGSqlParser tokenization path. 403 if (context.getTokenListHandle() != null) { 404 context.getTokenListHandle().processTokenList(tokens); 405 // A handler may delete or merge tokens. Rebuild the linked token 406 // chain before raw statement extraction and parsing consume it. 407 TBaseType.resetTokenChain(tokens, 0); 408 } 409 410 resultBuilder.sourceTokenList(tokens); 411 resultBuilder.tokenizationTimeMs(System.currentTimeMillis() - tokenStart); 412 413 // Success 414 resultBuilder.errorCode(0); 415 resultBuilder.errorMessage(""); 416 417 } catch (Exception e) { 418 // Error occurred 419 resultBuilder.errorCode(1); 420 String errorMsg = "Tokenization failed: " + e.getMessage(); 421 resultBuilder.errorMessage(errorMsg); 422 423 // Log error if enabled 424 if (context.isDumpResolverLog()) { 425 e.printStackTrace(); 426 } 427 } 428 429 return resultBuilder.build(); 430 } 431 432 /** 433 * Template method for extracting raw statements without full parsing. 434 * 435 * <p>This method performs tokenization and raw statement extraction, 436 * but skips the expensive full parsing and semantic analysis steps. 437 * 438 * <p><b>Algorithm:</b> 439 * <ol> 440 * <li>Tokenize SQL (via {@link #tokenize(ParserContext)})</li> 441 * <li>Extract raw statements (via {@link #extractRawStatements(ParserContext, TSourceTokenList, TCustomLexer, long)})</li> 442 * <li>Return result with tokens and raw statements</li> 443 * </ol> 444 * 445 * <p><b>Equivalent to legacy API:</b> {@code TGSqlParser.getrawsqlstatements()} 446 * 447 * @param context immutable context with all inputs 448 * @return immutable result with tokens and raw statements (no AST) 449 */ 450 @Override 451 public final SqlParseResult getrawsqlstatements(ParserContext context) { 452 try { 453 // Step 1: Tokenize with all post-processing (calls tokenize()) 454 SqlParseResult tokenizeResult = tokenize(context); 455 456 // Check tokenization result 457 if (tokenizeResult.getErrorCode() != 0) { 458 return tokenizeResult; 459 } 460 461 // Get tokens and lexer from tokenize result 462 TSourceTokenList tokens = tokenizeResult.getSourceTokenList(); 463 TCustomLexer lexer = tokenizeResult.getLexer(); 464 long tokenizationTimeMs = tokenizeResult.getTokenizationTimeMs(); 465 466 // Step 2: Extract raw statements (vendor-specific) 467 // Vendor implementation creates builder, populates it, and returns complete result 468 SqlParseResult extractResult = extractRawStatements(context, tokens, lexer, tokenizationTimeMs); 469 470 return extractResult; 471 } catch (Exception e) { 472 e.printStackTrace(); 473 SqlParseResult.Builder resultBuilder = new SqlParseResult.Builder(); 474 resultBuilder.errorCode(1); 475 resultBuilder.errorMessage("Raw statement extraction failed: " + e.getMessage() ); 476 if (context.isDumpResolverLog()) { 477 e.printStackTrace(); 478 } 479 return resultBuilder.build(); 480 } 481 } 482 483 484 485 // ========== Abstract Methods (MUST be implemented by subclasses) ========== 486 487 /** 488 * Get the lexer for this vendor. 489 * 490 * <p><b>Subclass Responsibility:</b> Return vendor-specific lexer instance. 491 * The lexer may be created fresh or cached/reused for performance. 492 * 493 * <p><b>Example:</b> 494 * <pre> 495 * protected TCustomLexer getLexer(ParserContext context) { 496 * TLexerOracle lexer = new TLexerOracle(); 497 * lexer.delimiterchar = delimiterChar; 498 * lexer.defaultDelimiterStr = defaultDelimiterStr; 499 * return lexer; 500 * } 501 * </pre> 502 * 503 * @param context the parser context 504 * @return configured lexer instance (never null) 505 */ 506 protected abstract TCustomLexer getLexer(ParserContext context); 507 508 /** 509 * Get the main parser for this vendor. 510 * 511 * <p><b>Subclass Responsibility:</b> Return vendor-specific parser instance. 512 * The parser may be created fresh or cached/reused for performance. 513 * If reusing, the token list should be updated. 514 * 515 * <p><b>Example:</b> 516 * <pre> 517 * protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) { 518 * TParserOracleSql parser = new TParserOracleSql(tokens); 519 * parser.lexer = getLexer(context); 520 * return parser; 521 * } 522 * </pre> 523 * 524 * @param context the parser context 525 * @param tokens the source token list 526 * @return configured parser instance (never null) 527 */ 528 protected abstract TCustomParser getParser(ParserContext context, TSourceTokenList tokens); 529 530 /** 531 * Perform tokenization using vendor-specific lexer. 532 * 533 * <p><b>Template Method:</b> This method implements the common tokenization 534 * algorithm across all database vendors. Subclasses customize through one hook: 535 * {@link #tokenizeVendorSql()} - Call vendor-specific tokenization logic 536 * 537 * <p><b>Algorithm:</b> 538 * <ol> 539 * <li>Store parser context</li> 540 * <li>Prepare SQL reader (file/string with charset detection)</li> 541 * <li>Configure lexer with input reader and charset</li> 542 * <li>Reset lexer state</li> 543 * <li>Clear token list and reset position</li> 544 * <li>Reset token table cache</li> 545 * <li>Call {@link #tokenizeVendorSql()} hook</li> 546 * <li>Return populated token list</li> 547 * </ol> 548 * 549 * @param context parser context with SQL input configuration 550 * @param lexer the lexer instance (same as this.flexer) 551 * @return token list populated by vendor-specific tokenization 552 * @throws RuntimeException if tokenization fails 553 */ 554 protected TSourceTokenList performTokenization(ParserContext context, TCustomLexer lexer) { 555 this.parserContext = context; 556 557 // Set token handle from context if provided (allows TGSqlParser.setTokenHandle() to work) 558 if (context.getTokenHandle() != null) { 559 this.tokenHandle = context.getTokenHandle(); 560 } 561 562 try { 563 PreparedSqlReader prepared = prepareSqlReader(context); 564 BufferedReader finputstream = prepared.getReader(); 565 String effectiveCharset = prepared.getCharset(); 566 567 // Configure lexer with input (lexer is vendor-specific flexer from subclass) 568 lexer.yyinput = finputstream; 569 if (effectiveCharset != null && !effectiveCharset.isEmpty()) { 570 lexer.setSqlCharset(effectiveCharset); 571 } 572 lexer.reset(); 573 574 // Reset token list 575 this.sourcetokenlist.clear(); 576 this.sourcetokenlist.curpos = -1; 577 578 // Reset token table cache 579 lexer.resetTokenTable(); 580 581 // HOOK: Call vendor-specific tokenization 582 try { 583 tokenizeVendorSql(); 584 } finally { 585 // Tokenization has fully drained the input into sourcetokenlist; 586 // nothing reads yyinput after this point. Closing here releases 587 // the file descriptor the reader may hold -- without it, every 588 // file parse on this path leaked one descriptor for the life of 589 // the JVM. A caller-supplied stream is closed too: it is at EOF 590 // and the legacy path (TGSqlParser.readsql) closes it the same 591 // way on the next parse. 592 try { 593 finputstream.close(); 594 } catch (IOException ignored) { 595 // Nothing to do: the input was already consumed. 596 } 597 } 598 599 return this.sourcetokenlist; 600 601 } catch (Exception e) { 602 throw new RuntimeException("Tokenization failed: " + e.getMessage(), e); 603 } 604 } 605 606 /** 607 * Call vendor-specific tokenization logic. 608 * 609 * <p><b>Hook Method:</b> Called by {@link #performTokenization} to execute 610 * vendor-specific SQL-to-token conversion logic. 611 * 612 * <p><b>Subclass Responsibility:</b> Call the vendor-specific tokenization method 613 * (e.g., dooraclesqltexttotokenlist, domssqlsqltexttotokenlist) which reads 614 * from lexer and populates sourcetokenlist. 615 * 616 * <p><b>Example (Oracle):</b> 617 * <pre> 618 * protected void tokenizeVendorSql() { 619 * dooraclesqltexttotokenlist(); 620 * } 621 * </pre> 622 * 623 * <p><b>Example (MSSQL):</b> 624 * <pre> 625 * protected void tokenizeVendorSql() { 626 * domssqlsqltexttotokenlist(); 627 * } 628 * </pre> 629 * 630 * <p><b>Example (PostgreSQL):</b> 631 * <pre> 632 * protected void tokenizeVendorSql() { 633 * dopostgresqltexttotokenlist(); 634 * } 635 * </pre> 636 */ 637 protected abstract void tokenizeVendorSql(); 638 639 /** 640 * Extract raw statements without full parsing (public API). 641 * 642 * <p>This public method allows external callers (like TGSqlParser) to extract 643 * raw statements from an already-tokenized source list without re-tokenization. 644 * 645 * @param context the parser context 646 * @param tokens the source token list (already tokenized) 647 * @return statement list (never null) 648 * @since 3.2.0.0 649 */ 650 public final TStatementList doExtractRawStatements(ParserContext context, TSourceTokenList tokens) { 651 // Caller-supplied tokens never passed a byte source, so there is nothing 652 // to measure at ingestion: a caller can build a list by hand, or widen a 653 // token from an accepted small parse past the cap. Measure the token 654 // text itself, joined and encoded ONCE. No-op in full builds. 655 if (TrialInputGuard.tokenTextExceedsCap(tokens)) { 656 return new TStatementList(); 657 } 658 659 // Create a dummy lexer since we already have tokens 660 TCustomLexer lexer = getLexer(context); 661 662 // Call vendor-specific extraction and extract statement list from result 663 SqlParseResult result = extractRawStatements(context, tokens, lexer, 0); 664 return result.getSqlStatements() != null ? result.getSqlStatements() : new TStatementList(); 665 } 666 667 668 /** 669 * Extract raw statements without full parsing. 670 * 671 * <p><b>Template Method:</b> This method implements the common algorithm for 672 * extracting raw statements across all database vendors. Subclasses customize 673 * the process through two hook methods: 674 * <ul> 675 * <li>{@link #setupVendorParsersForExtraction()} - Initialize vendor parsers</li> 676 * <li>{@link #extractVendorRawStatements(SqlParseResult.Builder)} - Call vendor extraction logic</li> 677 * </ul> 678 * 679 * <p><b>Algorithm:</b> 680 * <ol> 681 * <li>Create SqlParseResult.Builder</li> 682 * <li>Set common fields (lexer, tokens, tokenization time)</li> 683 * <li>Store context and tokens for extraction</li> 684 * <li>Initialize SQL command resolver</li> 685 * <li>Call {@link #setupVendorParsersForExtraction()} hook</li> 686 * <li>Time the extraction</li> 687 * <li>Call {@link #extractVendorRawStatements(SqlParseResult.Builder)} hook</li> 688 * <li>Set parsing time</li> 689 * <li>Build and return result</li> 690 * </ol> 691 * 692 * @param context the parser context 693 * @param tokens the source token list 694 * @param lexer the lexer instance (for including in result) 695 * @param tokenizationTimeMs tokenization time from tokenize() step 696 * @return complete SqlParseResult with raw statements and metadata 697 */ 698 protected SqlParseResult extractRawStatements(ParserContext context, 699 TSourceTokenList tokens, 700 TCustomLexer lexer, 701 long tokenizationTimeMs) { 702 // Create builder for result construction 703 SqlParseResult.Builder builder = new SqlParseResult.Builder(); 704 705 // Set common result fields 706 builder.lexer(lexer); 707 builder.sourceTokenList(tokens); 708 builder.tokenizationTimeMs(tokenizationTimeMs); 709 710 // CRITICAL: Include parser(s) in result so TGSqlParser can use them in common parsing loop 711 TCustomParser parser = getParser(context, tokens); 712 builder.parser(parser); 713 714 // Include secondary parser for vendors that have one (e.g., Oracle PL/SQL parser) 715 TCustomParser secondaryParser = getSecondaryParser(context, tokens); 716 if (secondaryParser != null) { 717 builder.secondaryParser(secondaryParser); 718 } 719 720 // Store context and tokens for extraction 721 this.sourcetokenlist = tokens; 722 if (this.sqlstatements == null) { 723 this.sqlstatements = new TStatementList(); 724 } else { 725 this.sqlstatements.clear(); 726 } 727 this.syntaxErrors.clear(); // Clear syntax errors from previous extraction 728 this.parserContext = context; 729 730 // Initialize SQL command resolver (if not already done). Use the 731 // context vendor: a delegated parser's own vendor is the grammar 732 // family (e.g. MssqlSqlParser is dbvmssql even for dbvexasol input), 733 // while statement recognition must follow the requested dialect. 734 if (this.sqlcmds == null) { 735 this.sqlcmds = SqlCmdsFactory.get(context != null ? context.getVendor() : vendor); 736 } 737 738 // HOOK 1: Vendor-specific parser setup (sqlcmds injection, token list update) 739 setupVendorParsersForExtraction(); 740 741 // Time the extraction 742 long extractStart = System.currentTimeMillis(); 743 744 // HOOK 2: Call vendor-specific raw statement extraction 745 extractVendorRawStatements(builder); 746 747 builder.parsingTimeMs(System.currentTimeMillis() - extractStart); 748 749 // Add extracted statements to result 750 builder.sqlStatements(this.sqlstatements); 751 752 // Add collected syntax errors to result 753 if (!syntaxErrors.isEmpty()) { 754 builder.syntaxErrors(syntaxErrors instanceof ArrayList ? 755 (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors)); 756 } 757 758 return builder.build(); 759 } 760 761 /** 762 * Setup vendor-specific parsers for raw statement extraction. 763 * 764 * <p><b>Hook Method:</b> Called by {@link #extractRawStatements} after initializing 765 * sqlcmds but before calling the vendor-specific extraction logic. 766 * 767 * <p><b>Subclass Responsibility:</b> Inject sqlcmds into vendor parser(s) and 768 * update their token lists. Examples: 769 * <ul> 770 * <li><b>Single parser (MSSQL):</b> Inject into fparser only</li> 771 * <li><b>Dual parsers (Oracle):</b> Inject into both fparser and fplsqlparser</li> 772 * </ul> 773 * 774 * <p><b>Example (MSSQL):</b> 775 * <pre> 776 * protected void setupVendorParsersForExtraction() { 777 * this.fparser.sqlcmds = this.sqlcmds; 778 * this.fparser.sourcetokenlist = this.sourcetokenlist; 779 * } 780 * </pre> 781 * 782 * <p><b>Example (Oracle with dual parsers):</b> 783 * <pre> 784 * protected void setupVendorParsersForExtraction() { 785 * this.fparser.sqlcmds = this.sqlcmds; 786 * this.fplsqlparser.sqlcmds = this.sqlcmds; 787 * this.fparser.sourcetokenlist = this.sourcetokenlist; 788 * this.fplsqlparser.sourcetokenlist = this.sourcetokenlist; 789 * } 790 * </pre> 791 */ 792 protected abstract void setupVendorParsersForExtraction(); 793 794 /** 795 * Call vendor-specific raw statement extraction logic. 796 * 797 * <p><b>Hook Method:</b> Called by {@link #extractRawStatements} to execute 798 * the vendor-specific logic for identifying statement boundaries. 799 * 800 * <p><b>Subclass Responsibility:</b> Call the vendor-specific extraction method 801 * (e.g., dooraclegetrawsqlstatements, domssqlgetrawsqlstatements) passing the 802 * builder. The extraction method will populate the builder with raw statements. 803 * 804 * <p><b>Example (Oracle):</b> 805 * <pre> 806 * protected void extractVendorRawStatements(SqlParseResult.Builder builder) { 807 * dooraclegetrawsqlstatements(builder); 808 * } 809 * </pre> 810 * 811 * <p><b>Example (MSSQL):</b> 812 * <pre> 813 * protected void extractVendorRawStatements(SqlParseResult.Builder builder) { 814 * domssqlgetrawsqlstatements(builder); 815 * } 816 * </pre> 817 * 818 * @param builder the result builder to populate with raw statements 819 */ 820 protected abstract void extractVendorRawStatements(SqlParseResult.Builder builder); 821 822 /** 823 * Perform actual parsing with syntax checking. 824 * 825 * <p><b>Subclass Responsibility:</b> Parse SQL using vendor-specific parser 826 * and optional secondary parser (e.g., PL/SQL for Oracle). 827 * 828 * <p><b>Important:</b> This method receives raw statements that have already been 829 * extracted by {@link #getrawsqlstatements(ParserContext)}. Subclasses should NOT 830 * re-extract statements - just parse each statement to build the AST. 831 * 832 * <p><b>Example:</b> 833 * <pre> 834 * protected TStatementList performParsing(ParserContext context, 835 * TCustomParser parser, 836 * TCustomParser secondaryParser, 837 * TSourceTokenList tokens, 838 * TStatementList rawStatements) { 839 * // Use the passed-in rawStatements (DO NOT re-extract!) 840 * for (int i = 0; i < rawStatements.size(); i++) { 841 * TCustomSqlStatement stmt = rawStatements.get(i); 842 * stmt.parsestatement(...); // Build AST for each statement 843 * } 844 * return rawStatements; 845 * } 846 * </pre> 847 * 848 * @param context the parser context 849 * @param parser the main parser instance 850 * @param secondaryParser secondary parser (may be null) 851 * @param tokens the source token list 852 * @param rawStatements raw statements already extracted (never null) 853 * @return statement list with parsed AST (never null) 854 */ 855 protected abstract TStatementList performParsing(ParserContext context, 856 TCustomParser parser, 857 TCustomParser secondaryParser, 858 TSourceTokenList tokens, 859 TStatementList rawStatements); 860 861 // ========== Hook Methods (MAY be overridden by subclasses) ========== 862 863 /** 864 * Get secondary parser (e.g., PL/SQL for Oracle). 865 * 866 * <p><b>Hook Method:</b> Default implementation returns null. 867 * Override if vendor needs a secondary parser. 868 * The parser may be created fresh or cached/reused for performance. 869 * 870 * <p><b>Example (Oracle):</b> 871 * <pre> 872 * protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) { 873 * TParserOraclePLSql plsqlParser = new TParserOraclePLSql(tokens); 874 * plsqlParser.lexer = getLexer(context); 875 * return plsqlParser; 876 * } 877 * </pre> 878 * 879 * @param context the parser context 880 * @param tokens the source token list 881 * @return secondary parser instance, or null if not needed 882 */ 883 protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) { 884 return null; // Most vendors don't need this 885 } 886 887 /** 888 * Post-tokenization normalization. 889 * <p> 890 * Handles matching parentheses wrapping around SQL and marks semicolons 891 * before closing parens to be ignored. 892 * <p> 893 * Extracted from: TGSqlParser.doAfterTokenize() (lines 5123-5161) 894 * 895 * @param tokens the source token list (mutable) 896 */ 897 protected void doAfterTokenize(TSourceTokenList tokens) { 898 int leftParenCount = 0; 899 int rightParenCount = 0; 900 int leftIndex = 0; 901 int rightIndex = tokens.size() - 1; 902 903 // Count opening parentheses at the beginning 904 while (leftIndex < tokens.size() && tokens.get(leftIndex).tokencode == '(') { 905 leftParenCount++; 906 leftIndex++; 907 } 908 909 // Count closing parentheses at the end 910 while (rightIndex >= 0 && tokens.get(rightIndex).tokencode == ')') { 911 rightParenCount++; 912 rightIndex--; 913 } 914 915 // Set matching parentheses to be ignored 916 int parensToIgnore = Math.min(leftParenCount, rightParenCount); 917 // if there is a semicolon before the right parenthesis, set the semicolon to be ignored 918 // mantisbt/view.php?id=3690 919 920 if ((parensToIgnore > 0) && (tokens.get(tokens.size() - 1 - (parensToIgnore - 1) - 1).tokencode == ';')){ 921 // set to whitespace that this semicolon will be ignored during getting raw sql 922 tokens.get(tokens.size() - 1 - (parensToIgnore - 1) - 1).tokentype = ETokenType.ttwhitespace; 923 // set to ignore by yacc that this semicolon will be ignored during parsing 924 tokens.get(tokens.size() - 1 - (parensToIgnore - 1) - 1).tokenstatus = ETokenStatus.tsignorebyyacc; 925 } 926 } 927 928 /** 929 * Process tokens using token table (vendor-specific token code adjustments). 930 * <p> 931 * Currently handles BigQuery and Snowflake to convert DO keywords to identifiers 932 * when there's no corresponding WHILE/FOR. 933 * <p> 934 * Extracted from: TGSqlParser.processTokensInTokenTable() (lines 5186-5209) 935 * 936 * @param context the parser context 937 * @param lexer the lexer (for accessing TOKEN_TABLE) 938 * @param tokens the source token list (mutable) 939 */ 940 protected void processTokensInTokenTable(ParserContext context, TCustomLexer lexer, TSourceTokenList tokens) { 941 // Get token table from lexer 942 long[][] TOKEN_TABLE1 = lexer.TOKEN_TABLE; 943 944 switch (vendor){ 945 case dbvbigquery: 946 case dbvsnowflake: 947 // case 1, DO keyword: if no corresponding FOR, WHILE etc keywords found, 948 // set DO keyword's token code to TBaseType.ident 949 if (TOKEN_TABLE1[TBaseType.rrw_do][0] > 0){ 950 if ((TOKEN_TABLE1[TBaseType.rrw_while][0] == 0) && (TOKEN_TABLE1[TBaseType.rrw_for][0] == 0)){ 951 for(int i=0; i<tokens.size(); i++){ 952 TSourceToken st = tokens.get(i); 953 if (st.tokencode == TBaseType.rrw_do){ 954 st.tokencode = TBaseType.ident; 955 } 956 } 957 } 958 } 959 break; 960 } 961 } 962 963 /** 964 * Process tokens before parsing (vendor-specific adjustments). 965 * 966 * <p><b>Hook Method:</b> Default implementation handles Snowflake consecutive semicolons. 967 * Override if vendor needs additional token preprocessing. 968 * 969 * <p>Extracted from: TGSqlParser.processTokensBeforeParse() (lines 5165-5184) 970 * 971 * <p><b>Example:</b> 972 * <pre> 973 * protected void processTokensBeforeParse(ParserContext context, TSourceTokenList tokens) { 974 * super.processTokensBeforeParse(context, tokens); // Call base implementation 975 * // Add vendor-specific processing... 976 * } 977 * </pre> 978 * 979 * @param context the parser context 980 * @param tokens the source token list (mutable) 981 */ 982 protected void processTokensBeforeParse(ParserContext context, TSourceTokenList tokens) { 983 // For performance, only process for Snowflake as this is currently only needed there 984 // mantisbt/view.php?id=3579 985 if (vendor != EDbVendor.dbvsnowflake) return; 986 987 // If there are consecutive semicolon tokens, mark the second semicolon token as deleted 988 for(int i=0; i<tokens.size(); i++){ 989 TSourceToken st = tokens.get(i); 990 if (st.tokencode == ';'){ 991 TSourceToken nextToken = st.nextSolidToken(); 992 if (nextToken != null){ 993 if (nextToken.tokencode == ';'){ 994 nextToken.tokenstatus = ETokenStatus.tsdeleted; 995 } 996 } 997 } 998 } 999 } 1000 1001 /** 1002 * Perform semantic analysis on parsed statements. 1003 * 1004 * <p><b>Hook Method:</b> Default implementation does nothing. 1005 * Override to provide vendor-specific semantic analysis. 1006 * 1007 * <p><b>Typical Implementation:</b> 1008 * <ul> 1009 * <li>Column-to-table resolution (TSQLResolver)</li> 1010 * <li>Dataflow analysis</li> 1011 * <li>Reference resolution</li> 1012 * <li>Scope resolution</li> 1013 * </ul> 1014 * 1015 * @param context the parser context 1016 * @param statements the parsed statements (mutable) 1017 */ 1018 protected void performSemanticAnalysis(ParserContext context, TStatementList statements) { 1019 // Default implementation: no semantic analysis 1020 // Subclasses can override for vendor-specific behavior 1021 } 1022 1023 /** 1024 * Perform interpretation/evaluation on parsed statements. 1025 * 1026 * <p><b>Hook Method:</b> Default implementation does nothing. 1027 * Override to provide AST interpretation/evaluation. 1028 * 1029 * <p><b>Typical Implementation:</b> 1030 * <ul> 1031 * <li>Execute simple SQL statements</li> 1032 * <li>Evaluate expressions</li> 1033 * <li>Constant folding</li> 1034 * <li>Static analysis</li> 1035 * </ul> 1036 * 1037 * @param context the parser context 1038 * @param statements the parsed statements (mutable) 1039 */ 1040 protected void performInterpreter(ParserContext context, TStatementList statements) { 1041 // Default implementation: no interpreter 1042 // Subclasses can override to provide AST interpretation 1043 } 1044 1045 /** 1046 * Copy error messages from a statement to the parser's error collection. 1047 * 1048 * <p>This method should be called by performParsing implementations 1049 * when a statement has syntax errors. 1050 * 1051 * @param statement the statement with errors 1052 */ 1053 protected void copyErrorsFromStatement(TCustomSqlStatement statement) { 1054 if (statement == null || statement.getSyntaxErrors() == null) { 1055 return; 1056 } 1057 1058 for (int i = 0; i < statement.getSyntaxErrors().size(); i++) { 1059 this.syntaxErrors.add(new TSyntaxError((TSyntaxError) statement.getSyntaxErrors().get(i))); 1060 } 1061 } 1062 1063 /** 1064 * Attempt error recovery for CREATE TABLE/INDEX statements with unsupported options. 1065 * 1066 * <p>When parsing CREATE TABLE or CREATE INDEX statements, the parser may encounter 1067 * vendor-specific options that are not in the grammar. This method implements the 1068 * legacy error recovery behavior by marking unsupported tokens after the main 1069 * definition as SQL*Plus commands (effectively ignoring them). 1070 * 1071 * <p><b>Recovery Strategy:</b> 1072 * <ol> 1073 * <li>Find the closing ')' of the column/index definitions (nested=0)</li> 1074 * <li>Mark all remaining tokens (except ';') as sqlpluscmd to ignore them</li> 1075 * <li>Clear errors and re-parse the statement</li> 1076 * </ol> 1077 * 1078 * <p><b>When to call:</b> After parsing a statement that has errors. 1079 * Only recovers if ENABLE_ERROR_RECOVER_IN_CREATE_TABLE is true. 1080 * 1081 * @param statement the statement to attempt recovery on 1082 * @param parseResult the result code from parsing (0 = success) 1083 * @param onlyNeedRawParseTree whether only raw parse tree is needed 1084 * @return new parse result after recovery attempt, or original if no recovery 1085 */ 1086 protected int attemptErrorRecovery(TCustomSqlStatement statement, int parseResult, boolean onlyNeedRawParseTree) { 1087 boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE; 1088 1089 if (doRecover && ((parseResult != 0) || (statement.getErrorCount() > 0))) { 1090 if (((statement.sqlstatementtype == ESqlStatementType.sstcreatetable) 1091 || ((statement.sqlstatementtype == ESqlStatementType.sstcreateindex) && (this.vendor != EDbVendor.dbvcouchbase)) 1092 ) && (!TBaseType.c_createTableStrictParsing) 1093 ) { 1094 // Only parse main body of create table/index, ignore unsupported options after closing ')' 1095 int nested = 0; 1096 boolean isIgnore = false; 1097 boolean isFoundIgnoreToken = false; 1098 TSourceToken firstIgnoreToken = null; 1099 1100 for (int k = 0; k < statement.sourcetokenlist.size(); k++) { 1101 TSourceToken st = statement.sourcetokenlist.get(k); 1102 if (isIgnore) { 1103 if (st.issolidtoken() && (st.tokencode != ';')) { 1104 isFoundIgnoreToken = true; 1105 if (firstIgnoreToken == null) { 1106 firstIgnoreToken = st; 1107 } 1108 } 1109 if (st.tokencode != ';') { 1110 st.tokencode = TBaseType.sqlpluscmd; 1111 } 1112 continue; 1113 } 1114 if (st.tokencode == (int) ')') { 1115 nested--; 1116 if (nested == 0) { 1117 // Check if next token is "AS ( SELECT" (table created from select) 1118 boolean isSelect = false; 1119 TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1); 1120 if (st1 != null) { 1121 TSourceToken st2 = st.searchToken((int) '(', 2); 1122 if (st2 != null) { 1123 TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3); 1124 isSelect = (st3 != null); 1125 } 1126 } 1127 if (!isSelect) isIgnore = true; 1128 } 1129 } 1130 if ((st.tokencode == (int) '(') || (st.tokencode == TBaseType.left_parenthesis_2)) { 1131 nested++; 1132 } 1133 } 1134 1135 // For Oracle, validate that ignored tokens are valid table properties 1136 if ((this.vendor == EDbVendor.dbvoracle) && (firstIgnoreToken != null) 1137 && (!TBaseType.searchOracleTablePros(firstIgnoreToken.toString()))) { 1138 // Not a valid Oracle table property, don't ignore 1139 isFoundIgnoreToken = false; 1140 } 1141 1142 if (isFoundIgnoreToken) { 1143 statement.clearError(); 1144 parseResult = statement.parsestatement(null, false, onlyNeedRawParseTree); 1145 } 1146 } 1147 } 1148 1149 return parseResult; 1150 } 1151 1152 /** 1153 * Get the syntax errors collected during parsing. 1154 * 1155 * @return list of syntax errors (never null) 1156 */ 1157 public List<TSyntaxError> getSyntaxErrors() { 1158 return syntaxErrors; 1159 } 1160 1161 /** 1162 * Get the count of syntax errors. 1163 * 1164 * @return number of syntax errors 1165 */ 1166 public int getErrorCount() { 1167 return syntaxErrors.size(); 1168 } 1169 1170 /** 1171 * Check if a token is a dollar function delimiter ($$, $tag$, etc.) for PostgreSQL-family databases. 1172 * <p> 1173 * Migrated from TGSqlParser.isDollarFunctionDelimiter() (lines 5074-5080). 1174 * <p> 1175 * Dollar-quoted strings are used in PostgreSQL-family databases to delimit function bodies. 1176 * Each vendor has its own delimiter token code. 1177 * 1178 * @param tokencode the token code to check 1179 * @param dbVendor the database vendor 1180 * @return true if the token is a dollar function delimiter for the given vendor 1181 */ 1182 protected boolean isDollarFunctionDelimiter(int tokencode, EDbVendor dbVendor) { 1183 return ((tokencode == TBaseType.rrw_postgresql_function_delimiter) && (dbVendor == EDbVendor.dbvpostgresql)) 1184 || ((tokencode == TBaseType.rrw_postgresql_function_delimiter) && (dbVendor == EDbVendor.dbvduckdb)) 1185 || ((tokencode == TBaseType.rrw_greenplum_function_delimiter) && (dbVendor == EDbVendor.dbvgreenplum)) 1186 || ((tokencode == TBaseType.rrw_redshift_function_delimiter) && (dbVendor == EDbVendor.dbvredshift)) 1187 || ((tokencode == TBaseType.rrw_snowflake_function_delimiter) && (dbVendor == EDbVendor.dbvsnowflake)); 1188 } 1189 1190 /** 1191 * Mantis 4497: classify a '/' that was tentatively flagged as an Oracle 1192 * SQL*Plus statement terminator (tokencode {@link TBaseType#sqlpluscmd}, but 1193 * tokentype still {@link ETokenType#ttslash}) by the vendor tokenizer. 1194 * <p> 1195 * PostgreSQL/psql, Redshift and Greenplum have no '/' statement terminator -- 1196 * '/' is purely the division operator. A genuine SQL*Plus terminator alone on 1197 * a line is only ever followed by end-of-input or the start of a brand-new 1198 * statement; it is never followed by an operand. Conversely, a division slash 1199 * is always followed by its right-hand operand, which can be an identifier, a 1200 * literal, '(', a unary sign, or even a keyword that begins an expression 1201 * (NULL, CAST, CASE, ...). 1202 * <p> 1203 * We therefore treat the slash as division for everything except the two 1204 * unambiguous terminator signals: end-of-input, or a following keyword from a 1205 * conservative allowlist of strongly-reserved DML/DDL/TCL statement verbs 1206 * (see {@link #isStatementStartKeyword(int)}). This keeps the common 1207 * Oracle-style multi-statement script ({@code CREATE ... / CREATE ... /}) 1208 * splitting at the slash while letting genuine division expressions parse as a 1209 * single statement. 1210 * <p> 1211 * <b>Known limitations of this next-token heuristic</b> (both are acceptable 1212 * because these dialects do not natively support a '/' terminator at all, so 1213 * the terminator handling is only a courtesy for pasted Oracle scripts): 1214 * <ul> 1215 * <li>A bare '/' followed by a <i>non-reserved</i> command word that is not 1216 * on the allowlist (e.g. {@code COPY}, {@code VACUUM}, {@code VALUES}) 1217 * is treated as division rather than a terminator, so such scripts are 1218 * not split there. Those words are deliberately excluded because they 1219 * are also valid unquoted identifiers and would otherwise break real 1220 * division expressions whose right operand is such a column.</li> 1221 * <li>Conversely, dividing by a column whose unquoted name happens to be one 1222 * of the strongly-reserved verbs on the allowlist (e.g. a column literally 1223 * named {@code select} or {@code create}) would be misread as a 1224 * terminator. Such column names are effectively never used in practice.</li> 1225 * </ul> 1226 * 1227 * @param slash the slash token (tokentype ttslash) to classify 1228 * @return true if the slash is a division operator, false if it terminates 1229 */ 1230 protected boolean isDivisionOperatorContext(TSourceToken slash) { 1231 TSourceToken next = slash.nextSolidToken(); 1232 if (next == null) return false; // end of input -> genuine terminator 1233 return !isStatementStartKeyword(next.tokencode); 1234 } 1235 1236 /** 1237 * Returns true when the token code is a strongly-reserved DML/DDL/TCL verb 1238 * that, in practice, only ever begins a new SQL statement and is never used as 1239 * a bare unquoted column identifier. Used by 1240 * {@link #isDivisionOperatorContext(TSourceToken)} to tell a genuine SQL*Plus 1241 * '/' terminator from the division operator. 1242 * <p> 1243 * Non-reserved command words that double as plausible identifiers (SET, SHOW, 1244 * RESET, ANALYZE, LOCK, COPY, VACUUM, VALUES, COMMENT, ...) are intentionally 1245 * excluded: treating them as terminators would break valid division 1246 * expressions whose right operand is such a column. 1247 */ 1248 private boolean isStatementStartKeyword(int tokencode) { 1249 return tokencode == TBaseType.rrw_select 1250 || tokencode == TBaseType.rrw_insert 1251 || tokencode == TBaseType.rrw_update 1252 || tokencode == TBaseType.rrw_delete 1253 || tokencode == TBaseType.rrw_merge 1254 || tokencode == TBaseType.rrw_create 1255 || tokencode == TBaseType.rrw_alter 1256 || tokencode == TBaseType.rrw_drop 1257 || tokencode == TBaseType.rrw_truncate 1258 || tokencode == TBaseType.rrw_grant 1259 || tokencode == TBaseType.rrw_revoke 1260 || tokencode == TBaseType.rrw_explain 1261 || tokencode == TBaseType.rrw_with 1262 || tokencode == TBaseType.rrw_begin 1263 || tokencode == TBaseType.rrw_declare 1264 || tokencode == TBaseType.rrw_call 1265 || tokencode == TBaseType.rrw_do 1266 || tokencode == TBaseType.rrw_commit 1267 || tokencode == TBaseType.rrw_rollback 1268 || tokencode == TBaseType.rrw_savepoint; 1269 } 1270 1271 /** 1272 * Hook method called when a raw statement is complete. 1273 * <p> 1274 * This method is called by vendor-specific raw statement extraction methods 1275 * (e.g., dooraclegetrawsqlstatements) when a statement boundary is detected. 1276 * It sets up the statement with parser references and adds it to the statement list. 1277 * 1278 * @param context parser context 1279 * @param statement the completed statement 1280 * @param mainParser main parser instance 1281 * @param secondaryParser secondary parser instance (may be null) 1282 * @param statementList statement list to add to 1283 * @param isLastStatement true if this is the last statement 1284 * @param builder optional result builder (used during raw statement extraction, may be null) 1285 */ 1286 protected void onRawStatementComplete(ParserContext context, 1287 TCustomSqlStatement statement, 1288 TCustomParser mainParser, 1289 TCustomParser secondaryParser, 1290 TStatementList statementList, 1291 boolean isLastStatement, 1292 SqlParseResult.Builder builder) { 1293 if (statement == null || statementList == null) { 1294 return; 1295 } 1296 1297 // CRITICAL: Set gsqlparser reference NOW (before parsing) so nested statements 1298 // can access parser's dbvendor via getGsqlparser().getDbVendor() 1299 // This matches legacy behavior from doongetrawsqlstatementevent() 1300 if (context != null && context.getGsqlparser() != null) { 1301 // Cast to TGSqlParser - we know the type from buildContext() 1302 statement.setGsqlparser((gudusoft.gsqlparser.TGSqlParser) context.getGsqlparser()); 1303 } 1304 statement.parser = mainParser; 1305 statement.plsqlparser = secondaryParser; 1306 1307 if (statement.sourcetokenlist != null && statement.sourcetokenlist.size() > 0) { 1308 TSourceToken startToken = statement.sourcetokenlist.get(0); 1309 TSourceToken endToken = statement.sourcetokenlist.get(statement.sourcetokenlist.size() - 1); 1310 1311 statement.setStartToken(startToken); 1312 statement.setEndToken(endToken); 1313 1314 // Always set lastTokenOfStatementBeenValidated when a statement is found 1315 // This ensures getLastLineNoOfLastStatementBeenValidated() returns valid line number 1316 // for any successfully identified statement, regardless of whether it ends with semicolon 1317 if (context != null && endToken != null && builder != null) { 1318 builder.lastTokenOfStatementBeenValidated(endToken); 1319 } 1320 } 1321 1322 // Vendor-specific statement completion logic (migrated from TGSqlParser.doongetrawsqlstatementevent lines 5129-5178) 1323 onRawStatementCompleteVendorSpecific(statement); 1324 1325 statementList.add(statement); 1326 1327 } 1328 1329 /** 1330 * Hook for vendor-specific logic when a raw statement is completed. 1331 * <p> 1332 * Migrated from TGSqlParser.doongetrawsqlstatementevent() (lines 5129-5178). 1333 * <p> 1334 * This method is called after basic statement setup but before adding to the statement list. 1335 * Subclasses can override to add vendor-specific token manipulations or metadata. 1336 * <p> 1337 * Default implementation handles PostgreSQL-family routine body processing. 1338 * 1339 * @param statement the completed statement 1340 */ 1341 protected void onRawStatementCompleteVendorSpecific(TCustomSqlStatement statement) { 1342 // Handle PostgreSQL-family databases: Mark non-SQL/PLSQL routine body tokens 1343 // Migrated from TGSqlParser.doongetrawsqlstatementevent() lines 5143-5178 1344 if (((this.vendor == EDbVendor.dbvpostgresql) || (this.vendor == EDbVendor.dbvgreenplum) 1345 || (this.vendor == EDbVendor.dbvredshift) || (this.vendor == EDbVendor.dbvsnowflake)) 1346 && (statement instanceof TRoutine)) { 1347 1348 TRoutine routine = (TRoutine) statement; 1349 if (!routine.isBodyInSQL()) { 1350 TSourceToken st; 1351 boolean inBody = false; 1352 StringBuilder routineBodyBuilder = new StringBuilder(); 1353 1354 for (int i = 0; i < statement.sourcetokenlist.size(); i++) { 1355 st = statement.sourcetokenlist.get(i); 1356 1357 // Check for dollar function delimiter ($$, $tag$, etc.) 1358 if (isDollarFunctionDelimiter(st.tokencode, this.vendor)) { 1359 if (!inBody) { 1360 inBody = true; 1361 routineBodyBuilder.setLength(0); 1362 routineBodyBuilder.append(st.toString()); 1363 } else { 1364 inBody = false; 1365 routineBodyBuilder.append(st.toString()); 1366 break; 1367 } 1368 continue; 1369 } 1370 1371 if (inBody) { 1372 // Mark body tokens as sqlpluscmd so they're not parsed as SQL 1373 st.tokencode = TBaseType.sqlpluscmd; 1374 routineBodyBuilder.append(st.toString()); 1375 } 1376 } 1377 1378 routine.setRoutineBody(routineBodyBuilder.toString()); 1379 } 1380 } 1381 } 1382 1383 private static final int ENCODING_UTF16 = 1; 1384 private static final int ENCODING_UTF32 = 2; 1385 private static final int ENCODING_UTF8_BOM = 3; 1386 1387 /** 1388 * Trial size verdict for the input currently being prepared, set by 1389 * {@link #prepareSqlReader} and consumed by {@link #tokenize}. Always null 1390 * in full builds. 1391 */ 1392 private String trialRefusalMessage = null; 1393 1394 /** 1395 * Resolve the active input source and hand back a reader over it. 1396 * 1397 * <p>This is the one place where bytes become characters, so it is also the 1398 * one place the trial cap is measured -- in RAW BYTES, before decoding. 1399 * A byte-backed source is read once into a bounded snapshot; when the 1400 * snapshot is within the cap it IS the whole input, and the reader is built 1401 * over it so the source is never read twice. Measuring anywhere downstream 1402 * would mean re-encoding decoded text, which is what every earlier revision 1403 * of this gate got wrong. 1404 */ 1405 protected PreparedSqlReader prepareSqlReader(ParserContext context) throws IOException { 1406 BufferedReader reader; 1407 String effectiveCharset = context.getSqlCharset(); 1408 1409 if (context.getSqlText() != null) { 1410 // A String has no wire format: measured as characters, which keeps 1411 // any re-parse of already-ingested text (a routine body, dynamic 1412 // SQL) inside the cap automatically -- see TrialInputGuard. 1413 TrialInputGuard.Result verdict = TrialInputGuard.checkText(context.getSqlText()); 1414 if (verdict.isRefused()) { 1415 trialRefusalMessage = verdict.getRefusalMessage(); 1416 return new PreparedSqlReader(new BufferedReader(new StringReader("")), effectiveCharset); 1417 } 1418 reader = new BufferedReader(new StringReader(context.getSqlText())); 1419 return new PreparedSqlReader(reader, effectiveCharset); 1420 } 1421 1422 if (context.getSqlFilename() != null && !context.getSqlFilename().isEmpty()) { 1423 FileInputStream fileStream = new FileInputStream(context.getSqlFilename()); 1424 // Deliberately NOT File.length(): the file can change between a stat 1425 // and the read, and the bytes that decide the verdict must be the 1426 // same bytes that feed decoding. 1427 InputStream measured = applyTrialGuard(fileStream); 1428 // We opened this stream, so we close it whenever the guard stops 1429 // using it: on a refusal, and when a snapshot replaces it. A 1430 // caller-supplied stream (branch below) is never closed here. 1431 if (measured == null) { 1432 fileStream.close(); 1433 return new PreparedSqlReader(new BufferedReader(new StringReader("")), effectiveCharset); 1434 } 1435 if (measured != fileStream) { 1436 fileStream.close(); 1437 } 1438 BufferedInputStream bufferedStream = new BufferedInputStream(measured, 8); 1439 int encodingType = detectEncodingFromBom(bufferedStream); 1440 String charsetToUse = resolveCharsetName(encodingType, context.getSqlCharset()); 1441 InputStreamReader streamReader = new InputStreamReader(bufferedStream, charsetToUse); 1442 reader = new BufferedReader(streamReader); 1443 skipBomIfPresent(reader, encodingType); 1444 return new PreparedSqlReader(reader, charsetToUse); 1445 } 1446 1447 InputStream contextStream = context.getSqlInputStream(); 1448 if (contextStream != null) { 1449 contextStream = applyTrialGuard(contextStream); 1450 if (contextStream == null) { 1451 return new PreparedSqlReader(new BufferedReader(new StringReader("")), effectiveCharset); 1452 } 1453 BufferedInputStream bufferedStream = (contextStream instanceof BufferedInputStream) 1454 ? (BufferedInputStream) contextStream 1455 : new BufferedInputStream(contextStream, 8); 1456 int encodingType = detectEncodingFromBom(bufferedStream); 1457 String charsetToUse = resolveCharsetName(encodingType, context.getSqlCharset()); 1458 InputStreamReader streamReader = new InputStreamReader(bufferedStream, charsetToUse); 1459 reader = new BufferedReader(streamReader); 1460 skipBomIfPresent(reader, encodingType); 1461 return new PreparedSqlReader(reader, charsetToUse); 1462 } 1463 1464 // Default: empty input is valid, return reader for empty string 1465 reader = new BufferedReader(new StringReader("")); 1466 return new PreparedSqlReader(reader, effectiveCharset); 1467 } 1468 1469 /** 1470 * Measure a byte source against the trial cap and return the stream the 1471 * caller should actually read: the captured snapshot on acceptance, or null 1472 * when the input is refused (with {@link #trialRefusalMessage} set). Returns 1473 * the source untouched in full builds. 1474 * 1475 * <p>The snapshot is operation-local. It is never stored as parser state, so 1476 * a pooled or reused parser cannot serve a later input from an earlier 1477 * capture. 1478 */ 1479 private InputStream applyTrialGuard(InputStream source) throws IOException { 1480 if (TBaseType.full_edition) { 1481 return source; 1482 } 1483 TrialInputGuard.Result verdict = TrialInputGuard.checkStream(source); 1484 if (verdict.isIoFailure()) { 1485 // A partial read that then failed is not a small legal input. 1486 throw verdict.getIoFailure(); 1487 } 1488 if (verdict.isRefused()) { 1489 trialRefusalMessage = verdict.getRefusalMessage(); 1490 return null; 1491 } 1492 byte[] snapshot = verdict.getSnapshot(); 1493 return snapshot == null ? source : new ByteArrayInputStream(snapshot); 1494 } 1495 1496 private int detectEncodingFromBom(BufferedInputStream stream) throws IOException { 1497 if (stream == null || !stream.markSupported()) { 1498 return 0; 1499 } 1500 1501 byte[] bom = new byte[4]; 1502 stream.mark(bom.length + 1); 1503 int read = stream.read(bom, 0, bom.length); 1504 stream.reset(); 1505 1506 if (read < 2) { 1507 return 0; 1508 } 1509 1510 if (((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) 1511 || ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF))) { 1512 if (read >= 4 && (((bom[2] == (byte) 0xFF) && (bom[3] == (byte) 0xFE)) 1513 || ((bom[2] == (byte) 0xFE) && (bom[3] == (byte) 0xFF)))) { 1514 return ENCODING_UTF32; 1515 } 1516 return ENCODING_UTF16; 1517 } 1518 1519 if (read >= 3 && (bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) { 1520 return ENCODING_UTF8_BOM; 1521 } 1522 1523 return 0; 1524 } 1525 1526 private String resolveCharsetName(int encodingType, String contextCharset) { 1527 switch (encodingType) { 1528 case ENCODING_UTF16: 1529 return "UTF-16"; 1530 case ENCODING_UTF32: 1531 return "UTF-32"; 1532 case ENCODING_UTF8_BOM: 1533 return "UTF-8"; 1534 default: 1535 if (contextCharset != null && !contextCharset.isEmpty()) { 1536 return contextCharset; 1537 } 1538 return Charset.defaultCharset().name(); 1539 } 1540 } 1541 1542 private void skipBomIfPresent(BufferedReader reader, int encodingType) throws IOException { 1543 if (encodingType != ENCODING_UTF8_BOM || reader == null || !reader.markSupported()) { 1544 return; 1545 } 1546 1547 reader.mark(1); 1548 int ch = reader.read(); 1549 if (ch != 0xFEFF && ch != -1) { 1550 reader.reset(); 1551 } 1552 } 1553 1554 // ========== Utility Methods ========== 1555 1556 /** 1557 * Initialize global context and frame stack for statement parsing. 1558 * <p> 1559 * This method sets up the semantic analysis infrastructure required during 1560 * the parsing phase. It creates: 1561 * <ul> 1562 * <li>Global context (TContext) for semantic analysis</li> 1563 * <li>SQL environment (TSQLEnv) with vendor-specific configuration</li> 1564 * <li>Frame stack for scope management</li> 1565 * <li>Global scope frame as the outermost scope</li> 1566 * </ul> 1567 * 1568 * <p><b>When to call:</b> At the beginning of performParsing(), before parsing statements. 1569 * 1570 * <p><b>Cleanup required:</b> Must call {@code globalFrame.popMeFromStack(frameStack)} 1571 * after all statements are parsed to clean up the frame stack. 1572 * 1573 * <p><b>Extracted from:</b> Identical implementations in OracleSqlParser and MssqlSqlParser 1574 * to eliminate ~16 lines of duplicate code per parser. 1575 */ 1576 protected void initializeGlobalContext() { 1577 // Initialize global context for semantic analysis 1578 this.globalContext = new TContext(); 1579 this.sqlEnv = new TSQLEnv(this.vendor) { 1580 @Override 1581 public void initSQLEnv() { 1582 // Vendor-specific initialization can be added by subclasses if needed 1583 } 1584 }; 1585 this.globalContext.setSqlEnv(this.sqlEnv, this.sqlstatements); 1586 1587 // Create global scope frame 1588 this.frameStack = new java.util.Stack<TFrame>(); 1589 TGlobalScope globalScope = new TGlobalScope(); 1590 globalScope.resetCurrentStmtIndex(); 1591 globalScope.setSqlEnv(this.sqlEnv); 1592 this.globalFrame = new TFrame(globalScope); 1593 this.globalFrame.pushMeToStack(this.frameStack); 1594 } 1595 1596 /** 1597 * Handle exceptions that occur during individual statement parsing. 1598 * <p> 1599 * This method provides robust error handling that allows parsing to continue 1600 * even when individual statements throw exceptions. It: 1601 * <ul> 1602 * <li>Creates a detailed {@link TSyntaxError} with exception information</li> 1603 * <li>Captures statement location (line, column) from first token</li> 1604 * <li>Includes statement number, exception type, and message</li> 1605 * <li>Optionally logs full stack trace if debugging is enabled</li> 1606 * <li>Adds error to {@link #syntaxErrors} list for user feedback</li> 1607 * </ul> 1608 * 1609 * <p><b>Benefits:</b> 1610 * <ul> 1611 * <li>Parsing continues for remaining statements after exception</li> 1612 * <li>Users get complete error feedback for all statements</li> 1613 * <li>Developers get stack traces for debugging parser issues</li> 1614 * </ul> 1615 * 1616 * <p><b>Example error message:</b><br> 1617 * {@code "Exception during parsing statement 3: NullPointerException - Cannot invoke..."} 1618 * 1619 * <p><b>Extracted from:</b> Identical implementations in OracleSqlParser and MssqlSqlParser 1620 * to eliminate ~51 lines of duplicate code per parser. 1621 * 1622 * @param stmt the statement that failed to parse 1623 * @param statementIndex 0-based index of the statement in the statement list 1624 * @param ex the exception that was thrown during parsing 1625 */ 1626 protected void handleStatementParsingException(TCustomSqlStatement stmt, int statementIndex, Exception ex) { 1627 // Create user-friendly error message with context 1628 String errorMsg = String.format("Exception during parsing statement %d: %s - %s", 1629 statementIndex + 1, // Convert to 1-based for user readability 1630 ex.getClass().getSimpleName(), 1631 ex.getMessage() != null ? ex.getMessage() : "No details"); 1632 1633 // Get first token of statement for error location 1634 TSourceToken firstToken = null; 1635 if (stmt.sourcetokenlist != null && stmt.sourcetokenlist.size() > 0) { 1636 firstToken = stmt.sourcetokenlist.get(0); 1637 } 1638 1639 // Create syntax error with exception details 1640 TSyntaxError syntaxError; 1641 if (firstToken != null) { 1642 // Use token location for accurate error reporting 1643 syntaxError = new TSyntaxError( 1644 firstToken.getAstext(), 1645 firstToken.lineNo, 1646 firstToken.columnNo, 1647 errorMsg, 1648 EErrorType.sperror, 1649 TBaseType.MSG_ERROR_SYNTAX_ERROR, 1650 stmt, 1651 firstToken.posinlist 1652 ); 1653 } else { 1654 // Fallback if no token info available 1655 syntaxError = new TSyntaxError( 1656 "", 1657 0, 1658 0, 1659 errorMsg, 1660 EErrorType.sperror, 1661 TBaseType.MSG_ERROR_SYNTAX_ERROR, 1662 stmt, 1663 -1 1664 ); 1665 } 1666 1667 this.syntaxErrors.add(syntaxError); 1668 1669 // Log to console if debugging enabled 1670 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1671 System.err.println("ERROR: " + errorMsg); 1672 ex.printStackTrace(); 1673 } 1674 } 1675 1676 /** 1677 * Hook method for vendor-specific post-processing after a statement is parsed. 1678 * <p> 1679 * This method is called after each statement is successfully parsed but before 1680 * error recovery and error collection. Subclasses can override this to perform 1681 * vendor-specific operations such as: 1682 * <ul> 1683 * <li>Checking for vendor-specific syntax errors in nested statements</li> 1684 * <li>Validating vendor-specific constraints</li> 1685 * <li>Collecting vendor-specific metadata</li> 1686 * </ul> 1687 * 1688 * <p><b>Default implementation:</b> Does nothing (no-op). 1689 * 1690 * <p><b>Example override (Oracle):</b><br> 1691 * <pre>{@code 1692 * @Override 1693 * protected void afterStatementParsed(TCustomSqlStatement stmt) { 1694 * if (stmt.isoracleplsql()) { 1695 * findAllSyntaxErrorsInPlsql(stmt); 1696 * } 1697 * } 1698 * }</pre> 1699 * 1700 * <p><b>When called:</b> After {@code stmt.parsestatement()} succeeds, 1701 * before {@code handleCreateTableErrorRecovery()} and {@code copyErrorsFromStatement()}. 1702 * 1703 * @param stmt the statement that was just parsed 1704 */ 1705 protected void afterStatementParsed(TCustomSqlStatement stmt) { 1706 // Default: no additional processing 1707 // Subclasses override to add vendor-specific post-processing 1708 } 1709 1710 /** 1711 * Get next source token from the lexer. 1712 * <p> 1713 * This method wraps the lexer's yylexwrap() call and performs several important tasks: 1714 * <ul> 1715 * <li>Fetches the next raw token from the lexer</li> 1716 * <li>Combines consecutive whitespace/newline tokens for cleaner token stream</li> 1717 * <li>Sets token metadata (vendor, status, container, position in list)</li> 1718 * <li>Optionally calls token handler callback</li> 1719 * </ul> 1720 * 1721 * <p><b>Token Consolidation Rules:</b> 1722 * <ul> 1723 * <li>Whitespace after a newline is merged into the newline token</li> 1724 * <li>Consecutive newlines are merged into a single newline token</li> 1725 * </ul> 1726 * 1727 * <p><b>Implementation Note:</b> 1728 * This method is extracted from TGSqlParser.getanewsourcetoken() and made 1729 * available to all database-specific parsers to avoid code duplication. 1730 * 1731 * @return next source token, or null if end of input 1732 */ 1733 protected TSourceToken getanewsourcetoken() { 1734 TSourceToken pst = null, prevst; 1735 1736 while (true) { 1737 pst = new TSourceToken(""); 1738 if (lexer.yylexwrap(pst) == 0) { 1739 pst = null; 1740 break; 1741 } 1742 1743 pst.setDbvendor(vendor); 1744 pst.tokenstatus = ETokenStatus.tsoriginal; 1745 1746 if (pst.tokentype == ETokenType.ttreturn) { 1747 pst.setAstext(towinlinebreak(pst.getAstext())); 1748 } 1749 1750 // Combine space & linebreak after a linebreak into one 1751 if ((pst.tokentype == ETokenType.ttwhitespace) 1752 && (sourcetokenlist.curpos >= 0)) { 1753 prevst = sourcetokenlist.get(sourcetokenlist.curpos); 1754 if (prevst.tokentype == ETokenType.ttreturn) { 1755 // Can't discard whitespace after linebreak, it will be used 1756 // to judge whether / at the beginning of the line is a sqlplus cmd or not 1757 // check isValidPlaceForDivToSqlplusCmd for more 1758 prevst.setAstext(prevst.getAstext() + pst.getAstext()); 1759 continue; 1760 } 1761 } 1762 1763 // Combine consecutive newlines 1764 if ((pst.tokentype == ETokenType.ttreturn) 1765 && (sourcetokenlist.curpos >= 0)) { 1766 prevst = sourcetokenlist.get(sourcetokenlist.curpos); 1767 1768 if (prevst.tokentype == ETokenType.ttreturn) { 1769 prevst.setAstext(prevst.getAstext() + pst.getAstext()); 1770 continue; 1771 } 1772 1773 // Note: The original code has a commented section about merging 1774 // whitespace with newline. We're preserving the behavior here 1775 // which does NOT merge preceding whitespace with newline. 1776 } 1777 1778 break; 1779 } 1780 1781 if (pst != null) { 1782 pst.container = sourcetokenlist; 1783 sourcetokenlist.curpos = sourcetokenlist.curpos + 1; 1784 pst.posinlist = sourcetokenlist.curpos; 1785 1786 // Optional token handler callback. Fires inline, unchanged: an 1787 // over-cap input is refused before tokenization starts, so no token 1788 // can reach the callback from a rejected input and there is nothing 1789 // to defer. 1790 if (tokenHandle != null) { 1791 tokenHandle.processToken(pst); 1792 } 1793 } 1794 1795 lexer.setTokenTableValue(pst); 1796 return pst; 1797 } 1798 1799 /** 1800 * Convert line breaks to Windows format. 1801 * <p> 1802 * Currently returns the input unchanged. This method exists for compatibility 1803 * with the original TGSqlParser implementation. 1804 * 1805 * @param s Input string 1806 * @return String with Windows line breaks (currently unchanged) 1807 */ 1808 protected String towinlinebreak(String s) { 1809 return s; 1810 // if (s == null) return null; 1811 // return s.replace("\n", "\r\n"); 1812 } 1813 1814 /** 1815 * Get the delimiter character for this vendor. 1816 * 1817 * @return delimiter character (e.g., ';', '/', '$') 1818 */ 1819 public char getDelimiterChar() { 1820 return delimiterChar; 1821 } 1822 1823 /** 1824 * Get the default delimiter string for this vendor. 1825 * 1826 * @return default delimiter string 1827 */ 1828 public String getDefaultDelimiterStr() { 1829 return defaultDelimiterStr; 1830 } 1831 1832 @Override 1833 public String toString() { 1834 return getClass().getSimpleName() + "{vendor=" + vendor + "}"; 1835 } 1836}