Skip to content

Commit d47f329

Browse files
feat: parse SQLite INSERT OR <algorithm> with the INSERT clauses
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 bccb1c8 commit d47f329

4 files changed

Lines changed: 188 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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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"),
26+
OR_FAIL("OR FAIL"),
27+
OR_IGNORE("OR IGNORE"),
28+
OR_REPLACE("OR REPLACE"),
29+
OR_ROLLBACK("OR ROLLBACK");
30+
31+
private final String keywords;
32+
33+
InsertConflictAlgorithm(String keywords) {
34+
this.keywords = keywords;
35+
}
36+
37+
/** The words printed between INSERT and INTO. */
38+
public String getKeywords() {
39+
return keywords;
40+
}
41+
42+
/** The algorithm without the OR, as in {@code INSERT OR IGNORE}: IGNORE. */
43+
public String getAlgorithm() {
44+
return keywords.substring("OR ".length());
45+
}
46+
47+
/**
48+
* The algorithm of {@code INSERT OR <algorithm>}.
49+
*
50+
* @param algorithm ABORT, FAIL, IGNORE, REPLACE or ROLLBACK, in any case
51+
* @return the matching constant
52+
* @throws IllegalArgumentException when the word is not a SQLite conflict algorithm
53+
*/
54+
public static InsertConflictAlgorithm fromAlgorithm(String algorithm) {
55+
String upperCase = algorithm.toUpperCase(java.util.Locale.ROOT);
56+
for (InsertConflictAlgorithm value : values()) {
57+
if (value.getAlgorithm().equals(upperCase)) {
58+
return value;
59+
}
60+
}
61+
throw new IllegalArgumentException(
62+
"Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK but found " + algorithm);
63+
}
64+
65+
@Override
66+
public String toString() {
67+
return keywords;
68+
}
69+
}

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

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

