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
34 changes: 29 additions & 5 deletions src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,41 @@ public abstract class AbstractJSqlParser<P> {
protected boolean errorRecovery = false;
protected List<ParseException> parseErrors = new ArrayList<>();

public enum AdjacentStringLiterals {
OFF, NEWLINE, WHITESPACE
}

public enum Dialect {
ANSI_SQL, ORACLE, MYSQL(Feature.allowBackslashEscapeCharacter,
ANSI_SQL(AdjacentStringLiterals.NEWLINE), ORACLE, MYSQL(AdjacentStringLiterals.OFF,
Feature.allowBackslashEscapeCharacter,
Feature.allowHashLineComments,
Feature.allowDoubleQuotedStrings), MARIADB(Feature.allowBackslashEscapeCharacter,
Feature.allowDoubleQuotedStrings), MARIADB(AdjacentStringLiterals.OFF,
Feature.allowBackslashEscapeCharacter,
Feature.allowHashLineComments,
Feature.allowDoubleQuotedStrings), SQLSERVER(
Feature.allowSquareBracketQuotation), POSTGRESQL, H2, EXASOL;
Feature.allowDoubleQuotedStrings), SQLSERVER(AdjacentStringLiterals.OFF,
Feature.allowSquareBracketQuotation), POSTGRESQL(
AdjacentStringLiterals.NEWLINE), H2, EXASOL;

private final Set<Feature> lexerFeatures;
private final AdjacentStringLiterals adjacentStringLiterals;

Dialect(Feature... lexerFeatures) {
Dialect(AdjacentStringLiterals adjacentStringLiterals, Feature... lexerFeatures) {
this.adjacentStringLiterals = adjacentStringLiterals;
this.lexerFeatures = lexerFeatures.length == 0 ? EnumSet.noneOf(Feature.class)
: EnumSet.copyOf(Arrays.asList(lexerFeatures));
}

Dialect(Feature... lexerFeatures) {
this(AdjacentStringLiterals.OFF, lexerFeatures);
}

public Set<Feature> getLexerFeatures() {
return lexerFeatures;
}

public AdjacentStringLiterals getAdjacentStringLiterals() {
return adjacentStringLiterals;
}
}

public P withSquareBracketQuotation() {
Expand Down Expand Up @@ -82,12 +99,19 @@ public P withTimeOut(long timeOutMillSeconds) {

public P withDialect(Dialect dialect) {
withFeature(Feature.dialect, dialect.name());
if (dialect.getAdjacentStringLiterals() != AdjacentStringLiterals.OFF) {
withFeature(Feature.adjacentStringLiterals, dialect.getAdjacentStringLiterals().name());
}
for (Feature lexerFeature : dialect.getLexerFeatures()) {
withFeature(lexerFeature, true);
}
return me();
}

public P withAdjacentStringLiterals(AdjacentStringLiterals adjacentStringLiterals) {
return withFeature(Feature.adjacentStringLiterals, adjacentStringLiterals.name());
}

public P withAllowedNestingDepth(int allowedNestingDepth) {
return withFeature(Feature.allowedNestingDepth, allowedNestingDepth);
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,14 @@ public enum Feature {
*/
allowDoubleQuotedStrings(false),

/**
* concatenates adjacent String Literals: NEWLINE when separated by whitespace with at least one
* newline (the SQL standard and PostgreSQL), WHITESPACE across any whitespace (GoogleSQL,
* Spark/Databricks); OFF by default, where the second literal stays an alias (MySQL, SQL
* Server) or fails (everywhere else)
*/
adjacentStringLiterals("OFF"),

/**
* allows MySQL `#` line comments; disabled by default, where a lone `#` stays the binary
* operator (#2507: PostgreSQL bitwise XOR / geometric intersection)
Expand Down
26 changes: 26 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,24 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
* This replaces an expensive syntactic LOOKAHEAD(NamedExpressionListExprFirst())
* that caused exponential backtracking with deeply nested expressions.
*/
/**
* Semantic lookahead for the adjacent string literal concatenation in PrimaryExpression:
* true when Feature.adjacentStringLiterals is on and the next token is a string literal
* meeting the mode's separation rule (WHITESPACE: any, NEWLINE: a newline between the
* tokens, detected through the token line numbers).
*/
protected boolean isAdjacentStringConcat() {
String mode = getAsString(Feature.adjacentStringLiterals);
if (mode == null || AdjacentStringLiterals.OFF.name().equals(mode)) {
return false;
}
if (getToken(1).kind != S_CHAR_LITERAL) {
return false;
}
return AdjacentStringLiterals.WHITESPACE.name().equals(mode)
|| getToken(1).beginLine > getToken(0).endLine;
}

protected boolean isNamedExprListAhead() {
int depth = 0;
for (int i = 1; ; i++) {
Expand Down Expand Up @@ -8319,6 +8337,7 @@ Expression PrimaryExpression() #PrimaryExpression:
Expression timezoneRightExpr = null;
Token token = null;
Token sign = null;
Token adjacentToken = null;
String tmp = "";
ColDataType type = null;
boolean not = false;
Expand Down Expand Up @@ -8419,6 +8438,13 @@ Expression PrimaryExpression() #PrimaryExpression:
| LOOKAHEAD(2, {!interrupted}) (token=<K_TRUE> | token=<K_FALSE>) { retval = new BooleanValue(token.image); }

| token=<S_CHAR_LITERAL> { retval = new StringValue(token.image); linkAST(retval,jjtThis); }
( LOOKAHEAD({ isAdjacentStringConcat() })
adjacentToken=<S_CHAR_LITERAL>
{
((StringValue) retval)
.setValue(((StringValue) retval).getValue() + new StringValue(adjacentToken.image).getValue());
}
)*

| "{d" token=<S_CHAR_LITERAL> "}" { retval = new DateValue(token.image); }

Expand Down
4 changes: 2 additions & 2 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ Define the Parser Features

JSQLParser interprets Squared Brackets ``[..]`` as Arrays, which does not work with MS SQL Server and T-SQL. Please use the Parser Features to instruct JSQLParser to read Squared Brackets as Quotes instead.

JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. JSQLParser reads Double Quotes ``".."`` as quoted identifiers (ANSI SQL); reading them as String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode) needs the ``DoubleQuotedStrings`` parser feature.
JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. JSQLParser reads Double Quotes ``".."`` as quoted identifiers (ANSI SQL); reading them as String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode) needs the ``DoubleQuotedStrings`` parser feature. Adjacent String Literals concatenate optionally: only across a newline (``NEWLINE``, the SQL standard and PostgreSQL) or across any whitespace (``WHITESPACE``, GoogleSQL and Spark/Databricks).

Additionally there are Features to control the Parser's effort at the cost of the performance.

Expand Down Expand Up @@ -319,7 +319,7 @@ Additionally there are Features to control the Parser's effort at the cost of th
.withBackslashEscapeCharacter(true)
);

Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter``, ``withHashLineComments`` and ``withDoubleQuotedStrings`` (MySQL and MariaDB syntax, the latter for the default sql_mode), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. Features set explicitly after the dialect preset win over the preset.
Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter``, ``withHashLineComments`` and ``withDoubleQuotedStrings`` (MySQL and MariaDB syntax, the latter for the default sql_mode), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. ``withDialect(Dialect.POSTGRESQL)`` and ``withDialect(Dialect.ANSI_SQL)`` turn on the newline rule for adjacent String Literals. Features set explicitly after the dialect preset win over the preset.

.. code-block:: java

Expand Down
63 changes: 63 additions & 0 deletions src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -655,4 +655,67 @@ public void testDoubleQuotedStringsPreset() throws Exception {
p -> p.withDialect(AbstractJSqlParser.Dialect.SQLSERVER))).getSelectBody();
assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column);
}

@Test
public void testAdjacentStringLiteralsNewline() throws Exception {
// default (off): the MySQL / SQL Server alias reading, unchanged
assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a' 'b'").toString());
assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'").toString());
assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE x = 'a'\n'b'"));
// newline mode: the standard / Postgres reading, "Two string constants
// that are only separated by whitespace with at least one newline are
// concatenated"
PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT 'a'\n'b'",
p -> p.withAdjacentStringLiterals(
AbstractJSqlParser.AdjacentStringLiterals.NEWLINE)))
.getSelectBody();
Expression expression = select.getSelectItems().get(0).getExpression();
assertTrue(expression instanceof StringValue);
assertEquals("ab", ((StringValue) expression).getValue());
assertEquals("SELECT 'ab'", select.toString());
// same line keeps the alias reading under newline mode
assertEquals("SELECT 'a' 'b'",
CCJSqlParserUtil.parse("SELECT 'a' 'b'",
p -> p.withAdjacentStringLiterals(
AbstractJSqlParser.AdjacentStringLiterals.NEWLINE))
.toString());
// expression positions concatenate too
assertEquals("SELECT * FROM t WHERE x = 'ab'",
CCJSqlParserUtil.parse("SELECT * FROM t WHERE x = 'a'\n'b'",
p -> p.withAdjacentStringLiterals(
AbstractJSqlParser.AdjacentStringLiterals.NEWLINE))
.toString());
// three parts merge into one literal
assertEquals("SELECT 'abc'",
CCJSqlParserUtil.parse("SELECT 'a'\n'b'\n'c'",
p -> p.withAdjacentStringLiterals(
AbstractJSqlParser.AdjacentStringLiterals.NEWLINE))
.toString());
// the ANSI SQL and Postgres presets carry it, the MySQL preset does not
for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] {
AbstractJSqlParser.Dialect.ANSI_SQL, AbstractJSqlParser.Dialect.POSTGRESQL}) {
assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'",
p -> p.withDialect(dialect)).toString());
}
assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'",
p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL)).toString());
}

@Test
public void testAdjacentStringLiteralsWhitespace() throws Exception {
// whitespace mode: the GoogleSQL / Spark reading, same line
// concatenates (their literal chunking)
assertEquals("SELECT 'ab'",
CCJSqlParserUtil.parse("SELECT 'a' 'b'",
p -> p.withAdjacentStringLiterals(
AbstractJSqlParser.AdjacentStringLiterals.WHITESPACE))
.toString());
// combined with allowDoubleQuotedStrings: the BigQuery chunking shape
assertEquals("SELECT \"12\"",
CCJSqlParserUtil.parse("SELECT \"1\" \"2\"",
p -> p.withDoubleQuotedStrings(true).withAdjacentStringLiterals(
AbstractJSqlParser.AdjacentStringLiterals.WHITESPACE))
.toString());
}
}
Loading