@@ -347,15 +347,55 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
347347 * from column references like col, schema.col, a.b.c.col.
348348 *
349349 * Replaces LOOKAHEAD(16) on Function() with a targeted O(chain-length) check.
350+ * Long chains are memoized per start token: the phase-2 lookahead routines
351+ * re-evaluate the same walk many times per position and each chain part
352+ * would walk the very same suffix again, which makes long chains parse in
353+ * quadratic time. Chains up to CHAIN_CACHE_THRESHOLD parts skip the cache
354+ * entirely, keeping the normal short-name path allocation- and map-free.
350355 */
356+ private static final int CHAIN_CACHE_THRESHOLD = 8;
357+
358+ // Sentinel returned by the chain walks when the non-filling pass ran past
359+ // CHAIN_CACHE_THRESHOLD delimiter/part pairs and the caller must retry
360+ // through the per-token cache.
361+ private static final int LONG_CHAIN = -1;
362+
363+ private final Map<Token, Boolean> isFunctionAheadCache = new HashMap<Token, Boolean>();
364+
351365 protected boolean isFunctionAhead() {
366+ int r = isFunctionAheadWalk(false);
367+ if (r != LONG_CHAIN) {
368+ return r == 1;
369+ }
370+ Token key = getToken(1);
371+ Boolean cached = isFunctionAheadCache.get(key);
372+ if (cached != null) {
373+ return cached;
374+ }
375+ boolean result = isFunctionAheadWalk(true) == 1;
376+ isFunctionAheadCache.put(key, result);
377+ return result;
378+ }
379+
380+ // True when a fresh evaluation started at this token would pass all
381+ // first-token guards and run the same chain walk.
382+ private boolean isFunctionAheadChainStartEligible(Token t) {
383+ return !t.image.equals("{") && t.kind != K_APPROXIMATE && !isNonFunctionKeyword(t)
384+ && t.kind != S_LONG && t.kind != S_DOUBLE && t.kind != S_HEX
385+ && t.kind != S_CHAR_LITERAL && t.kind != OPENING_BRACKET
386+ && t.kind != CLOSING_BRACKET && t.kind != EOF;
387+ }
388+
389+ // Returns 1 for true, 0 for false, or LONG_CHAIN when the walk exceeded
390+ // CHAIN_CACHE_THRESHOLD delimiter/part pairs (only in the non-filling pass).
391+ private int isFunctionAheadWalk(boolean fill) {
352392 try {
353393 int i = 1;
354394 Token t = getToken(i);
355395
356396 // JDBC escape function: {fn ...} — must check for FN keyword
357397 if (t.image.equals("{")) {
358- return getToken(2).kind == K_FN;
398+ return getToken(2).kind == K_FN ? 1 : 0 ;
359399 }
360400
361401 // Optional APPROXIMATE keyword
@@ -367,43 +407,54 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
367407 // Exclude tokens that have their own dedicated branches
368408 // after Function() in PrimaryExpression
369409 if (isNonFunctionKeyword(t)) {
370- return false ;
410+ return 0 ;
371411 }
372412
373413 // First token must not be a literal, bracket, or EOF
374414 if (t.kind == S_LONG || t.kind == S_DOUBLE || t.kind == S_HEX
375415 || t.kind == S_CHAR_LITERAL || t.kind == OPENING_BRACKET
376416 || t.kind == CLOSING_BRACKET || t.kind == EOF) {
377- return false ;
417+ return 0 ;
378418 }
379419 i++;
380420
381421 // Walk through dotted name chain
422+ int delimiters = 0;
423+ List<Token> chainParts = fill ? new ArrayList<Token>() : null;
382424 while (true) {
383425 t = getToken(i);
384426 if (t.image.equals(".") || t.image.equals("..")
385427 || t.image.equals("...") || t.image.equals(":")) {
428+ if (!fill && ++delimiters > CHAIN_CACHE_THRESHOLD) {
429+ return LONG_CHAIN;
430+ }
431+ if (fill) {
432+ chainParts.add(getToken(i + 1));
433+ }
386434 i++; // skip delimiter
387435 i++; // skip next name part
388436 } else {
389437 break;
390438 }
391439 }
392440
393- // Must be followed by (
394- if ( getToken(i).kind != OPENING_BRACKET) {
395- return false;
396- }
441+ // Must be followed by (, and not by the Oracle join syntax column(+)
442+ boolean result = getToken(i).kind == OPENING_BRACKET
443+ && !(getToken(i + 1).image.equals("+")
444+ && getToken(i + 2).kind == CLOSING_BRACKET);
397445
398- // Exclude Oracle join syntax: column(+)
399- if (getToken(i + 1).image.equals("+")
400- && getToken(i + 2).kind == CLOSING_BRACKET) {
401- return false;
446+ // Each eligible chain part would walk the same remaining suffix and
447+ // obtain the same answer, so memoize them all in one pass.
448+ if (fill) {
449+ for (Token part : chainParts) {
450+ if (isFunctionAheadChainStartEligible(part)) {
451+ isFunctionAheadCache.put(part, result);
452+ }
453+ }
402454 }
403-
404- return true;
455+ return result ? 1 : 0;
405456 } catch (TokenMgrException e) {
406- return false ;
457+ return 0 ;
407458 }
408459 }
409460
@@ -736,24 +787,54 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
736787 /**
737788 * Scans ahead through a dotted identifier chain and checks if '*' follows.
738789 * Identifies table.* patterns for AllTableColumns.
790+ * Long chains are memoized per start token like {@link #isFunctionAhead()}
791+ * (one walk fills every eligible chain part); short chains skip the cache.
739792 */
793+ private final Map<Token, Boolean> isAllTableColumnsAheadCache =
794+ new HashMap<Token, Boolean>();
795+
740796 protected boolean isAllTableColumnsAhead() {
797+ int r = isAllTableColumnsWalk(false);
798+ if (r != LONG_CHAIN) {
799+ return r == 1;
800+ }
801+ Token key = getToken(1);
802+ Boolean cached = isAllTableColumnsAheadCache.get(key);
803+ if (cached != null) {
804+ return cached;
805+ }
806+ boolean result = isAllTableColumnsWalk(true) == 1;
807+ isAllTableColumnsAheadCache.put(key, result);
808+ return result;
809+ }
810+
811+ // Returns 1 for true, 0 for false, or LONG_CHAIN when the walk exceeded
812+ // CHAIN_CACHE_THRESHOLD delimiter/part pairs (only in the non-filling pass).
813+ private int isAllTableColumnsWalk(boolean fill) {
741814 int i = 1;
742815 Token t = getToken(i);
743816
744817 // Must start with a name-like token
745818 if (t.kind == S_LONG || t.kind == S_DOUBLE || t.kind == S_HEX
746819 || t.kind == S_CHAR_LITERAL || t.kind == OPENING_BRACKET
747820 || t.kind == CLOSING_BRACKET || t.kind == EOF) {
748- return false ;
821+ return 0 ;
749822 }
750823 i++;
751824
752825 // Walk through dotted name chain
826+ int delimiters = 0;
827+ List<Token> chainParts = fill ? new ArrayList<Token>() : null;
753828 while (true) {
754829 t = getToken(i);
755830 if (t.image.equals(".") || t.image.equals("..")
756831 || t.image.equals("...")) {
832+ if (!fill && ++delimiters > CHAIN_CACHE_THRESHOLD) {
833+ return LONG_CHAIN;
834+ }
835+ if (fill) {
836+ chainParts.add(getToken(i + 1));
837+ }
757838 i++; // skip delimiter
758839 i++; // skip next part (could be "*")
759840 } else {
@@ -764,7 +845,21 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
764845 // It's AllTableColumns if the chain ended on "*"
765846 // i.e., the last name part we skipped over was "*"
766847 // Back up: the last token consumed was at (i-1)
767- return getToken(i - 1).image.equals("*");
848+ boolean result = getToken(i - 1).image.equals("*");
849+
850+ // Each chain part that would pass the name-like guard walks the same
851+ // remaining suffix and obtains the same answer.
852+ if (fill) {
853+ for (Token part : chainParts) {
854+ if (part.kind == S_LONG || part.kind == S_DOUBLE || part.kind == S_HEX
855+ || part.kind == S_CHAR_LITERAL || part.kind == OPENING_BRACKET
856+ || part.kind == CLOSING_BRACKET || part.kind == EOF) {
857+ continue;
858+ }
859+ isAllTableColumnsAheadCache.put(part, result);
860+ }
861+ }
862+ return result ? 1 : 0;
768863 }
769864
770865 /**
0 commit comments