From 93c0f323397abaca7d92d48562b1357a3035a18e Mon Sep 17 00:00:00 2001 From: ItsDeadlyProgrammer Date: Fri, 23 Oct 2026 11:22:15 +0530 Subject: [PATCH 1/3] feat: parse INSERT OR ABORT, FAIL, IGNORE and ROLLBACK (SQLite) UpsertType and the deparsers already had these values, but the grammar only accepted INSERT OR REPLACE. Part of #2852 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 17 +++++++++++++- .../statement/select/SQLiteTest.java | 22 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index c239c95eaf..a731dd0f18 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -8115,7 +8115,22 @@ Upsert Upsert(): { upsert.setUpsertType(UpsertType.REPLACE); } | ( - { upsert.setUpsertType(UpsertType.INSERT_OR_REPLACE); } + + ( + { upsert.setUpsertType(UpsertType.INSERT_OR_REPLACE); } + | + { upsert.setUpsertType(UpsertType.INSERT_OR_IGNORE); } + | + { upsert.setUpsertType(UpsertType.INSERT_OR_ROLLBACK); } + | + // SQLite conflict resolution algorithms that are not keywords of the parser + tk= { + requireDdlSyntax("ABORT".equalsIgnoreCase(tk.image) || "FAIL".equalsIgnoreCase(tk.image), + "Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK"); + upsert.setUpsertType("ABORT".equalsIgnoreCase(tk.image) + ? UpsertType.INSERT_OR_ABORT : UpsertType.INSERT_OR_FAIL); + } + ) ) ) [ LOOKAHEAD(2) { upsert.setUsingInto(true); } ] diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java index ecb1204956..53f04b5829 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java @@ -10,8 +10,15 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertThrows; public class SQLiteTest { @Test @@ -19,4 +26,19 @@ void testInsertOrReplaceUpsert() throws JSQLParserException { String sqlString = "INSERT OR REPLACE INTO kjobLocks VALUES (?, ?, ?)"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true); } + + @ParameterizedTest + @ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"}) + void testInsertOrConflictAlgorithm(String algorithm) throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "INSERT OR " + algorithm + " INTO t (a, b) VALUES (1, 2)", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "insert or " + algorithm.toLowerCase(Locale.ROOT) + " into t select * from s", true); + } + + @Test + void testInsertOrUnknownAlgorithm() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("INSERT OR NOTHING INTO t VALUES (1)")); + } } From 7eb65c8fd18c74a151d1a188435a06d13b3df85a Mon Sep 17 00:00:00 2001 From: ItsDeadlyProgrammer Date: Fri, 23 Oct 2026 16:26:59 +0530 Subject: [PATCH 2/3] feat: guard INSERT OR conflict clauses by the SQLITE dialect Add Dialect.SQLITE. INSERT OR ABORT, FAIL, IGNORE and ROLLBACK need it. INSERT OR REPLACE keeps parsing in every dialect. --- .../jsqlparser/parser/AbstractJSqlParser.java | 2 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 19 ++++++++++++++++--- src/site/sphinx/usage.rst | 2 ++ .../statement/select/SQLiteTest.java | 17 ++++++++++++++--- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index f38264f5e6..ffc924693c 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -48,7 +48,7 @@ public enum Dialect { AdjacentStringLiterals.WHITESPACE, Feature.allowDoubleQuotedStrings, Feature.allowBackslashEscapeCharacter), SNOWFLAKE( - Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB, TERADATA; + Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB, TERADATA, SQLITE; private final Set lexerFeatures; private final AdjacentStringLiterals adjacentStringLiterals; diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index a731dd0f18..141948cb1b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3139,6 +3139,11 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + private void requireSqliteSyntax(String syntax) throws ParseException { + requireDdlSyntax(Dialect.SQLITE.name().equals(getAsString(Feature.dialect)), + syntax + " needs Dialect.SQLITE"); + } + private void requireDdlSyntax(boolean valid, String message) throws ParseException { if (!valid) { throw new ParseException(message); @@ -8119,12 +8124,20 @@ Upsert Upsert(): ( { upsert.setUpsertType(UpsertType.INSERT_OR_REPLACE); } | - { upsert.setUpsertType(UpsertType.INSERT_OR_IGNORE); } + // the other conflict resolution algorithms are SQLite syntax + { + requireSqliteSyntax("INSERT OR IGNORE"); + upsert.setUpsertType(UpsertType.INSERT_OR_IGNORE); + } | - { upsert.setUpsertType(UpsertType.INSERT_OR_ROLLBACK); } + { + requireSqliteSyntax("INSERT OR ROLLBACK"); + upsert.setUpsertType(UpsertType.INSERT_OR_ROLLBACK); + } | - // SQLite conflict resolution algorithms that are not keywords of the parser + // ABORT and FAIL are not keywords of the parser tk= { + requireSqliteSyntax("INSERT OR " + tk.image); requireDdlSyntax("ABORT".equalsIgnoreCase(tk.image) || "FAIL".equalsIgnoreCase(tk.image), "Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK"); upsert.setUpsertType("ABORT".equalsIgnoreCase(tk.image) diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index a07b72f088..c784f81d3a 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -1604,6 +1604,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ - ``ALTER TABLE ... ALTER PRIMARY KEY USING COLUMNS (...)`` with optional hash sharding and storage parameters * - ``TERADATA`` - ``UPDATE target FROM sources SET ...`` with the FROM clause before SET + * - ``SQLITE`` + - ``INSERT OR ABORT``, ``INSERT OR FAIL``, ``INSERT OR IGNORE`` and ``INSERT OR ROLLBACK`` conflict clauses Features set explicitly *after* the preset win over it. diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java index 53f04b5829..6ef751ac10 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -31,14 +32,24 @@ void testInsertOrReplaceUpsert() throws JSQLParserException { @ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"}) void testInsertOrConflictAlgorithm(String algorithm) throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed( - "INSERT OR " + algorithm + " INTO t (a, b) VALUES (1, 2)", true); + "INSERT OR " + algorithm + " INTO t (a, b) VALUES (1, 2)", true, + parser -> parser.withDialect(Dialect.SQLITE)); TestUtils.assertSqlCanBeParsedAndDeparsed( - "insert or " + algorithm.toLowerCase(Locale.ROOT) + " into t select * from s", true); + "insert or " + algorithm.toLowerCase(Locale.ROOT) + " into t select * from s", true, + parser -> parser.withDialect(Dialect.SQLITE)); + } + + @ParameterizedTest + @ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "ROLLBACK"}) + void testInsertOrConflictAlgorithmNeedsSqliteDialect(String algorithm) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("INSERT OR " + algorithm + " INTO t VALUES (1)")); } @Test void testInsertOrUnknownAlgorithm() { assertThrows(JSQLParserException.class, - () -> CCJSqlParserUtil.parse("INSERT OR NOTHING INTO t VALUES (1)")); + () -> CCJSqlParserUtil.parse("INSERT OR NOTHING INTO t VALUES (1)", + parser -> parser.withDialect(Dialect.SQLITE))); } } From 7ffb392a8784604c68d03cd8e8755bf66fd0acab Mon Sep 17 00:00:00 2001 From: ItsDeadlyProgrammer Date: Sat, 24 Oct 2026 23:10:03 +0530 Subject: [PATCH 3/3] fix: keep CCJSqlParser under the class size limit for JaCoCo The extra INSERT OR alternatives pushed the constant pool of CCJSqlParser past 65535 entries once JaCoCo instrumented it, so the class was silently left without coverage data and the coverage check failed. Parse the algorithm with RelObjectName and decide in AbstractJSqlParser instead. --- .../jsqlparser/parser/AbstractJSqlParser.java | 25 +++++++++++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 32 +++---------------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index ffc924693c..7cee29a5e8 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.statement.upsert.UpsertType; import java.util.ArrayList; import java.util.Collections; @@ -259,6 +260,30 @@ public Integer getAsInteger(Feature f) { return getConfiguration().getAsInteger(f); } + /** + * The INSERT OR conflict algorithms other than REPLACE are SQLite syntax. + */ + protected UpsertType insertOrType(String algorithm) throws ParseException { + if ("REPLACE".equalsIgnoreCase(algorithm)) { + return UpsertType.INSERT_OR_REPLACE; + } + if (!Dialect.SQLITE.name().equals(getAsString(Feature.dialect))) { + throw new ParseException("INSERT OR " + algorithm + " needs Dialect.SQLITE"); + } + switch (algorithm.toUpperCase()) { + case "IGNORE": + return UpsertType.INSERT_OR_IGNORE; + case "ROLLBACK": + return UpsertType.INSERT_OR_ROLLBACK; + case "ABORT": + return UpsertType.INSERT_OR_ABORT; + case "FAIL": + return UpsertType.INSERT_OR_FAIL; + default: + throw new ParseException("Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK"); + } + } + public String getAsString(Feature f) { return getConfiguration().getAsString(f); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 141948cb1b..38addb1305 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3139,11 +3139,6 @@ public class CCJSqlParser extends AbstractJSqlParser { } } - private void requireSqliteSyntax(String syntax) throws ParseException { - requireDdlSyntax(Dialect.SQLITE.name().equals(getAsString(Feature.dialect)), - syntax + " needs Dialect.SQLITE"); - } - private void requireDdlSyntax(boolean valid, String message) throws ParseException { if (!valid) { throw new ParseException(message); @@ -8104,6 +8099,7 @@ OutputClause OutputClause(): Upsert Upsert(): { Upsert upsert = new Upsert(); + String algorithm; Table table = null; ExpressionList columns; List updateSets; @@ -8121,29 +8117,9 @@ Upsert Upsert(): | ( - ( - { upsert.setUpsertType(UpsertType.INSERT_OR_REPLACE); } - | - // the other conflict resolution algorithms are SQLite syntax - { - requireSqliteSyntax("INSERT OR IGNORE"); - upsert.setUpsertType(UpsertType.INSERT_OR_IGNORE); - } - | - { - requireSqliteSyntax("INSERT OR ROLLBACK"); - upsert.setUpsertType(UpsertType.INSERT_OR_ROLLBACK); - } - | - // ABORT and FAIL are not keywords of the parser - tk= { - requireSqliteSyntax("INSERT OR " + tk.image); - requireDdlSyntax("ABORT".equalsIgnoreCase(tk.image) || "FAIL".equalsIgnoreCase(tk.image), - "Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK"); - upsert.setUpsertType("ABORT".equalsIgnoreCase(tk.image) - ? UpsertType.INSERT_OR_ABORT : UpsertType.INSERT_OR_FAIL); - } - ) + // ABORT and FAIL are not keywords of the parser + algorithm=RelObjectName() + { upsert.setUpsertType(insertOrType(algorithm)); } ) ) [ LOOKAHEAD(2) { upsert.setUsingInto(true); } ]