Skip to content

Commit 07d5f32

Browse files
feat: parse SQLite INSERT OR <algorithm> with the INSERT clauses (#2924)
Read the algorithm in Insert() behind Dialect.SQLITE, so the OR-form keeps ON CONFLICT, RETURNING, DEFAULT VALUES and the table aliases. REPLACE without OR keeps its Upsert reading. Part of #2852
1 parent d2769be commit 07d5f32

4 files changed

Lines changed: 186 additions & 2 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/insert/Insert.java‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public enum OverridingMode {
6565
private boolean oracleMultiInsert = false;
6666
private boolean oracleMultiInsertFirst = false;
6767
private List<OracleMultiInsertBranch> oracleMultiInsertBranches;
68+
private InsertConflictAlgorithm conflictAlgorithm = null;
69+
private boolean omitInto = false;
6870

6971
public List<UpdateSet> getDuplicateUpdateSets() {
7072
return duplicateAction != null ? duplicateAction.getUpdateSets() : null;
@@ -405,6 +407,10 @@ private void appendWithItems(StringBuilder builder, Consumer<WithItem<?>> withIt
405407

406408
private void appendInsertPrefix(StringBuilder builder) {
407409
builder.append("INSERT");
410+
if (conflictAlgorithm != null) {
411+
// SQLite writes the conflict resolution before the INTO, see lang_conflict
412+
builder.append(" ").append(conflictAlgorithm.getKeywords());
413+
}
408414
if (oracleHint != null) {
409415
builder.append(" ").append(oracleHint);
410416
}
@@ -451,7 +457,8 @@ private static void appendOracleMultiInsertBranch(StringBuilder builder,
451457

452458
/** The target: table, partitions, columns and the options before the source. */
453459
private void appendTarget(StringBuilder builder) {
454-
builder.append(overwrite ? " OVERWRITE" : " INTO");
460+
// SQLite's INSERT OR ... may omit the INTO
461+
builder.append(overwrite ? " OVERWRITE" : omitInto ? "" : " INTO");
455462
if (tableKeyword) {
456463
builder.append(" TABLE");
457464
}
@@ -526,6 +533,37 @@ static StringBuilder appendColumnNamesTo(StringBuilder builder, List<Column> col
526533
return builder.append(")");
527534
}
528535

536+
/**
537+
* Whether the statement is written without the optional INTO, as in SQLite's
538+
* {@code INSERT OR IGNORE t VALUES (1)}.
539+
*/
540+
public boolean isOmitInto() {
541+
return omitInto;
542+
}
543+
544+
public void setOmitInto(boolean omitInto) {
545+
this.omitInto = omitInto;
546+
}
547+
548+
public Insert withOmitInto(boolean omitInto) {
549+
this.omitInto = omitInto;
550+
return this;
551+
}
552+
553+
/** The conflict resolution of {@code INSERT OR ...} and {@code REPLACE}, or null. */
554+
public InsertConflictAlgorithm getConflictAlgorithm() {
555+
return conflictAlgorithm;
556+
}
557+
558+
public void setConflictAlgorithm(InsertConflictAlgorithm conflictAlgorithm) {
559+
this.conflictAlgorithm = conflictAlgorithm;
560+
}
561+
562+
public Insert withConflictAlgorithm(InsertConflictAlgorithm conflictAlgorithm) {
563+
this.conflictAlgorithm = conflictAlgorithm;
564+
return this;
565+
}
566+
529567
public Insert withWithItemsList(List<WithItem<?>> withList) {
530568
this.withItemsList = withList;
531569
return this;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.insert;
11+
12+
/**
13+
* The conflict resolution algorithm of SQLite's {@code INSERT OR <algorithm>}, which {@link Insert}
14+
* carries as a prefix in front of the INTO keyword.
15+
*
16+
* <p>
17+
* SQLite accepts ABORT, FAIL, IGNORE, REPLACE and ROLLBACK after {@code INSERT OR} (see SQLite's
18+
* {@code lang_insert} and {@code lang_conflict} pages). MySQL's {@code REPLACE} statement keeps its
19+
* historic {@link net.sf.jsqlparser.statement.upsert.Upsert} reading.
20+
*
21+
* @see net.sf.jsqlparser.statement.update.UpdateConflictAlgorithm
22+
*/
23+
public enum InsertConflictAlgorithm {
24+
25+
OR_ABORT("OR ABORT"), OR_FAIL("OR FAIL"), OR_IGNORE("OR IGNORE"), OR_REPLACE(
26+
"OR REPLACE"), OR_ROLLBACK("OR ROLLBACK");
27+
28+
private final String keywords;
29+
30+
InsertConflictAlgorithm(String keywords) {
31+
this.keywords = keywords;
32+
}
33+
34+
/** The words printed between INSERT and INTO. */
35+
public String getKeywords() {
36+
return keywords;
37+
}
38+
39+
/** The algorithm without the OR, as in {@code INSERT OR IGNORE}: IGNORE. */
40+
public String getAlgorithm() {
41+
return keywords.substring("OR ".length());
42+
}
43+
44+
/**
45+
* The algorithm of {@code INSERT OR <algorithm>}.
46+
*
47+
* @param algorithm ABORT, FAIL, IGNORE, REPLACE or ROLLBACK, in any case
48+
* @return the matching constant
49+
* @throws IllegalArgumentException when the word is not a SQLite conflict algorithm
50+
*/
51+
public static InsertConflictAlgorithm fromAlgorithm(String algorithm) {
52+
String upperCase = algorithm.toUpperCase(java.util.Locale.ROOT);
53+
for (InsertConflictAlgorithm value : values()) {
54+
if (value.getAlgorithm().equals(upperCase)) {
55+
return value;
56+
}
57+
}
58+
throw new IllegalArgumentException(
59+
"Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK but found " + algorithm);
60+
}
61+
62+
@Override
63+
public String toString() {
64+
return keywords;
65+
}
66+
}

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2386,6 +2386,34 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
23862386
return isKeywordAhead("MATCH_RECOGNIZE") && getToken(2).kind == OPENING_BRACKET;
23872387
}
23882388

2389+
/**
2390+
* The conflict resolution of SQLite's INSERT OR &lt;algorithm&gt;. Every algorithm needs
2391+
* Dialect.SQLITE, as ABORT, FAIL, IGNORE and ROLLBACK are SQLite's own wording; without a
2392+
* dialect the Upsert() production keeps the historic reading of INSERT OR REPLACE.
2393+
*/
2394+
private InsertConflictAlgorithm insertOrAlgorithm(String algorithm) throws ParseException {
2395+
if (!"REPLACE".equalsIgnoreCase(algorithm)
2396+
&& !Dialect.SQLITE.name().equals(getAsString(Feature.dialect))) {
2397+
throw new ParseException("INSERT OR " + algorithm + " needs Dialect.SQLITE");
2398+
}
2399+
try {
2400+
return InsertConflictAlgorithm.fromAlgorithm(algorithm);
2401+
} catch (IllegalArgumentException e) {
2402+
throw new ParseException(e.getMessage());
2403+
}
2404+
}
2405+
2406+
/**
2407+
* Whether INSERT OR &lt;algorithm&gt; follows, the prefix that the plain Insert() production reads
2408+
* and that keeps ON CONFLICT, RETURNING, DEFAULT VALUES and the table aliases. It is read with
2409+
* Dialect.SQLITE only; without a dialect Upsert() keeps the historic INSERT OR REPLACE reading.
2410+
*/
2411+
private boolean isInsertOrPrefixAhead() {
2412+
return getToken(1).kind == K_INSERT
2413+
&& getToken(2).kind == K_OR
2414+
&& Dialect.SQLITE.name().equals(getAsString(Feature.dialect));
2415+
}
2416+
23892417
private boolean isCreateTableQueryPrefix() {
23902418
int kind = getToken(1).kind;
23912419
return kind == K_AS || kind == K_TABLE || kind == K_IGNORE || kind == K_REPLACE;
@@ -6977,6 +7005,10 @@ Statement SingleStatement() :
69777005
|
69787006
stm = Select()
69797007
|
7008+
// SQLite's INSERT OR <algorithm> takes the plain Insert() production, so that it keeps
7009+
// ON CONFLICT, RETURNING, DEFAULT VALUES and the table aliases; REPLACE stays an Upsert
7010+
LOOKAHEAD({ isInsertOrPrefixAhead() }) stm = Insert()
7011+
|
69807012
LOOKAHEAD(3) stm = Upsert()
69817013
|
69827014
LOOKAHEAD({ isResourceGroupStatementAhead() }) stm = ResourceGroupStatement()
@@ -11308,9 +11340,18 @@ Insert Insert():
1130811340
List<OracleMultiInsertBranch> oracleMultiInsertBranches = new ArrayList<OracleMultiInsertBranch>();
1130911341
OracleMultiInsertClause oracleMultiInsertClause = null;
1131011342
OracleMultiInsertBranch oracleMultiInsertBranch = null;
11343+
String algorithm;
1131111344
}
1131211345
{
11313-
<K_INSERT> { insert.setOracleHint(getOracleHint()); }
11346+
(
11347+
<K_INSERT>
11348+
// SQLite: INSERT OR <algorithm>. ABORT and FAIL are not keywords of the parser, so the
11349+
// word is read as a name and the algorithm is checked in AbstractJSqlParser
11350+
[ LOOKAHEAD({ getToken(1).kind == K_OR })
11351+
<K_OR> algorithm=RelObjectName()
11352+
{ insert.setConflictAlgorithm(insertOrAlgorithm(algorithm)); } ]
11353+
)
11354+
{ insert.setOracleHint(getOracleHint()); }
1131411355

1131511356
[
1131611357
LOOKAHEAD(2) (tk = <K_LOW_PRIORITY> | tk = <K_DELAYED> | tk = <K_HIGH_PRIORITY>)
@@ -11320,6 +11361,11 @@ Insert Insert():
1132011361
}
1132111362
]
1132211363
[ LOOKAHEAD(2) <K_IGNORE>{ modifierIgnore = true; }]
11364+
{
11365+
// SQLite takes the target of INSERT OR ... without the INTO, as in INSERT OR IGNORE t
11366+
insert.setOmitInto(insert.getConflictAlgorithm() != null
11367+
&& getToken(1).kind != K_INTO && getToken(1).kind != K_OVERWRITE);
11368+
}
1132311369
(
1132411370
LOOKAHEAD(2, (<K_ALL> | <K_FIRST>) (<K_INTO> | <K_WHEN>))
1132511371
(

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1515
import net.sf.jsqlparser.statement.create.table.CreateTable;
1616
import net.sf.jsqlparser.statement.create.table.TableOption;
17+
import net.sf.jsqlparser.statement.insert.Insert;
18+
import net.sf.jsqlparser.statement.insert.InsertConflictAlgorithm;
1719
import net.sf.jsqlparser.statement.update.Update;
1820
import net.sf.jsqlparser.statement.update.UpdateConflictAlgorithm;
1921
import net.sf.jsqlparser.test.TestUtils;
@@ -47,6 +49,38 @@ void testInsertOrConflictAlgorithm(String algorithm) throws JSQLParserException
4749
parser -> parser.withDialect(Dialect.SQLITE));
4850
}
4951

52+
@ParameterizedTest
53+
@ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"})
54+
void testInsertOrConflictAlgorithmIsAnInsert(String algorithm) throws JSQLParserException {
55+
Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(
56+
"INSERT OR " + algorithm + " INTO t (a) VALUES (1)", true,
57+
parser -> parser.withDialect(Dialect.SQLITE));
58+
assertEquals(InsertConflictAlgorithm.fromAlgorithm(algorithm),
59+
insert.getConflictAlgorithm());
60+
}
61+
62+
// The OR-form keeps the clauses the plain INSERT production reads, which the separate
63+
// Upsert() production never had: SQLite accepts INSERT OR IGNORE with ON CONFLICT and
64+
// RETURNING, and REPLACE is only the REPLACE algorithm without the OR.
65+
@ParameterizedTest
66+
@ValueSource(strings = {
67+
"INSERT OR IGNORE INTO t (a) VALUES (1) ON CONFLICT DO NOTHING",
68+
"INSERT OR IGNORE INTO t (a) VALUES (1) ON CONFLICT (a) DO UPDATE SET a = 2",
69+
"INSERT OR IGNORE INTO t (a) VALUES (1) RETURNING a",
70+
"INSERT OR IGNORE INTO t DEFAULT VALUES",
71+
"INSERT OR IGNORE INTO t AS alias VALUES (1)",
72+
"INSERT OR REPLACE INTO t (a) VALUES (1) RETURNING a"})
73+
void testInsertOrKeepsTheInsertClauses(String sql) throws JSQLParserException {
74+
TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true,
75+
parser -> parser.withDialect(Dialect.SQLITE));
76+
}
77+
78+
@Test
79+
void testInsertOrKeepsTheOptionalInto() throws JSQLParserException {
80+
TestUtils.assertSqlCanBeParsedAndDeparsed("INSERT OR IGNORE t VALUES (1)", true,
81+
parser -> parser.withDialect(Dialect.SQLITE));
82+
}
83+
5084
@ParameterizedTest
5185
@ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "ROLLBACK"})
5286
void testInsertOrConflictAlgorithmNeedsSqliteDialect(String algorithm) {

0 commit comments

Comments
 (0)