From b3fc3b4c6bfece96216dac57fcee99dc2df151c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Thu, 15 Oct 2026 22:24:18 +0800 Subject: [PATCH 1/4] perf: use typed fast paths in FeatureConfiguration getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 付典 --- .../parser/feature/FeatureConfiguration.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java b/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java index 0106431cc3..790c4e9a90 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java @@ -57,19 +57,24 @@ public Object getValue(Feature feature) { } public boolean getAsBoolean(Feature f) { - return Boolean.parseBoolean(String.valueOf(getValue(f))); + Object value = getValue(f); + return value instanceof Boolean ? (Boolean) value + : Boolean.parseBoolean(String.valueOf(value)); } public Long getAsLong(Feature f) { - return Long.valueOf(String.valueOf(getValue(f))); + Object value = getValue(f); + return value instanceof Long ? (Long) value : Long.valueOf(String.valueOf(value)); } public int getAsInt(Feature f) { - return Integer.parseInt(String.valueOf(getValue(f))); + Object value = getValue(f); + return value instanceof Integer ? (Integer) value : Integer.parseInt(String.valueOf(value)); } public Integer getAsInteger(Feature f) { - return Integer.parseInt(String.valueOf(getValue(f))); + Object value = getValue(f); + return value instanceof Integer ? (Integer) value : Integer.valueOf(String.valueOf(value)); } public String getAsString(Feature f) { From 1e6849cc25e8ee52fb567edcdb1321093750b836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Thu, 15 Oct 2026 22:24:19 +0800 Subject: [PATCH 2/4] perf: cache lexical configuration lookups and trim per-expression checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token actions query the wired FeatureConfiguration for every identifier, string literal and quoted identifier; PrimaryExpression pays new predicate checks after every expression. Cache the derived dialect/flag answers on the wired configuration's identity (it is only mutated before parsing starts), lazily allocate the DuckDB chained-sign list, reject non-operator images before the PostgreSQL json-access equals chain, and hoist the unsupported statements flag out of the capture loop. Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 137 +++++++++++++++--- 1 file changed, 117 insertions(+), 20 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2210dbe9d8..2b1b717eab 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -182,7 +182,10 @@ public class CCJSqlParser extends AbstractJSqlParser { } private void captureOpaqueStatements(Statements statements) throws ParseException { - while (getAsBoolean(Feature.allowUnsupportedStatements) && isOpaqueStatementAhead()) { + if (!getAsBoolean(Feature.allowUnsupportedStatements)) { + return; + } + while (isOpaqueStatementAhead()) { statements.add(UnsupportedStatement()); while (getToken(1).kind == ST_SEMICOLON) { getNextToken(); @@ -694,8 +697,14 @@ public class CCJSqlParser extends AbstractJSqlParser { } private boolean isPostgreSqlJsonAccess(Token token) { - return ("->".equals(token.image) || "->>".equals(token.image) - || "#>".equals(token.image) || "#>>".equals(token.image)) + // reject non-operator images with one charAt before the equals chain: this + // runs after every primary expression + String image = token.image; + if (image == null || image.length() < 2 || (image.charAt(0) != '-' && image.charAt(0) != '#')) { + return false; + } + return ("->".equals(image) || "->>".equals(image) + || "#>".equals(image) || "#>>".equals(image)) && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)); } @@ -2704,6 +2713,95 @@ PARSER_END(CCJSqlParser) TOKEN_MGR_DECLS : { public FeatureConfiguration configuration = new FeatureConfiguration(); + // The configuration is wired before parsing starts (CCJSqlParser.getConfiguration / + // withConfiguration) and no lexical action mutates it mid-parse. Cache the dialect and + // flag lookups the per-token actions need, keyed on the wired configuration's identity, + // so every token pays a field read instead of a map lookup. + private FeatureConfiguration lexicalConfiguration; + private boolean lexicalPostgreSqlDialect; + private boolean lexicalBigQueryDialect; + private boolean lexicalAllowHashLineComments; + private boolean lexicalAllowBackslashEscapes; + private boolean lexicalAllowDoubleQuotedStrings; + private boolean lexicalAllowSquareBracketQuotation; + + private void refreshLexicalConfiguration(FeatureConfiguration wired) { + lexicalConfiguration = wired; + Object dialect = wired.getValue(Feature.dialect); + lexicalPostgreSqlDialect = AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(dialect); + lexicalBigQueryDialect = AbstractJSqlParser.Dialect.BIGQUERY.name().equals(dialect); + lexicalAllowHashLineComments = Boolean.TRUE.equals(wired.getValue(Feature.allowHashLineComments)); + lexicalAllowBackslashEscapes = wired.getAsBoolean(Feature.allowBackslashEscapeCharacter); + lexicalAllowDoubleQuotedStrings = wired.getAsBoolean(Feature.allowDoubleQuotedStrings); + lexicalAllowSquareBracketQuotation = wired.getAsBoolean(Feature.allowSquareBracketQuotation); + } + + private boolean isPostgreSqlDialect() { + FeatureConfiguration wired = input_stream.featureConfiguration; + if (wired == null) { + return false; + } + if (wired != lexicalConfiguration) { + refreshLexicalConfiguration(wired); + } + return lexicalPostgreSqlDialect; + } + + private boolean isBigQueryDialect() { + FeatureConfiguration wired = input_stream.featureConfiguration; + if (wired == null) { + return false; + } + if (wired != lexicalConfiguration) { + refreshLexicalConfiguration(wired); + } + return lexicalBigQueryDialect; + } + + private boolean isAllowHashLineComments() { + FeatureConfiguration wired = input_stream.featureConfiguration; + if (wired == null) { + return false; + } + if (wired != lexicalConfiguration) { + refreshLexicalConfiguration(wired); + } + return lexicalAllowHashLineComments; + } + + private boolean isAllowBackslashEscapes() { + FeatureConfiguration wired = input_stream.featureConfiguration; + if (wired == null) { + return false; + } + if (wired != lexicalConfiguration) { + refreshLexicalConfiguration(wired); + } + return lexicalAllowBackslashEscapes; + } + + private boolean isAllowDoubleQuotedStrings() { + FeatureConfiguration wired = input_stream.featureConfiguration; + if (wired == null) { + return false; + } + if (wired != lexicalConfiguration) { + refreshLexicalConfiguration(wired); + } + return lexicalAllowDoubleQuotedStrings; + } + + private boolean isAllowSquareBracketQuotation() { + FeatureConfiguration wired = input_stream.featureConfiguration; + if (wired == null) { + return false; + } + if (wired != lexicalConfiguration) { + refreshLexicalConfiguration(wired); + } + return lexicalAllowSquareBracketQuotation; + } + // Identify the index of the quoting/escaping tokens public int charLiteralIndex = -1; @@ -2768,7 +2866,7 @@ TOKEN_MGR_DECLS : { // disambiguation can fetch PATTERN tokens before its production is entered. if (rowPatternContext == null && t.kind == S_IDENTIFIER && "MATCH_RECOGNIZE".equalsIgnoreCase(t.image) - && AbstractJSqlParser.Dialect.BIGQUERY.name().equals(configuration.getValue(Feature.dialect))) { + && isBigQueryDialect()) { rowPatternContext = new RowPatternTokenContext(); } if (rowPatternContext != null) { @@ -3640,8 +3738,7 @@ TOKEN: // that never opted in (the stream is only wired through the feature // consumers / withConfiguration); getValue avoids the String-based // getAsBoolean roundtrip - if (matchedToken.kind == S_IDENTIFIER && input_stream.featureConfiguration != null - && Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) { + if (matchedToken.kind == S_IDENTIFIER && isAllowHashLineComments()) { int hashIndex = matchedToken.image.indexOf('#'); if (hashIndex > 0) { truncateIdentifierToken(matchedToken, hashIndex); @@ -3649,8 +3746,7 @@ TOKEN: } // PostgreSQL does not allow # or @ in unquoted identifiers. Re-lex them as // operators, including #>, #>>, #- and @?, when touching the left operand. - if (matchedToken.kind == S_IDENTIFIER - && AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(configuration.getValue(Feature.dialect))) { + if (matchedToken.kind == S_IDENTIFIER && isPostgreSqlDialect()) { int hashIndex = matchedToken.image.indexOf('#'); int atIndex = matchedToken.image.indexOf('@'); int operatorIndex = hashIndex < 0 ? atIndex @@ -3676,8 +3772,7 @@ TOKEN: // that never opted in (the stream is only wired through the feature // consumers / withConfiguration); getValue avoids the String-based // getAsBoolean roundtrip - if (input_stream.featureConfiguration != null - && Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) { + if (isAllowHashLineComments()) { int hashIndex = matchedToken.image.indexOf('#'); if (hashIndex > 0) { truncateIdentifierToken(matchedToken, hashIndex); @@ -3728,8 +3823,8 @@ TOKEN: // escapes the next character only with backslash escapes or in a PostgreSQL E'' string; // otherwise the literal ends at the first quote that is not doubled, so LIKE ... ESCAPE '\\' // still works. - boolean backslashEscapes = configuration.getAsBoolean(Feature.allowBackslashEscapeCharacter) - || (AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(configuration.getValue(Feature.dialect)) + boolean backslashEscapes = isAllowBackslashEscapes() + || (isPostgreSqlDialect() && matchedToken.image.regionMatches(true, 0, "E'", 0, 2)); int end = closingSingleQuote(matchedToken.image, backslashEscapes); if (end > 0 && end < matchedToken.image.length()) { @@ -3747,14 +3842,13 @@ TOKEN: // the active quoting rules: a backslash escapes the next character only in // double-quoted strings with backslash escapes, as in MySQL. int end = closingQuote(matchedToken.image, 0, '"', - configuration.getAsBoolean(Feature.allowDoubleQuotedStrings) - && configuration.getAsBoolean(Feature.allowBackslashEscapeCharacter)); + isAllowDoubleQuotedStrings() && isAllowBackslashEscapes()); if (end > 0 && end < matchedToken.image.length()) { matchedToken.image = image.substring(0, end); input_stream.backup(image.length() - end); } } - if ( !configuration.getAsBoolean(Feature.allowSquareBracketQuotation) + if ( !isAllowSquareBracketQuotation() && matchedToken.image.charAt(0) == '[' ) { matchedToken.image = "["; @@ -3762,7 +3856,7 @@ TOKEN: matchedToken.kind = squaredBracketOpenIndex; input_stream.backup(image.length() - 1); } - if ( configuration.getAsBoolean(Feature.allowDoubleQuotedStrings) + if ( isAllowDoubleQuotedStrings() && matchedToken.image.charAt(0) == '"' ) { // `charLiteralIndex` defined in TokenManagerDeclaration above matchedToken.kind = charLiteralIndex; @@ -12019,7 +12113,7 @@ 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; @@ -12033,7 +12127,8 @@ Expression PrimaryExpression() #PrimaryExpression: [ ( 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(); } @@ -12249,8 +12344,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 643b2adf4a81dffcf5662782eb02ea8b4e09e4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Thu, 15 Oct 2026 22:24:19 +0800 Subject: [PATCH 3/4] perf: cache derived feature flags in SimpleCharStream.BeginToken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 付典 --- .../jsqlparser/parser/SimpleCharStream.java | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java index 7530045e57..142c009002 100644 --- a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java +++ b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java @@ -68,6 +68,24 @@ public class SimpleCharStream { String mySqlCommentError; boolean mySqlMinusSubstitution; + // derived from featureConfiguration, refreshed on identity change: the + // configuration is wired before parsing starts and is not mutated mid-parse + private FeatureConfiguration derivedFrom; + private boolean derivedMySqlDialect; + private boolean derivedBlankLineSeparator = true; + private boolean derivedAllowHashLineComments; + + private void refreshDerivedFeatures() { + derivedFrom = featureConfiguration; + derivedMySqlDialect = featureConfiguration != null + && AbstractJSqlParser.Dialect.MYSQL.name() + .equals(featureConfiguration.getValue(Feature.dialect)); + derivedBlankLineSeparator = featureConfiguration == null + || featureConfiguration.getAsBoolean(Feature.allowBlankLinesAsStatementSeparator); + derivedAllowHashLineComments = featureConfiguration != null + && featureConfiguration.getAsBoolean(Feature.allowHashLineComments); + } + /** * Constructor. */ @@ -201,17 +219,16 @@ public final char BeginToken() throws java.io.IOException { absoluteTokenBegin = totalCharsRead; - if (c == '#' && featureConfiguration != null - && featureConfiguration.getAsBoolean(Feature.allowHashLineComments)) { + if (featureConfiguration != derivedFrom) { + refreshDerivedFeatures(); + } + + if (c == '#' && derivedAllowHashLineComments) { buffer[bufpos] = HASH_SUBSTITUTION; c = HASH_SUBSTITUTION; - } else if ((c == '-' || c == '/' || c == '*') && featureConfiguration != null - && AbstractJSqlParser.Dialect.MYSQL.name() - .equals(featureConfiguration.getValue(Feature.dialect))) { + } else if ((c == '-' || c == '/' || c == '*') && derivedMySqlDialect) { c = mySqlCommentBoundary(c); - } else if (c == '\n' && featureConfiguration != null - && !featureConfiguration - .getAsBoolean(Feature.allowBlankLinesAsStatementSeparator) + } else if (c == '\n' && !derivedBlankLineSeparator && peekCharacters(2).equals("\n\n")) { // Blanking the first newline of the run stops ST_SEMICOLON matching "\n\n\n"; // line and column tracking was already updated when the newline was read. From 0f911fa7bf5dc1a80159eb46a9648e5abd748768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Fri, 16 Oct 2026 09:01:45 +0800 Subject: [PATCH 4/4] fix: invalidate cached lexical features on configuration changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track configuration mutations as well as object identity, preserve the public token manager configuration, and avoid caching dynamic subclass values. Cover parser reuse, in-place changes, replacement, and direct lexer APIs with 33 regression checks. Signed-off-by: 付典 --- .../jsqlparser/parser/SimpleCharStream.java | 12 +- .../parser/feature/FeatureConfiguration.java | 7 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 74 ++-- .../parser/LexicalConfigurationTest.java | 318 ++++++++++++++++++ 4 files changed, 356 insertions(+), 55 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/parser/LexicalConfigurationTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java index 142c009002..939b264104 100644 --- a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java +++ b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java @@ -68,15 +68,14 @@ public class SimpleCharStream { String mySqlCommentError; boolean mySqlMinusSubstitution; - // derived from featureConfiguration, refreshed on identity change: the - // configuration is wired before parsing starts and is not mutated mid-parse + // ReInit retains the configuration, which callers can mutate between parses. private FeatureConfiguration derivedFrom; + private long derivedModificationCount; private boolean derivedMySqlDialect; private boolean derivedBlankLineSeparator = true; private boolean derivedAllowHashLineComments; private void refreshDerivedFeatures() { - derivedFrom = featureConfiguration; derivedMySqlDialect = featureConfiguration != null && AbstractJSqlParser.Dialect.MYSQL.name() .equals(featureConfiguration.getValue(Feature.dialect)); @@ -84,6 +83,9 @@ private void refreshDerivedFeatures() { || featureConfiguration.getAsBoolean(Feature.allowBlankLinesAsStatementSeparator); derivedAllowHashLineComments = featureConfiguration != null && featureConfiguration.getAsBoolean(Feature.allowHashLineComments); + derivedFrom = featureConfiguration; + derivedModificationCount = featureConfiguration == null ? 0 + : featureConfiguration.getModificationCount(); } /** @@ -219,7 +221,9 @@ public final char BeginToken() throws java.io.IOException { absoluteTokenBegin = totalCharsRead; - if (featureConfiguration != derivedFrom) { + if (featureConfiguration != derivedFrom || featureConfiguration != null + && (featureConfiguration.getModificationCount() != derivedModificationCount + || featureConfiguration.getClass() != FeatureConfiguration.class)) { refreshDerivedFeatures(); } diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java b/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java index 790c4e9a90..068e4c852d 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/FeatureConfiguration.java @@ -20,6 +20,7 @@ public class FeatureConfiguration { private static final Logger LOG = Logger.getLogger(FeatureConfiguration.class.getName()); private final Map featureEnabled = new EnumMap<>(Feature.class); + private long modificationCount; public FeatureConfiguration() { // set default-value for all switchable features @@ -35,6 +36,7 @@ public FeatureConfiguration() { public FeatureConfiguration setValue(Feature feature, Object value) { if (feature.isConfigurable()) { featureEnabled.put(feature, value); + modificationCount++; } else { if (LOG.isLoggable(Level.WARNING)) { LOG.warning(feature.name() + " is not switchable - cannot set enabled = " + value); @@ -43,6 +45,11 @@ public FeatureConfiguration setValue(Feature feature, Object value) { return this; } + /** Returns a change stamp for caches of derived feature values. */ + public long getModificationCount() { + return modificationCount; + } + /** * @param feature * @return the configured feature value - can be null diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2b1b717eab..9d49d020fa 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2713,11 +2713,10 @@ PARSER_END(CCJSqlParser) TOKEN_MGR_DECLS : { public FeatureConfiguration configuration = new FeatureConfiguration(); - // The configuration is wired before parsing starts (CCJSqlParser.getConfiguration / - // withConfiguration) and no lexical action mutates it mid-parse. Cache the dialect and - // flag lookups the per-token actions need, keyed on the wired configuration's identity, - // so every token pays a field read instead of a map lookup. + // The public configuration can be replaced or mutated, including between ReInit calls. + // Subclasses may provide dynamic values without going through setValue. private FeatureConfiguration lexicalConfiguration; + private long lexicalModificationCount; private boolean lexicalPostgreSqlDialect; private boolean lexicalBigQueryDialect; private boolean lexicalAllowHashLineComments; @@ -2725,80 +2724,53 @@ TOKEN_MGR_DECLS : { private boolean lexicalAllowDoubleQuotedStrings; private boolean lexicalAllowSquareBracketQuotation; - private void refreshLexicalConfiguration(FeatureConfiguration wired) { - lexicalConfiguration = wired; - Object dialect = wired.getValue(Feature.dialect); + private void refreshLexicalConfiguration() { + long modificationCount = configuration.getModificationCount(); + if (configuration == lexicalConfiguration && modificationCount == lexicalModificationCount + && configuration.getClass() == FeatureConfiguration.class) { + return; + } + Object dialect = configuration.getValue(Feature.dialect); lexicalPostgreSqlDialect = AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(dialect); lexicalBigQueryDialect = AbstractJSqlParser.Dialect.BIGQUERY.name().equals(dialect); - lexicalAllowHashLineComments = Boolean.TRUE.equals(wired.getValue(Feature.allowHashLineComments)); - lexicalAllowBackslashEscapes = wired.getAsBoolean(Feature.allowBackslashEscapeCharacter); - lexicalAllowDoubleQuotedStrings = wired.getAsBoolean(Feature.allowDoubleQuotedStrings); - lexicalAllowSquareBracketQuotation = wired.getAsBoolean(Feature.allowSquareBracketQuotation); + lexicalAllowHashLineComments = Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments)); + lexicalAllowBackslashEscapes = configuration.getAsBoolean(Feature.allowBackslashEscapeCharacter); + lexicalAllowDoubleQuotedStrings = configuration.getAsBoolean(Feature.allowDoubleQuotedStrings); + lexicalAllowSquareBracketQuotation = configuration.getAsBoolean(Feature.allowSquareBracketQuotation); + lexicalConfiguration = configuration; + lexicalModificationCount = modificationCount; } private boolean isPostgreSqlDialect() { - FeatureConfiguration wired = input_stream.featureConfiguration; - if (wired == null) { - return false; - } - if (wired != lexicalConfiguration) { - refreshLexicalConfiguration(wired); - } + refreshLexicalConfiguration(); return lexicalPostgreSqlDialect; } private boolean isBigQueryDialect() { - FeatureConfiguration wired = input_stream.featureConfiguration; - if (wired == null) { - return false; - } - if (wired != lexicalConfiguration) { - refreshLexicalConfiguration(wired); - } + refreshLexicalConfiguration(); return lexicalBigQueryDialect; } private boolean isAllowHashLineComments() { - FeatureConfiguration wired = input_stream.featureConfiguration; - if (wired == null) { + if (input_stream.featureConfiguration == null) { return false; } - if (wired != lexicalConfiguration) { - refreshLexicalConfiguration(wired); - } + refreshLexicalConfiguration(); return lexicalAllowHashLineComments; } private boolean isAllowBackslashEscapes() { - FeatureConfiguration wired = input_stream.featureConfiguration; - if (wired == null) { - return false; - } - if (wired != lexicalConfiguration) { - refreshLexicalConfiguration(wired); - } + refreshLexicalConfiguration(); return lexicalAllowBackslashEscapes; } private boolean isAllowDoubleQuotedStrings() { - FeatureConfiguration wired = input_stream.featureConfiguration; - if (wired == null) { - return false; - } - if (wired != lexicalConfiguration) { - refreshLexicalConfiguration(wired); - } + refreshLexicalConfiguration(); return lexicalAllowDoubleQuotedStrings; } private boolean isAllowSquareBracketQuotation() { - FeatureConfiguration wired = input_stream.featureConfiguration; - if (wired == null) { - return false; - } - if (wired != lexicalConfiguration) { - refreshLexicalConfiguration(wired); - } + refreshLexicalConfiguration(); return lexicalAllowSquareBracketQuotation; } diff --git a/src/test/java/net/sf/jsqlparser/parser/LexicalConfigurationTest.java b/src/test/java/net/sf/jsqlparser/parser/LexicalConfigurationTest.java new file mode 100644 index 0000000000..631fd9295c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LexicalConfigurationTest.java @@ -0,0 +1,318 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.PlainSelect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class LexicalConfigurationTest { + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void parserOptionsCanChangeAfterReinitialization(boolean enabled) throws Exception { + CCJSqlParser parser = new CCJSqlParser("SELECT \"first\"") + .withDoubleQuotedStrings(!enabled); + assertQuotedExpression(parser, !enabled, "first"); + parser.ReInit("SELECT \"second\""); + parser.withDoubleQuotedStrings(enabled); + assertQuotedExpression(parser, enabled, "second"); + assertSqlCanBeParsedAndDeparsed("SELECT \"second\"", false, + p -> p.withDoubleQuotedStrings(enabled)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void sharedConfigurationCanBeMutatedInPlace(boolean enabled) throws Exception { + FeatureConfiguration configuration = new FeatureConfiguration() + .setValue(Feature.allowDoubleQuotedStrings, !enabled); + CCJSqlParser first = new CCJSqlParser("SELECT \"first\"") + .withConfiguration(configuration); + CCJSqlParser second = new CCJSqlParser("SELECT \"first\"") + .withConfiguration(configuration); + assertQuotedExpression(first, !enabled, "first"); + assertQuotedExpression(second, !enabled, "first"); + configuration.setValue(Feature.allowDoubleQuotedStrings, enabled); + first.ReInit("SELECT \"second\""); + second.ReInit("SELECT \"second\""); + second.withConfiguration(configuration); + assertQuotedExpression(first, enabled, "second"); + assertQuotedExpression(second, enabled, "second"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void parserConfigurationCanBeReplaced(boolean enabled) throws Exception { + CCJSqlParser parser = new CCJSqlParser("SELECT \"first\"") + .withDoubleQuotedStrings(!enabled); + assertQuotedExpression(parser, !enabled, "first"); + parser.ReInit("SELECT \"second\""); + parser.withConfiguration(new FeatureConfiguration() + .setValue(Feature.allowDoubleQuotedStrings, enabled)); + assertQuotedExpression(parser, enabled, "second"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void mySqlDialectCanChangeAfterReinitialization(boolean mysql) throws Exception { + CCJSqlParser parser = new CCJSqlParser("SELECT seed") + .withDialect(mysql ? Dialect.ANSI_SQL : Dialect.MYSQL); + parser.Statement(); + parser.ReInit("SELECT 1--2"); + parser.withDialect(mysql ? Dialect.MYSQL : Dialect.ANSI_SQL); + PlainSelect select = (PlainSelect) parser.Statement(); + assertEquals(mysql ? "SELECT 1 - -2" : "SELECT 1", select.toString()); + if (!mysql) { + assertEquals(1, assertInstanceOf(LongValue.class, + select.getSelectItem(0).getExpression()).getValue()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void hashCommentsCanChangeAfterReinitialization(boolean enabled) throws Exception { + CCJSqlParser parser = new CCJSqlParser("SELECT seed").withHashLineComments(!enabled); + parser.Statement(); + parser.ReInit("SELECT 1 # 2"); + parser.withHashLineComments(enabled); + PlainSelect select = (PlainSelect) parser.Statement(); + assertEquals(enabled ? "SELECT 1" : "SELECT 1 # 2", select.toString()); + if (enabled) { + assertEquals(1, assertInstanceOf(LongValue.class, + select.getSelectItem(0).getExpression()).getValue()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void blankLineSeparatorsCanChangeAfterReinitialization(boolean enabled) throws Exception { + CCJSqlParser parser = new CCJSqlParser("SELECT seed").withBlankLinesAsSeparator(!enabled); + parser.Statement(); + parser.ReInit(enabled ? "SELECT 1\n\n\nSELECT 2" : "SELECT 1\n\n\n + 2"); + parser.withBlankLinesAsSeparator(enabled); + Statements statements = parser.Statements(); + assertEquals(enabled ? 2 : 1, statements.size()); + assertEquals(enabled ? "SELECT 1" : "SELECT 1 + 2", statements.get(0).toString()); + if (enabled) { + assertEquals("SELECT 2", statements.get(1).toString()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void streamConfigurationCanBeReplaced(boolean enabled) throws Exception { + CCJSqlParser parser = new CCJSqlParser("SELECT seed").withHashLineComments(!enabled); + parser.Statement(); + parser.ReInit("SELECT 1 # 2"); + parser.withConfiguration(new FeatureConfiguration() + .setValue(Feature.allowHashLineComments, enabled)); + assertEquals(enabled ? "SELECT 1" : "SELECT 1 # 2", parser.Statement().toString()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void directTokenManagerObservesInPlaceChanges(boolean enabled) { + CCJSqlParserTokenManager lexer = lexer("\"first\" \"second\" \"third\""); + lexer.configuration.setValue(Feature.allowDoubleQuotedStrings, !enabled); + assertQuotedToken(lexer, !enabled, "first"); + lexer.configuration.setValue(Feature.allowDoubleQuotedStrings, enabled); + assertQuotedToken(lexer, enabled, "second"); + lexer.configuration.setValue(Feature.allowDoubleQuotedStrings, !enabled); + assertQuotedToken(lexer, !enabled, "third"); + assertEquals(CCJSqlParserConstants.EOF, lexer.getNextToken().kind); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void directTokenManagerObservesReinitialization(boolean enabled) { + CCJSqlParserTokenManager lexer = lexer("\"first\""); + lexer.configuration.setValue(Feature.allowDoubleQuotedStrings, !enabled); + assertQuotedToken(lexer, !enabled, "first"); + lexer.ReInit(new SimpleCharStream(new StringProvider("\"second\""))); + lexer.configuration.setValue(Feature.allowDoubleQuotedStrings, enabled); + assertQuotedToken(lexer, enabled, "second"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void directTokenManagerObservesConfigurationReplacement(boolean enabled) { + CCJSqlParserTokenManager lexer = lexer("\"first\" \"second\" \"third\""); + FeatureConfiguration original = new FeatureConfiguration() + .setValue(Feature.allowDoubleQuotedStrings, !enabled); + lexer.configuration = original; + assertQuotedToken(lexer, !enabled, "first"); + lexer.configuration = new FeatureConfiguration() + .setValue(Feature.allowDoubleQuotedStrings, enabled); + assertQuotedToken(lexer, enabled, "second"); + original.setValue(Feature.allowDoubleQuotedStrings, enabled); + lexer.configuration = original; + assertQuotedToken(lexer, enabled, "third"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void publicConfigurationRemainsAuthoritativeWithAWiredStream(boolean enabled) + throws Exception { + CCJSqlParser parser = new CCJSqlParser("\"first\" \"second\"") + .withDoubleQuotedStrings(!enabled); + assertQuotedToken(parser.token_source, !enabled, "first"); + parser.token_source.configuration = new FeatureConfiguration() + .setValue(Feature.allowDoubleQuotedStrings, enabled); + assertQuotedToken(parser.token_source, enabled, "second"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void directSquareBracketQuotationUsesPublicConfiguration(boolean enabled) { + CCJSqlParserTokenManager lexer = lexer("seed [second]"); + lexer.configuration.setValue(Feature.allowSquareBracketQuotation, !enabled); + assertEquals("seed", lexer.getNextToken().image); + lexer.configuration.setValue(Feature.allowSquareBracketQuotation, enabled); + Token token = lexer.getNextToken(); + assertEquals(enabled ? CCJSqlParserConstants.S_QUOTED_IDENTIFIER + : lexer.squaredBracketOpenIndex, token.kind); + assertEquals(enabled ? "[second]" : "[", token.image); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void directBackslashEscapesUsePublicConfiguration(boolean enabled) { + CCJSqlParserTokenManager lexer = lexer("'first' 'a\\'b'"); + lexer.configuration.setValue(Feature.allowBackslashEscapeCharacter, !enabled); + assertEquals("'first'", lexer.getNextToken().image); + lexer.configuration.setValue(Feature.allowBackslashEscapeCharacter, enabled); + Token token = lexer.getNextToken(); + assertEquals(CCJSqlParserConstants.S_CHAR_LITERAL, token.kind); + assertEquals(enabled ? "'a\\'b'" : "'a\\'", token.image); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void directPostgreSqlDialectUsesPublicConfiguration(boolean postgresql) { + CCJSqlParserTokenManager lexer = lexer("seed a#>b"); + lexer.configuration.setValue(Feature.dialect, + postgresql ? Dialect.ANSI_SQL.name() : Dialect.POSTGRESQL.name()); + assertEquals("seed", lexer.getNextToken().image); + lexer.configuration.setValue(Feature.dialect, + postgresql ? Dialect.POSTGRESQL.name() : Dialect.ANSI_SQL.name()); + assertEquals(postgresql ? "a" : "a#", lexer.getNextToken().image); + assertEquals(postgresql ? "#>" : ">", lexer.getNextToken().image); + assertEquals("b", lexer.getNextToken().image); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void directBigQueryDialectUsesPublicConfiguration(boolean bigquery) { + CCJSqlParserTokenManager lexer = lexer("seed MATCH_RECOGNIZE (ORDER BY ts PATTERN (A$))"); + lexer.configuration.setValue(Feature.dialect, + bigquery ? Dialect.ANSI_SQL.name() : Dialect.BIGQUERY.name()); + assertEquals("seed", lexer.getNextToken().image); + lexer.configuration.setValue(Feature.dialect, + bigquery ? Dialect.BIGQUERY.name() : Dialect.ANSI_SQL.name()); + List images = new ArrayList<>(); + Token token; + while ((token = lexer.getNextToken()).kind != CCJSqlParserConstants.EOF) { + images.add(token.image); + } + assertEquals(bigquery + ? List.of("MATCH_RECOGNIZE", "(", "ORDER", "BY", "ts", "PATTERN", "(", "A", "$", + ")", ")") + : List.of("MATCH_RECOGNIZE", "(", "ORDER", "BY", "ts", "PATTERN", "(", "A$", ")", + ")"), + images); + } + + @Test + void directHashCommentsStillRequireStreamWiring() { + CCJSqlParserTokenManager lexer = lexer("a#b"); + lexer.configuration.setValue(Feature.allowHashLineComments, true); + assertEquals("a#b", lexer.getNextToken().image); + assertEquals(CCJSqlParserConstants.EOF, lexer.getNextToken().kind); + } + + @Test + void configurationSubclassesCanSupplyDynamicValues() { + AtomicBoolean enabled = new AtomicBoolean(); + FeatureConfiguration configuration = new FeatureConfiguration() { + @Override + public Object getValue(Feature feature) { + return feature == Feature.allowDoubleQuotedStrings ? enabled.get() + : super.getValue(feature); + } + }; + CCJSqlParserTokenManager lexer = lexer("\"first\" \"second\" \"third\""); + lexer.configuration = configuration; + assertQuotedToken(lexer, false, "first"); + enabled.set(true); + assertQuotedToken(lexer, true, "second"); + enabled.set(false); + assertQuotedToken(lexer, false, "third"); + } + + @Test + void streamConfigurationSubclassesCanSupplyDynamicValues() throws Exception { + AtomicBoolean mysql = new AtomicBoolean(); + FeatureConfiguration configuration = new FeatureConfiguration() { + @Override + public Object getValue(Feature feature) { + return feature == Feature.dialect + ? (mysql.get() ? Dialect.MYSQL.name() : Dialect.ANSI_SQL.name()) + : super.getValue(feature); + } + }; + CCJSqlParser parser = new CCJSqlParser("SELECT seed").withConfiguration(configuration); + parser.Statement(); + mysql.set(true); + parser.ReInit("SELECT 1--2"); + assertEquals("SELECT 1 - -2", parser.Statement().toString()); + mysql.set(false); + parser.ReInit("SELECT 1--2"); + assertEquals("SELECT 1", parser.Statement().toString()); + } + + private static CCJSqlParserTokenManager lexer(String sql) { + return new CCJSqlParserTokenManager(new SimpleCharStream(new StringProvider(sql))); + } + + private static void assertQuotedToken(CCJSqlParserTokenManager lexer, boolean string, + String value) { + Token token = lexer.getNextToken(); + assertEquals(string ? CCJSqlParserConstants.S_CHAR_LITERAL + : CCJSqlParserConstants.S_QUOTED_IDENTIFIER, token.kind); + assertEquals('"' + value + '"', token.image); + } + + private static void assertQuotedExpression(CCJSqlParser parser, boolean string, String value) + throws Exception { + PlainSelect select = (PlainSelect) parser.Statement(); + Expression expression = select.getSelectItem(0).getExpression(); + if (string) { + assertEquals(value, assertInstanceOf(StringValue.class, expression).getValue()); + } else { + assertEquals('"' + value + '"', + assertInstanceOf(Column.class, expression).getColumnName()); + } + assertEquals("SELECT \"" + value + "\"", select.toString()); + } +}