Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/main/java/net/sf/jsqlparser/statement/insert/Insert.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public enum OverridingMode {
private boolean oracleMultiInsert = false;
private boolean oracleMultiInsertFirst = false;
private List<OracleMultiInsertBranch> oracleMultiInsertBranches;
private InsertConflictAlgorithm conflictAlgorithm = null;
private boolean omitInto = false;

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

private void appendInsertPrefix(StringBuilder builder) {
builder.append("INSERT");
if (conflictAlgorithm != null) {
// SQLite writes the conflict resolution before the INTO, see lang_conflict
builder.append(" ").append(conflictAlgorithm.getKeywords());
}
if (oracleHint != null) {
builder.append(" ").append(oracleHint);
}
Expand Down Expand Up @@ -451,7 +457,8 @@ private static void appendOracleMultiInsertBranch(StringBuilder builder,

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

/**
* Whether the statement is written without the optional INTO, as in SQLite's
* {@code INSERT OR IGNORE t VALUES (1)}.
*/
public boolean isOmitInto() {
return omitInto;
}

public void setOmitInto(boolean omitInto) {
this.omitInto = omitInto;
}

public Insert withOmitInto(boolean omitInto) {
this.omitInto = omitInto;
return this;
}

/** The conflict resolution of {@code INSERT OR ...} and {@code REPLACE}, or null. */
public InsertConflictAlgorithm getConflictAlgorithm() {
return conflictAlgorithm;
}

public void setConflictAlgorithm(InsertConflictAlgorithm conflictAlgorithm) {
this.conflictAlgorithm = conflictAlgorithm;
}

public Insert withConflictAlgorithm(InsertConflictAlgorithm conflictAlgorithm) {
this.conflictAlgorithm = conflictAlgorithm;
return this;
}

public Insert withWithItemsList(List<WithItem<?>> withList) {
this.withItemsList = withList;
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*-
* #%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.insert;

/**
* The conflict resolution algorithm of SQLite's {@code INSERT OR <algorithm>}, which {@link Insert}
* carries as a prefix in front of the INTO keyword.
*
* <p>
* SQLite accepts ABORT, FAIL, IGNORE, REPLACE and ROLLBACK after {@code INSERT OR} (see SQLite's
* {@code lang_insert} and {@code lang_conflict} pages). MySQL's {@code REPLACE} statement keeps its
* historic {@link net.sf.jsqlparser.statement.upsert.Upsert} reading.
*
* @see net.sf.jsqlparser.statement.update.UpdateConflictAlgorithm
*/
public enum InsertConflictAlgorithm {

OR_ABORT("OR ABORT"), OR_FAIL("OR FAIL"), OR_IGNORE("OR IGNORE"), OR_REPLACE(
"OR REPLACE"), OR_ROLLBACK("OR ROLLBACK");

private final String keywords;

InsertConflictAlgorithm(String keywords) {
this.keywords = keywords;
}

/** The words printed between INSERT and INTO. */
public String getKeywords() {
return keywords;
}

/** The algorithm without the OR, as in {@code INSERT OR IGNORE}: IGNORE. */
public String getAlgorithm() {
return keywords.substring("OR ".length());
}

/**
* The algorithm of {@code INSERT OR <algorithm>}.
*
* @param algorithm ABORT, FAIL, IGNORE, REPLACE or ROLLBACK, in any case
* @return the matching constant
* @throws IllegalArgumentException when the word is not a SQLite conflict algorithm
*/
public static InsertConflictAlgorithm fromAlgorithm(String algorithm) {
String upperCase = algorithm.toUpperCase(java.util.Locale.ROOT);
for (InsertConflictAlgorithm value : values()) {
if (value.getAlgorithm().equals(upperCase)) {
return value;
}
}
throw new IllegalArgumentException(
"Expected ABORT, FAIL, IGNORE, REPLACE or ROLLBACK but found " + algorithm);
}

@Override
public String toString() {
return keywords;
}
}
48 changes: 47 additions & 1 deletion src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,34 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
return isKeywordAhead("MATCH_RECOGNIZE") && getToken(2).kind == OPENING_BRACKET;
}

/**
* The conflict resolution of SQLite's INSERT OR &lt;algorithm&gt;. Every algorithm needs
* Dialect.SQLITE, as ABORT, FAIL, IGNORE and ROLLBACK are SQLite's own wording; without a
* dialect the Upsert() production keeps the historic reading of INSERT OR REPLACE.
*/
private InsertConflictAlgorithm insertOrAlgorithm(String algorithm) throws ParseException {
if (!"REPLACE".equalsIgnoreCase(algorithm)
&& !Dialect.SQLITE.name().equals(getAsString(Feature.dialect))) {
throw new ParseException("INSERT OR " + algorithm + " needs Dialect.SQLITE");
}
try {
return InsertConflictAlgorithm.fromAlgorithm(algorithm);
} catch (IllegalArgumentException e) {
throw new ParseException(e.getMessage());
}
}

/**
* Whether INSERT OR &lt;algorithm&gt; follows, the prefix that the plain Insert() production reads
* and that keeps ON CONFLICT, RETURNING, DEFAULT VALUES and the table aliases. It is read with
* Dialect.SQLITE only; without a dialect Upsert() keeps the historic INSERT OR REPLACE reading.
*/
private boolean isInsertOrPrefixAhead() {
return getToken(1).kind == K_INSERT
&& getToken(2).kind == K_OR
&& Dialect.SQLITE.name().equals(getAsString(Feature.dialect));
}

private boolean isCreateTableQueryPrefix() {
int kind = getToken(1).kind;
return kind == K_AS || kind == K_TABLE || kind == K_IGNORE || kind == K_REPLACE;
Expand Down Expand Up @@ -6766,6 +6794,10 @@ Statement SingleStatement() :
|
stm = Select()
|
// SQLite's INSERT OR <algorithm> takes the plain Insert() production, so that it keeps
// ON CONFLICT, RETURNING, DEFAULT VALUES and the table aliases; REPLACE stays an Upsert
LOOKAHEAD({ isInsertOrPrefixAhead() }) stm = Insert()
|
LOOKAHEAD(3) stm = Upsert()
|
LOOKAHEAD({ isResourceGroupStatementAhead() }) stm = ResourceGroupStatement()
Expand Down Expand Up @@ -10928,9 +10960,18 @@ Insert Insert():
List<OracleMultiInsertBranch> oracleMultiInsertBranches = new ArrayList<OracleMultiInsertBranch>();
OracleMultiInsertClause oracleMultiInsertClause = null;
OracleMultiInsertBranch oracleMultiInsertBranch = null;
String algorithm;
}
{
<K_INSERT> { insert.setOracleHint(getOracleHint()); }
(
<K_INSERT>
// SQLite: INSERT OR <algorithm>. ABORT and FAIL are not keywords of the parser, so the
// word is read as a name and the algorithm is checked in AbstractJSqlParser
[ LOOKAHEAD({ getToken(1).kind == K_OR })
<K_OR> algorithm=RelObjectName()
{ insert.setConflictAlgorithm(insertOrAlgorithm(algorithm)); } ]
)
{ insert.setOracleHint(getOracleHint()); }

[
LOOKAHEAD(2) (tk = <K_LOW_PRIORITY> | tk = <K_DELAYED> | tk = <K_HIGH_PRIORITY>)
Expand All @@ -10940,6 +10981,11 @@ Insert Insert():
}
]
[ LOOKAHEAD(2) <K_IGNORE>{ modifierIgnore = true; }]
{
// SQLite takes the target of INSERT OR ... without the INTO, as in INSERT OR IGNORE t
insert.setOmitInto(insert.getConflictAlgorithm() != null
&& getToken(1).kind != K_INTO && getToken(1).kind != K_OVERWRITE);
}
(
LOOKAHEAD(2, (<K_ALL> | <K_FIRST>) (<K_INTO> | <K_WHEN>))
(
Expand Down
34 changes: 34 additions & 0 deletions src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.create.table.TableOption;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.insert.InsertConflictAlgorithm;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.update.UpdateConflictAlgorithm;
import net.sf.jsqlparser.test.TestUtils;
Expand Down Expand Up @@ -47,6 +49,38 @@ void testInsertOrConflictAlgorithm(String algorithm) throws JSQLParserException
parser -> parser.withDialect(Dialect.SQLITE));
}

@ParameterizedTest
@ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"})
void testInsertOrConflictAlgorithmIsAnInsert(String algorithm) throws JSQLParserException {
Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(
"INSERT OR " + algorithm + " INTO t (a) VALUES (1)", true,
parser -> parser.withDialect(Dialect.SQLITE));
assertEquals(InsertConflictAlgorithm.fromAlgorithm(algorithm),
insert.getConflictAlgorithm());
}

// The OR-form keeps the clauses the plain INSERT production reads, which the separate
// Upsert() production never had: SQLite accepts INSERT OR IGNORE with ON CONFLICT and
// RETURNING, and REPLACE is only the REPLACE algorithm without the OR.
@ParameterizedTest
@ValueSource(strings = {
"INSERT OR IGNORE INTO t (a) VALUES (1) ON CONFLICT DO NOTHING",
"INSERT OR IGNORE INTO t (a) VALUES (1) ON CONFLICT (a) DO UPDATE SET a = 2",
"INSERT OR IGNORE INTO t (a) VALUES (1) RETURNING a",
"INSERT OR IGNORE INTO t DEFAULT VALUES",
"INSERT OR IGNORE INTO t AS alias VALUES (1)",
"INSERT OR REPLACE INTO t (a) VALUES (1) RETURNING a"})
void testInsertOrKeepsTheInsertClauses(String sql) throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true,
parser -> parser.withDialect(Dialect.SQLITE));
}

@Test
void testInsertOrKeepsTheOptionalInto() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed("INSERT OR IGNORE t VALUES (1)", true,
parser -> parser.withDialect(Dialect.SQLITE));
}

@ParameterizedTest
@ValueSource(strings = {"ABORT", "FAIL", "IGNORE", "ROLLBACK"})
void testInsertOrConflictAlgorithmNeedsSqliteDialect(String algorithm) {
Expand Down
Loading