Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 111 additions & 16 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -347,15 +347,55 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
* from column references like col, schema.col, a.b.c.col.
*
* Replaces LOOKAHEAD(16) on Function() with a targeted O(chain-length) check.
* Long chains are memoized per start token: the phase-2 lookahead routines
* re-evaluate the same walk many times per position and each chain part
* would walk the very same suffix again, which makes long chains parse in
* quadratic time. Chains up to CHAIN_CACHE_THRESHOLD parts skip the cache
* entirely, keeping the normal short-name path allocation- and map-free.
*/
private static final int CHAIN_CACHE_THRESHOLD = 8;

// Sentinel returned by the chain walks when the non-filling pass ran past
// CHAIN_CACHE_THRESHOLD delimiter/part pairs and the caller must retry
// through the per-token cache.
private static final int LONG_CHAIN = -1;

private final Map<Token, Boolean> isFunctionAheadCache = new HashMap<Token, Boolean>();

protected boolean isFunctionAhead() {
int r = isFunctionAheadWalk(false);
if (r != LONG_CHAIN) {
return r == 1;
}
Token key = getToken(1);
Boolean cached = isFunctionAheadCache.get(key);
if (cached != null) {
return cached;
}
boolean result = isFunctionAheadWalk(true) == 1;
isFunctionAheadCache.put(key, result);
return result;
}

// True when a fresh evaluation started at this token would pass all
// first-token guards and run the same chain walk.
private boolean isFunctionAheadChainStartEligible(Token t) {
return !t.image.equals("{") && t.kind != K_APPROXIMATE && !isNonFunctionKeyword(t)
&& t.kind != S_LONG && t.kind != S_DOUBLE && t.kind != S_HEX
&& t.kind != S_CHAR_LITERAL && t.kind != OPENING_BRACKET
&& t.kind != CLOSING_BRACKET && t.kind != EOF;
}

