From 270d861e8f3a5eddb73c0093f8ccdb5fdf836918 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 29 Oct 2026 07:39:16 +0100 Subject: [PATCH 1/4] perf: create the deque of NOT prefixes only for a negated condition `Condition()` created a deque for leading `NOT` and `!` prefixes on every call, though few conditions have one. An empty `ArrayDeque` allocates its 17-slot array up front, so parsing `performance.sql` allocated 585 KB per parse for deques that stayed empty. The deque is now created at the first `NOT` or `!`. --- .../jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 7339e68ba..494219be4 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -12181,14 +12181,20 @@ Expression Condition(): Expression result; Expression left; // Leading NOT / ! prefixes, leftmost token wraps outermost (issue #1170). - final Deque exclamationMarks = new ArrayDeque(); + Deque exclamationMarks = null; int oraclePrior = EqualsTo.NO_ORACLE_PRIOR; int oracleJoin = EqualsTo.NO_ORACLE_JOIN; } { ( LOOKAHEAD({ getToken(1).kind == K_NOT || "!".equals(getToken(1).image) }) - ( { exclamationMarks.addFirst(Boolean.FALSE); } | "!" { exclamationMarks.addFirst(Boolean.TRUE); } ) + ( | "!" ) + { + if (exclamationMarks == null) { + exclamationMarks = new ArrayDeque(); + } + exclamationMarks.addFirst("!".equals(getToken(0).image)); + } )* ( LOOKAHEAD( JdbcNamedParameter() ":", { @@ -12258,7 +12264,7 @@ Expression Condition(): && result instanceof SupportsOldOracleJoinSyntax) { ((SupportsOldOracleJoinSyntax) result).setOraclePriorPosition(oraclePrior); } - while (!exclamationMarks.isEmpty()) { + while (exclamationMarks != null && !exclamationMarks.isEmpty()) { result = new NotExpression(result, exclamationMarks.pollFirst()); } return result; From d1921570a187b1bc981ca74b4511b6f444e93dbd Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 29 Oct 2026 07:39:16 +0100 Subject: [PATCH 2/4] perf: precompile the whitespace pattern of LikeExpression.KeyWord.from `KeyWord.from` runs for every `LIKE`-family condition the parser builds, and `String.replaceAll` compiled its `\s+` pattern on each call. Parsing `performance.sql` allocated 201 KB per parse for those compilations. The pattern is now a precompiled constant. --- .../expression/operators/relational/LikeExpression.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java index 64d610fa1..125e38a82 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression.operators.relational; import java.util.Locale; +import java.util.regex.Pattern; import net.sf.jsqlparser.expression.BinaryExpression; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; @@ -174,9 +175,11 @@ public LikeExpression withRightExpression(Expression arg0) { public enum KeyWord { LIKE, ILIKE, RLIKE, REGEXP_LIKE, REGEXP, GLOB, SIMILAR_TO, MATCH_ANY, MATCH_ALL, MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_REGEXP; + private static final Pattern WHITESPACE = Pattern.compile("\\s+"); + public static KeyWord from(String keyword) { return Enum.valueOf(KeyWord.class, - keyword.toUpperCase(Locale.ROOT).replaceAll("\\s+", "_")); + WHITESPACE.matcher(keyword.toUpperCase(Locale.ROOT)).replaceAll("_")); } } From a6624b7d3e255a98e2e2dd7f933fb964b87b2541 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 29 Oct 2026 08:50:53 +0100 Subject: [PATCH 3/4] perf: create PrimaryExpression's sign and JSON operator lists on first use `PrimaryExpression()` created two lists on every call: one for chained signs such as `- -1` and one for JSON operators such as `->`. Nearly every primary expression has neither, so both lists usually stayed empty. They are now created at their first element. --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 494219be4..a8a9219dd 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -13198,21 +13198,29 @@ Expression PrimaryExpression() #PrimaryExpression: Token token = null; Token sign = null; Token innerSign; - List innerSigns = new ArrayList(); + List innerSigns = null; ColDataType type = null; boolean not = false; boolean exclamationMarkNot = false; boolean dateExpressionAllowed = true; ExpressionList list; - final List> jsonIdents = new ArrayList>(); + List> jsonIdents = null; } { [ { not=true; } | "!" { not=true; exclamationMarkNot=true; } ] [ ( sign="+" | sign="-" | sign="~" ) // DuckDB's chained signs, e.g. - -1 or +-+1 - ( LOOKAHEAD(1) ( innerSign="+" | innerSign="-" | innerSign="~" ) { innerSigns.add(innerSign); } )* + ( + LOOKAHEAD(1) ( innerSign="+" | innerSign="-" | innerSign="~" ) + { + if (innerSigns == null) { + innerSigns = new ArrayList(); + } + innerSigns.add(innerSign); + } + )* ] ( { retval = new NullValue(); } @@ -13411,6 +13419,9 @@ Expression PrimaryExpression() #PrimaryExpression: expression=SimpleExpression() ) { + if (jsonIdents == null) { + jsonIdents = new ArrayList>(); + } jsonIdents.add(new AbstractMap.SimpleEntry(expression, token.image )); } )+ @@ -13430,8 +13441,10 @@ Expression PrimaryExpression() #PrimaryExpression: timezoneExpr.setLeftExpression(retval); retval=timezoneExpr; } - for (int i = innerSigns.size() - 1; i >= 0; i--) { - retval = new SignedExpression(innerSigns.get(i).image.charAt(0), retval); + if (innerSigns != null) { + for (int i = innerSigns.size() - 1; i >= 0; i--) { + retval = new SignedExpression(innerSigns.get(i).image.charAt(0), retval); + } } if (sign != null) { retval = new SignedExpression(sign.image.charAt(0), retval); From d896a6af5f103cfbb5d16f9b8ef22870e5e538b5 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 29 Oct 2026 08:50:53 +0100 Subject: [PATCH 4/4] perf: match XML function names without joining the name parts `consumeXmlLeadingKeyword` and `FunctionArgumentList` called `Function.getName()` for every function to check for `XMLPARSE`, `XMLELEMENT` or `XMLFOREST`, and `getName()` joins the name parts into a new string on each call. Both checks now compare the single name part. --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index a8a9219dd..103dbd21b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1333,12 +1333,8 @@ public class CCJSqlParser extends AbstractJSqlParser { * everywhere else, e.g. f(content). */ private void consumeXmlLeadingKeyword(Function function) { - String name = function.getName(); - if (name == null) { - return; - } - name = name.toUpperCase(java.util.Locale.ROOT); - if (!"XMLPARSE".equals(name) && !"XMLELEMENT".equals(name) && !"XMLFOREST".equals(name)) { + if (!hasUnqualifiedName(function, "XMLPARSE") && !hasUnqualifiedName(function, "XMLELEMENT") + && !hasUnqualifiedName(function, "XMLFOREST")) { return; } Token token = getToken(1); @@ -1358,6 +1354,12 @@ public class CCJSqlParser extends AbstractJSqlParser { function.setExtraKeyword(getNextToken().image); } + // Matches the single name part, since getName() joins the parts into a new string on each call. + private static boolean hasUnqualifiedName(Function function, String name) { + List nameParts = function.getMultipartName(); + return nameParts != null && nameParts.size() == 1 && name.equalsIgnoreCase(nameParts.get(0)); + } + /** * An unquoted YEAR, MONTH, DAY, HOUR, MINUTE or SECOND passed on its own as a function argument, as in * DATEADD(day, 1, d), is a date unit. Anywhere else it is an ordinary identifier, so it is parsed as a @@ -15439,7 +15441,7 @@ ExpressionList FunctionArgumentList(Function retval): Token extraKeywordToken; } { - { if ("XMLFOREST".equalsIgnoreCase(retval.getName())) { return XmlForestArguments(); } } + { if (hasUnqualifiedName(retval, "XMLFOREST")) { return XmlForestArguments(); } } [ LOOKAHEAD(2) extraKeywordToken = { retval.setExtraKeyword(extraKeywordToken.image); } ] expressionList=ExpressionList() // explicit lookahead: InternalFunction accepts ORDER BY after IGNORE NULLS too