diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index f38264f5e6..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; @@ -48,7 +49,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; @@ -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 c239c95eaf..38addb1305 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -8099,6 +8099,7 @@ OutputClause OutputClause(): Upsert Upsert(): { Upsert upsert = new Upsert(); + String algorithm; Table table = null; ExpressionList columns; List updateSets; @@ -8115,7 +8116,10 @@ Upsert Upsert(): { upsert.setUpsertType(UpsertType.REPLACE); } | ( - { upsert.setUpsertType(UpsertType.INSERT_OR_REPLACE); } + + // ABORT and FAIL are not keywords of the parser + algorithm=RelObjectName() + { upsert.setUpsertType(insertOrType(algorithm)); } ) ) [ LOOKAHEAD(2) { upsert.setUsingInto(true); } ] 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 ecb1204956..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,8 +10,16 @@ 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; +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 +27,29 @@ 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, + parser -> parser.withDialect(Dialect.SQLITE)); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "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)", + parser -> parser.withDialect(Dialect.SQLITE))); + } }