// Returns 1 for true, 0 for false, or LONG_CHAIN when the walk exceeded
// CHAIN_CACHE_THRESHOLD delimiter/part pairs (only in the non-filling pass).
private int isFunctionAheadWalk(boolean fill) {
try {
int i = 1;
Token t = getToken(i);

// JDBC escape function: {fn ...} — must check for FN keyword
if (t.image.equals("{")) {
return getToken(2).kind == K_FN;
return getToken(2).kind == K_FN ? 1 : 0;
}

// Optional APPROXIMATE keyword
Expand All @@ -367,43 +407,54 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
// Exclude tokens that have their own dedicated branches
// after Function() in PrimaryExpression
if (isNonFunctionKeyword(t)) {
return false;
return 0;
}

// First token must not be a literal, bracket, or EOF
if (t.kind == S_LONG || t.kind == S_DOUBLE || t.kind == S_HEX
|| t.kind == S_CHAR_LITERAL || t.kind == OPENING_BRACKET
|| t.kind == CLOSING_BRACKET || t.kind == EOF) {
return false;
return 0;
}
i++;

// Walk through dotted name chain
int delimiters = 0;
List<Token> chainParts = fill ? new ArrayList<Token>() : null;
while (true) {
t = getToken(i);
if (t.image.equals(".") || t.image.equals("..")
|| t.image.equals("...") || t.image.equals(":")) {
if (!fill && ++delimiters > CHAIN_CACHE_THRESHOLD) {
return LONG_CHAIN;
}
if (fill) {
chainParts.add(getToken(i + 1));
}
i++; // skip delimiter
i++; // skip next name part
} else {
break;
}
}

// Must be followed by (
if (getToken(i).kind != OPENING_BRACKET) {
return false;
}
// Must be followed by (, and not by the Oracle join syntax column(+)
boolean result = getToken(i).kind == OPENING_BRACKET
&& !(getToken(i + 1).image.equals("+")
&& getToken(i + 2).kind == CLOSING_BRACKET);

// Exclude Oracle join syntax: column(+)
if (getToken(i + 1).image.equals("+")
&& getToken(i + 2).kind == CLOSING_BRACKET) {
return false;
// Each eligible chain part would walk the same remaining suffix and
// obtain the same answer, so memoize them all in one pass.
if (fill) {
for (Token part : chainParts) {
if (isFunctionAheadChainStartEligible(part)) {
isFunctionAheadCache.put(part, result);
}
}
}

return true;
return result ? 1 : 0;
} catch (TokenMgrException e) {
return false;
return 0;
}
}

Expand Down Expand Up @@ -736,24 +787,54 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
/**
* Scans ahead through a dotted identifier chain and checks if '*' follows.
* Identifies table.* patterns for AllTableColumns.
* Long chains are memoized per start token like {@link #isFunctionAhead()}
* (one walk fills every eligible chain part); short chains skip the cache.
*/
private final Map<Token, Boolean> isAllTableColumnsAheadCache =
new HashMap<Token, Boolean>();

protected boolean isAllTableColumnsAhead() {
int r = isAllTableColumnsWalk(false);
if (r != LONG_CHAIN) {
return r == 1;
}
Token key = getToken(1);
Boolean cached = isAllTableColumnsAheadCache.get(key);
if (cached != null) {
return cached;
}
boolean result = isAllTableColumnsWalk(true) == 1;
isAllTableColumnsAheadCache.put(key, result);
return result;
}

// Returns 1 for true, 0 for false, or LONG_CHAIN when the walk exceeded
// CHAIN_CACHE_THRESHOLD delimiter/part pairs (only in the non-filling pass).
private int isAllTableColumnsWalk(boolean fill) {
int i = 1;
Token t = getToken(i);

// Must start with a name-like token
if (t.kind == S_LONG || t.kind == S_DOUBLE || t.kind == S_HEX
|| t.kind == S_CHAR_LITERAL || t.kind == OPENING_BRACKET
|| t.kind == CLOSING_BRACKET || t.kind == EOF) {
return false;
return 0;
}
i++;

// Walk through dotted name chain
int delimiters = 0;
List<Token> chainParts = fill ? new ArrayList<Token>() : null;
while (true) {
t = getToken(i);
if (t.image.equals(".") || t.image.equals("..")
|| t.image.equals("...")) {
if (!fill && ++delimiters > CHAIN_CACHE_THRESHOLD) {
return LONG_CHAIN;
}
if (fill) {
chainParts.add(getToken(i + 1));
}
i++; // skip delimiter
i++; // skip next part (could be "*")
} else {
Expand All @@ -764,7 +845,21 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
// It's AllTableColumns if the chain ended on "*"
// i.e., the last name part we skipped over was "*"
// Back up: the last token consumed was at (i-1)
return getToken(i - 1).image.equals("*");
boolean result = getToken(i - 1).image.equals("*");

// Each chain part that would pass the name-like guard walks the same
// remaining suffix and obtains the same answer.
if (fill) {
for (Token part : chainParts) {
if (part.kind == S_LONG || part.kind == S_DOUBLE || part.kind == S_HEX
|| part.kind == S_CHAR_LITERAL || part.kind == OPENING_BRACKET
|| part.kind == CLOSING_BRACKET || part.kind == EOF) {
continue;
}
isAllTableColumnsAheadCache.put(part, result);
}
}
return result ? 1 : 0;
}

/**
Expand Down
94 changes: 94 additions & 0 deletions src/test/java/net/sf/jsqlparser/parser/LongChainPredicateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package net.sf.jsqlparser.parser;

import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.AllTableColumns;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.junit.jupiter.api.Test;

/**
* Guards the threshold-gated chain-walk memoization in isFunctionAhead() /
* isAllTableColumnsAhead(): chains on both sides of CHAIN_CACHE_THRESHOLD and inside the memoized
* path must keep the exact same AST shapes as the plain walk. The speed-up itself is
* constant-factor only and is carried by the measured numbers, not by a CI timing assertion.
*/
public class LongChainPredicateTest {

private static String chain(int innerDelimiters, String last) {
// innerDelimiters = dots between s0..sN (chain length before the last part)
StringBuilder sb = new StringBuilder("s0");
for (int i = 1; i <= innerDelimiters; i++) {
sb.append(".s").append(i);
}
return sb.append(".").append(last).toString();
}

private Object firstExpression(String sql) throws Exception {
Select select = (Select) CCJSqlParserUtil.parse(sql);
return ((PlainSelect) select).getSelectItems().get(0).getExpression();
}

@Test
void functionOnShortChainFastPath() throws Exception {
// total 8 delimiters: the walk stays inside the plain non-cached path
String sql = "SELECT " + chain(7, "f(1)") + " FROM t";
assertSqlCanBeParsedAndDeparsed(sql);
assertTrue(firstExpression(sql) instanceof Function,
"8-delimiter chain ending in ( must stay a Function");
}

@Test
void functionOnLongChainMemoPath() throws Exception {
// total 9 delimiters: the walk aborts past the threshold and goes through the cache
String sql = "SELECT " + chain(8, "f(1)") + " FROM t";
assertSqlCanBeParsedAndDeparsed(sql);
assertTrue(firstExpression(sql) instanceof Function,
"9-delimiter chain ending in ( must stay a Function on the memoized path");
}

@Test
void columnOnShortChainFastPath() throws Exception {
String sql = "SELECT " + chain(7, "col") + " FROM t";
assertSqlCanBeParsedAndDeparsed(sql);
assertTrue(firstExpression(sql) instanceof Column);
assertFalse(firstExpression(sql) instanceof Function);
}

@Test
void columnOnLongChainMemoPath() throws Exception {
String sql = "SELECT " + chain(8, "col") + " FROM t";
assertSqlCanBeParsedAndDeparsed(sql);
assertTrue(firstExpression(sql) instanceof Column);
assertFalse(firstExpression(sql) instanceof Function);
}

@Test
void columnOnVeryLongChainMemoPath() throws Exception {
String sql = "SELECT " + chain(40, "col") + " FROM t";
assertSqlCanBeParsedAndDeparsed(sql);
assertTrue(firstExpression(sql) instanceof Column);
}

@Test
void allTableColumnsAcrossThreshold() throws Exception {
String shortChain = "SELECT " + chain(7, "*") + " FROM t";
assertSqlCanBeParsedAndDeparsed(shortChain);
assertTrue(firstExpression(shortChain) instanceof AllTableColumns);

String longChain = "SELECT " + chain(8, "*") + " FROM t";
assertSqlCanBeParsedAndDeparsed(longChain);
assertTrue(firstExpression(longChain) instanceof AllTableColumns);
}

@Test
void oracleOuterJoinColumnPlusOnMemoPath() throws Exception {
// the column(+) exclusion must survive the memoized walk
assertSqlCanBeParsedAndDeparsed(
"SELECT * FROM a, b WHERE " + chain(8, "x(+)") + " = b.x");
}
}
Loading