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 &lt; 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}