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