diff --git a/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java b/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java index 94ec2ab5a..6f710fb19 100644 --- a/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java +++ b/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java @@ -65,6 +65,8 @@ public enum OverridingMode { private boolean oracleMultiInsert = false; private boolean oracleMultiInsertFirst = false; private List oracleMultiInsertBranches; + private InsertConflictAlgorithm conflictAlgorithm = null; + private boolean omitInto = false; public List getDuplicateUpdateSets() { return duplicateAction != null ? duplicateAction.getUpdateSets() : null; @@ -405,6 +407,10 @@ private void appendWithItems(StringBuilder builder, Consumer> 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); } @@ -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"); } @@ -526,6 +533,37 @@ static StringBuilder appendColumnNamesTo(StringBuilder builder, List 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> withList) { this.withItemsList = withList; return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/insert/InsertConflictAlgorithm.java b/src/main/java/net/sf/jsqlparser/statement/insert/InsertConflictAlgorithm.java new file mode 100644 index 000000000..0bd3bb317 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/insert/InsertConflictAlgorithm.java @@ -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 }, which {@link Insert} + * carries as a prefix in front of the INTO keyword. + * + *

+ * 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 }. + * + * @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; + } +} diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f21f6617b..36bd27b16 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2383,6 +2383,34 @@ public class CCJSqlParser extends AbstractJSqlParser { return isKeywordAhead("MATCH_RECOGNIZE") && getToken(2).kind == OPENING_BRACKET; } + /** + * The conflict resolution of SQLite's INSERT OR <algorithm>. 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 <algorithm> 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; @@ -6766,6 +6794,10 @@ Statement SingleStatement() : | stm = Select() | + // SQLite's INSERT OR 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() @@ -10928,9 +10960,18 @@ Insert Insert(): List oracleMultiInsertBranches = new ArrayList(); OracleMultiInsertClause oracleMultiInsertClause = null; OracleMultiInsertBranch oracleMultiInsertBranch = null; + String algorithm; } { - { insert.setOracleHint(getOracleHint()); } + ( + + // SQLite: INSERT OR . 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 }) + algorithm=RelObjectName() + { insert.setConflictAlgorithm(insertOrAlgorithm(algorithm)); } ] + ) + { insert.setOracleHint(getOracleHint()); } [ LOOKAHEAD(2) (tk = | tk = | tk = ) @@ -10940,6 +10981,11 @@ Insert Insert(): } ] [ LOOKAHEAD(2) { 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, ( | ) ( | )) ( 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 fd2d8da26..2bbac7513 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SQLiteTest.java @@ -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; @@ -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) {