Skip to content

Commit e60aeb9

Browse files
committed
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
1 parent 7123d25 commit e60aeb9

7 files changed

Lines changed: 104 additions & 1 deletion

File tree

‎src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import net.sf.jsqlparser.parser.feature.Feature;
1313
import net.sf.jsqlparser.parser.feature.FeatureConfiguration;
14+
import net.sf.jsqlparser.statement.update.UpdateConflictAlgorithm;
1415
import net.sf.jsqlparser.statement.upsert.UpsertType;
1516

1617
import java.util.ArrayList;
@@ -284,6 +285,18 @@ protected UpsertType insertOrType(String algorithm) throws ParseException {
284285
}
285286
}
286287

288+
protected UpdateConflictAlgorithm updateOrAlgorithm(String algorithm) throws ParseException {
289+
if (!Dialect.SQLITE.name().equals(getAsString(Feature.dialect))) {
290+
throw new ParseException("UPDATE OR " + algorithm + " needs Dialect.SQLITE");
291+
}
292+
for (UpdateConflictAlgorithm conflictAlgorithm : UpdateConflictAlgorithm.values()) {
293+
if (conflictAlgorithm.name().equalsIgnoreCase(algorithm)) {
294+
return conflictAlgorithm;
295+
}
296+
}
297+
throw new ParseException("Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK");
298+
}
299+
287300
public String getAsString(Feature f) {
288301
return getConfiguration().getAsString(f);
289302
}

