From e60aeb9c14b9e16bb2d344b88a43f3907714dc6a Mon Sep 17 00:00:00 2001 From: Dylan Merigaud Date: Sun, 1 Nov 2026 08:59:00 +0100 Subject: [PATCH] feat: parse UPDATE OR conflict algorithms in the SQLite dialect SQLite accepts ABORT, FAIL, IGNORE, REPLACE and ROLLBACK after UPDATE OR. Update gets a conflictAlgorithm (UpdateConflictAlgorithm), printed by toString() and UpdateDeParser. The grammar reads the word with RelObjectName and AbstractJSqlParser.updateOrAlgorithm decides it, as INSERT OR does. UPDATE OR needs Dialect.SQLITE. Part of #2852 --- .../jsqlparser/parser/AbstractJSqlParser.java | 13 ++++++ .../jsqlparser/statement/update/Update.java | 17 ++++++++ .../update/UpdateConflictAlgorithm.java | 23 ++++++++++ .../util/deparser/UpdateDeParser.java | 3 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 4 ++ src/site/sphinx/usage.rst | 3 +- .../statement/select/SQLiteTest.java | 42 +++++++++++++++++++ 7 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/update/UpdateConflictAlgorithm.java diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 7cee29a5e8..28d969ed16 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.update.UpdateConflictAlgorithm; import net.sf.jsqlparser.statement.upsert.UpsertType; import java.util.ArrayList; @@ -284,6 +285,18 @@ protected UpsertType insertOrType(String algorithm) throws ParseException { } } + protected UpdateConflictAlgorithm updateOrAlgorithm(String algorithm) throws ParseException { + if (!Dialect.SQLITE.name().equals(getAsString(Feature.dialect))) { + throw new ParseException("UPDATE OR " + algorithm + " needs Dialect.SQLITE"); + } + for (UpdateConflictAlgorithm conflictAlgorithm : UpdateConflictAlgorithm.values()) { + if (conflictAlgorithm.name().equalsIgnoreCase(algorithm)) { + return conflictAlgorithm; + } + } + throw new ParseException("Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK"); + } + public String getAsString(Feature f) { return getConfiguration().getAsString(f); } diff --git a/src/main/java/net/sf/jsqlparser/statement/update/Update.java b/src/main/java/net/sf/jsqlparser/statement/update/Update.java index 6351832276..defe9e4422 100644 --- a/src/main/java/net/sf/jsqlparser/statement/update/Update.java +++ b/src/main/java/net/sf/jsqlparser/statement/update/Update.java @@ -55,6 +55,7 @@ public class Update extends ASTNodeAccessImpl implements Statement { private Limit limit; private OptionClause option; private ReturningClause returningClause; + private UpdateConflictAlgorithm conflictAlgorithm; private UpdateModifierPriority modifierPriority; private boolean modifierIgnore; @@ -344,6 +345,14 @@ public Update setReturningClause(ReturningClause returningClause) { return this; } + public UpdateConflictAlgorithm getConflictAlgorithm() { + return conflictAlgorithm; + } + + public void setConflictAlgorithm(UpdateConflictAlgorithm conflictAlgorithm) { + this.conflictAlgorithm = conflictAlgorithm; + } + public UpdateModifierPriority getModifierPriority() { return modifierPriority; } @@ -381,6 +390,9 @@ public String toString() { if (oracleHint != null) { b.append(oracleHint).append(" "); } + if (conflictAlgorithm != null) { + b.append("OR ").append(conflictAlgorithm.name()).append(" "); + } if (modifierPriority != null) { b.append(modifierPriority.name()).append(" "); } @@ -505,6 +517,11 @@ public Update withExpressions(List expressions) { return this; } + public Update withConflictAlgorithm(UpdateConflictAlgorithm conflictAlgorithm) { + this.setConflictAlgorithm(conflictAlgorithm); + return this; + } + public Update withModifierPriority(UpdateModifierPriority modifierPriority) { this.setModifierPriority(modifierPriority); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/update/UpdateConflictAlgorithm.java b/src/main/java/net/sf/jsqlparser/statement/update/UpdateConflictAlgorithm.java new file mode 100644 index 0000000000..b140024339 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/update/UpdateConflictAlgorithm.java @@ -0,0 +1,23 @@ +/*- + * #%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.statement.update; + +import java.util.Locale; + +/** + * The conflict resolution algorithm of SQLite's {@code UPDATE OR }. + */ +public enum UpdateConflictAlgorithm { + ABORT, FAIL, IGNORE, REPLACE, ROLLBACK; + + public static UpdateConflictAlgorithm from(String algorithm) { + return Enum.valueOf(UpdateConflictAlgorithm.class, algorithm.toUpperCase(Locale.ROOT)); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java index 8c6b3ab490..a817034ae5 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java @@ -49,6 +49,9 @@ public void deParse(Update update) { if (update.getOracleHint() != null) { builder.append(update.getOracleHint()).append(" "); } + if (update.getConflictAlgorithm() != null) { + builder.append("OR ").append(update.getConflictAlgorithm()).append(" "); + } if (update.getModifierPriority() != null) { builder.append(update.getModifierPriority()).append(" "); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 087c530b43..f2109cf63b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -7703,11 +7703,15 @@ Update Update(): Token tk = null; UpdateModifierPriority modifierPriority = null; boolean modifierIgnore = false; + String algorithm; OutputClause outputClause = null; } { { update.setOracleHint(getOracleHint()); } + // SQLite: UPDATE OR ; ABORT and FAIL are not keywords of the parser + [ algorithm=RelObjectName() + { update.setConflictAlgorithm(updateOrAlgorithm(algorithm)); } ] [ LOOKAHEAD(2) { modifierPriority = UpdateModifierPriority.LOW_PRIORITY; }] [ LOOKAHEAD(2) { modifierIgnore = true; }] table=TableWithAliasAndMysqlIndexHint() diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 250dda5b6f..b201f9b0f2 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -1612,7 +1612,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ * - ``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 + - ``INSERT OR ABORT``, ``INSERT OR FAIL``, ``INSERT OR IGNORE`` and ``INSERT OR ROLLBACK`` conflict clauses; + ``UPDATE OR`` with any of ``ABORT``, ``FAIL``, ``IGNORE``, ``REPLACE`` and ``ROLLBACK`` 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 6ef751ac10..6d58235fe0 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java @@ -12,6 +12,8 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.statement.update.UpdateConflictAlgorithm; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -19,6 +21,8 @@ import java.util.Locale; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; public class SQLiteTest { @@ -52,4 +56,42 @@ void testInsertOrUnknownAlgorithm() { () -> CCJSqlParserUtil.parse("INSERT OR NOTHING INTO t VALUES (1)", parser -> parser.withDialect(Dialect.SQLITE))); } + + @ParameterizedTest + @ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"}) + void testUpdateOrConflictAlgorithm(String algorithm) throws JSQLParserException { + Update update = (Update) TestUtils.assertSqlCanBeParsedAndDeparsed( + "UPDATE OR " + algorithm + " t SET a = 1, b = 2 WHERE c = 3", true, + parser -> parser.withDialect(Dialect.SQLITE)); + assertEquals(UpdateConflictAlgorithm.valueOf(algorithm), update.getConflictAlgorithm()); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "update or " + algorithm.toLowerCase(Locale.ROOT) + " t set a = 1", true, + parser -> parser.withDialect(Dialect.SQLITE)); + } + + // A parser without any UPDATE OR support throws here too: these cases pin the SQLITE + // dialect guard, they do not show that UPDATE OR parses. + @ParameterizedTest + @ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"}) + void testUpdateOrConflictAlgorithmNeedsSqliteDialect(String algorithm) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("UPDATE OR " + algorithm + " t SET a = 1")); + } + + // A parser without any UPDATE OR support throws here too: this case pins the check of the + // word after OR, it does not show that UPDATE OR parses. + @Test + void testUpdateOrUnknownAlgorithm() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("UPDATE OR NOTHING t SET a = 1", + parser -> parser.withDialect(Dialect.SQLITE))); + } + + @Test + void testUpdateWithoutConflictAlgorithm() throws JSQLParserException { + Update update = (Update) TestUtils.assertSqlCanBeParsedAndDeparsed( + "UPDATE t SET a = 1", true, parser -> parser.withDialect(Dialect.SQLITE)); + assertNull(update.getConflictAlgorithm()); + } }