2386+
/**
2387+
* The conflict resolution of SQLite's INSERT OR &lt;algorithm&gt;. Every algorithm needs
2388+
* Dialect.SQLITE, as ABORT, FAIL, IGNORE and ROLLBACK are SQLite's own wording; without a
2389+
* dialect the Upsert() production keeps the historic reading of INSERT OR REPLACE.
2390+
*/
2391+
private InsertConflictAlgorithm insertOrAlgorithm(String algorithm) throws ParseException {
2392+
if (!"REPLACE".equalsIgnoreCase(algorithm)
2393+
&& !Dialect.SQLITE.name().equals(getAsString(Feature.dialect))) {
2394+
throw new ParseException("INSERT OR " + algorithm + " needs Dialect.SQLITE");
2395+
}
2396+
try {
2397+
return InsertConflictAlgorithm.fromAlgorithm(algorithm);
2398+
} catch (IllegalArgumentException e) {
2399+
throw new ParseException(e.getMessage());
2400+
}
2401+
}
2402+
2403+
/**
2404+
* Whether INSERT OR &lt;algorithm&gt; follows, the prefix that the plain Insert() production reads
2405+
* and that keeps ON CONFLICT, RETURNING, DEFAULT VALUES and the table aliases. It is read with
2406+
* Dialect.SQLITE only; without a dialect Upsert() keeps the historic INSERT OR REPLACE reading.
2407+
*/
2408+
private boolean isInsertOrPrefixAhead() {
2409+
return getToken(1).kind == K_INSERT
2410+
&& getToken(2).kind == K_OR
2411+
&& Dialect.SQLITE.name().equals(getAsString(Feature.dialect));
2412+
}
2413+
23862414
private boolean isCreateTableQueryPrefix() {
23872415
int kind = getToken(1).kind;
23882416
return kind == K_AS || kind == K_TABLE || kind == K_IGNORE || kind == K_REPLACE;
@@ -6766,6 +6794,10 @@ Statement SingleStatement() :
67666794
|
67676795
stm = Select()
67686796
|
6797+
// SQLite's INSERT OR <algorithm> takes the plain Insert() production, so that it keeps
6798+
// ON CONFLICT, RETURNING, DEFAULT VALUES and the table aliases; REPLACE stays an Upsert
6799+
LOOKAHEAD({ isInsertOrPrefixAhead() }) stm = Insert()
6800+
|
67696801
LOOKAHEAD(3) stm = Upsert()
67706802
|
67716803
LOOKAHEAD({ isResourceGroupStatementAhead() }) stm = ResourceGroupStatement()
@@ -10928,9 +10960,18 @@ Insert Insert():
1092810960
List<OracleMultiInsertBranch> oracleMultiInsertBranches = new ArrayList<OracleMultiInsertBranch>();
1092910961
OracleMultiInsertClause oracleMultiInsertClause = null;
1093010962
OracleMultiInsertBranch oracleMultiInsertBranch = null;
10963+
String algorithm;
1093110964
}
1093210965
{
10933-
<K_INSERT> { insert.setOracleHint(getOracleHint()); }
10966+
(
10967+
<K_INSERT>
10968+
// SQLite: INSERT OR <algorithm>. ABORT and FAIL are not keywords of the parser, so the
10969+
// word is read as a name and the algorithm is checked in AbstractJSqlParser
10970+
[ LOOKAHEAD({ getToken(1).kind == K_OR })
10971+
<K_OR> algorithm=RelObjectName()
10972+
{ insert.setConflictAlgorithm(insertOrAlgorithm(algorithm)); } ]
10973+
)
10974+
{ insert.setOracleHint(getOracleHint()); }
1093410975

1093510976
[
1093610977
LOOKAHEAD(2) (tk = <K_LOW_PRIORITY> | tk = <K_DELAYED> | tk = <K_HIGH_PRIORITY>)
@@ -10940,6 +10981,11 @@ Insert Insert():
1094010981
}
1094110982
]
1094210983
[ LOOKAHEAD(2) <K_IGNORE>{ modifierIgnore = true; }]
10984+
{
10985+
// SQLite takes the target of INSERT OR ... without the INTO, as in INSERT OR IGNORE t
10986+
insert.setOmitInto(insert.getConflictAlgorithm() != null
10987+
&& getToken(1).kind != K_INTO && getToken(1).kind != K_OVERWRITE);
10988+
}
1094310989
(
1094410990
LOOKAHEAD(2, (<K_ALL> | <K_FIRST>) (<K_INTO> | <K_WHEN>))
1094510991
(

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

Lines changed: 33 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,37 @@ 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), insert.getConflictAlgorithm());
59+
}
60+
61+
// The OR-form keeps the clauses the plain INSERT production reads, which the separate
62+
// Upsert() production never had: SQLite accepts INSERT OR IGNORE with ON CONFLICT and
63+
// RETURNING, and REPLACE is only the REPLACE algorithm without the OR.
64+
@ParameterizedTest
65+
@ValueSource(strings = {
66+
"INSERT OR IGNORE INTO t (a) VALUES (1) ON CONFLICT DO NOTHING",
67+
"INSERT OR IGNORE INTO t (a) VALUES (1) ON CONFLICT (a) DO UPDATE SET a = 2",
68+
"INSERT OR IGNORE INTO t (a) VALUES (1) RETURNING a",
69+
"INSERT OR IGNORE INTO t DEFAULT VALUES",
70+
"INSERT OR IGNORE INTO t AS alias VALUES (1)",
71+
"INSERT OR REPLACE INTO t (a) VALUES (1) RETURNING a"})
72+
void testInsertOrKeepsTheInsertClauses(String sql) throws JSQLParserException {
73+
TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true,
74+
parser -> parser.withDialect(Dialect.SQLITE));
75+
}
76+
77+
@Test
78+
void testInsertOrKeepsTheOptionalInto() throws JSQLParserException {
79+
TestUtils.assertSqlCanBeParsedAndDeparsed("INSERT OR IGNORE t VALUES (1)", true,
80+
parser -> parser.withDialect(Dialect.SQLITE));
81+
}
82+
5083
@ParameterizedTest
5184
@ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "ROLLBACK"})
5285
void testInsertOrConflictAlgorithmNeedsSqliteDialect(String algorithm) {

0 commit comments

Comments
 (0)