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