‎src/main/java/net/sf/jsqlparser/statement/update/Update.java‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class Update extends ASTNodeAccessImpl implements Statement {
5555
private Limit limit;
5656
private OptionClause option;
5757
private ReturningClause returningClause;
58+
private UpdateConflictAlgorithm conflictAlgorithm;
5859
private UpdateModifierPriority modifierPriority;
5960
private boolean modifierIgnore;
6061

@@ -344,6 +345,14 @@ public Update setReturningClause(ReturningClause returningClause) {
344345
return this;
345346
}
346347

348+
public UpdateConflictAlgorithm getConflictAlgorithm() {
349+
return conflictAlgorithm;
350+
}
351+
352+
public void setConflictAlgorithm(UpdateConflictAlgorithm conflictAlgorithm) {
353+
this.conflictAlgorithm = conflictAlgorithm;
354+
}
355+
347356
public UpdateModifierPriority getModifierPriority() {
348357
return modifierPriority;
349358
}
@@ -381,6 +390,9 @@ public String toString() {
381390
if (oracleHint != null) {
382391
b.append(oracleHint).append(" ");
383392
}
393+
if (conflictAlgorithm != null) {
394+
b.append("OR ").append(conflictAlgorithm.name()).append(" ");
395+
}
384396
if (modifierPriority != null) {
385397
b.append(modifierPriority.name()).append(" ");
386398
}
@@ -505,6 +517,11 @@ public Update withExpressions(List<Expression> expressions) {
505517
return this;
506518
}
507519

520+
public Update withConflictAlgorithm(UpdateConflictAlgorithm conflictAlgorithm) {
521+
this.setConflictAlgorithm(conflictAlgorithm);
522+
return this;
523+
}
524+
508525
public Update withModifierPriority(UpdateModifierPriority modifierPriority) {
509526
this.setModifierPriority(modifierPriority);
510527
return this;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.update;
11+
12+
import java.util.Locale;
13+
14+
/**
15+
* The conflict resolution algorithm of SQLite's {@code UPDATE OR <algorithm>}.
16+
*/
17+
public enum UpdateConflictAlgorithm {
18+
ABORT, FAIL, IGNORE, REPLACE, ROLLBACK;
19+
20+
public static UpdateConflictAlgorithm from(String algorithm) {
21+
return Enum.valueOf(UpdateConflictAlgorithm.class, algorithm.toUpperCase(Locale.ROOT));
22+
}
23+
}

‎src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public void deParse(Update update) {
4949
if (update.getOracleHint() != null) {
5050
builder.append(update.getOracleHint()).append(" ");
5151
}
52+
if (update.getConflictAlgorithm() != null) {
53+
builder.append("OR ").append(update.getConflictAlgorithm()).append(" ");
54+
}
5255
if (update.getModifierPriority() != null) {
5356
builder.append(update.getModifierPriority()).append(" ");
5457
}

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7703,11 +7703,15 @@ Update Update():
77037703
Token tk = null;
77047704
UpdateModifierPriority modifierPriority = null;
77057705
boolean modifierIgnore = false;
7706+
String algorithm;
77067707

77077708
OutputClause outputClause = null;
77087709
}
77097710
{
77107711
<K_UPDATE> { update.setOracleHint(getOracleHint()); }
7712+
// SQLite: UPDATE OR <algorithm>; ABORT and FAIL are not keywords of the parser
7713+
[ <K_OR> algorithm=RelObjectName()
7714+
{ update.setConflictAlgorithm(updateOrAlgorithm(algorithm)); } ]
77117715
[ LOOKAHEAD(2) <K_LOW_PRIORITY> { modifierPriority = UpdateModifierPriority.LOW_PRIORITY; }]
77127716
[ LOOKAHEAD(2) <K_IGNORE> { modifierIgnore = true; }]
77137717
table=TableWithAliasAndMysqlIndexHint()

‎src/site/sphinx/usage.rst‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1612,7 +1612,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
16121612
* - ``TERADATA``
16131613
- ``UPDATE target FROM sources SET ...`` with the FROM clause before SET
16141614
* - ``SQLITE``
1615-
- ``INSERT OR ABORT``, ``INSERT OR FAIL``, ``INSERT OR IGNORE`` and ``INSERT OR ROLLBACK`` conflict clauses
1615+
- ``INSERT OR ABORT``, ``INSERT OR FAIL``, ``INSERT OR IGNORE`` and ``INSERT OR ROLLBACK`` conflict clauses;
1616+
``UPDATE OR`` with any of ``ABORT``, ``FAIL``, ``IGNORE``, ``REPLACE`` and ``ROLLBACK``
16161617

16171618
Features set explicitly *after* the preset win over it.
16181619

‎src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@
1212
import net.sf.jsqlparser.JSQLParserException;
1313
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
1414
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
15+
import net.sf.jsqlparser.statement.update.Update;
16+
import net.sf.jsqlparser.statement.update.UpdateConflictAlgorithm;
1517
import net.sf.jsqlparser.test.TestUtils;
1618
import org.junit.jupiter.api.Test;
1719
import org.junit.jupiter.params.ParameterizedTest;
1820
import org.junit.jupiter.params.provider.ValueSource;
1921

2022
import java.util.Locale;
2123

24+
import static org.junit.jupiter.api.Assertions.assertEquals;
25+
import static org.junit.jupiter.api.Assertions.assertNull;
2226
import static org.junit.jupiter.api.Assertions.assertThrows;
2327

2428
public class SQLiteTest {
@@ -52,4 +56,42 @@ void testInsertOrUnknownAlgorithm() {
5256
() -> CCJSqlParserUtil.parse("INSERT OR NOTHING INTO t VALUES (1)",
5357
parser -> parser.withDialect(Dialect.SQLITE)));
5458
}
59+
60+
@ParameterizedTest
61+
@ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"})
62+
void testUpdateOrConflictAlgorithm(String algorithm) throws JSQLParserException {
63+
Update update = (Update) TestUtils.assertSqlCanBeParsedAndDeparsed(
64+
"UPDATE OR " + algorithm + " t SET a = 1, b = 2 WHERE c = 3", true,
65+
parser -> parser.withDialect(Dialect.SQLITE));
66+
assertEquals(UpdateConflictAlgorithm.valueOf(algorithm), update.getConflictAlgorithm());
67+
68+
TestUtils.assertSqlCanBeParsedAndDeparsed(
69+
"update or " + algorithm.toLowerCase(Locale.ROOT) + " t set a = 1", true,
70+
parser -> parser.withDialect(Dialect.SQLITE));
71+
}
72+
73+
// A parser without any UPDATE OR support throws here too: these cases pin the SQLITE
74+
// dialect guard, they do not show that UPDATE OR parses.
75+
@ParameterizedTest
76+
@ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"})
77+
void testUpdateOrConflictAlgorithmNeedsSqliteDialect(String algorithm) {
78+
assertThrows(JSQLParserException.class,
79+
() -> CCJSqlParserUtil.parse("UPDATE OR " + algorithm + " t SET a = 1"));
80+
}
81+
82+
// A parser without any UPDATE OR support throws here too: this case pins the check of the
83+
// word after OR, it does not show that UPDATE OR parses.
84+
@Test
85+
void testUpdateOrUnknownAlgorithm() {
86+
assertThrows(JSQLParserException.class,
87+
() -> CCJSqlParserUtil.parse("UPDATE OR NOTHING t SET a = 1",
88+
parser -> parser.withDialect(Dialect.SQLITE)));
89+
}
90+
91+
@Test
92+
void testUpdateWithoutConflictAlgorithm() throws JSQLParserException {
93+
Update update = (Update) TestUtils.assertSqlCanBeParsedAndDeparsed(
94+
"UPDATE t SET a = 1", true, parser -> parser.withDialect(Dialect.SQLITE));
95+
assertNull(update.getConflictAlgorithm());
96+
}
5597
}

0 commit comments

Comments
 (0)