diff --git a/pkg/rain/query_insert.go b/pkg/rain/query_insert.go index fc834a1..d8663a1 100644 --- a/pkg/rain/query_insert.go +++ b/pkg/rain/query_insert.go @@ -24,6 +24,7 @@ type InsertQuery struct { columns []schema.ColumnReference returning []schema.Expression conflict *insertConflictClause + ignore bool ctes []cteDefinition defaultValues bool @@ -210,6 +211,14 @@ func (b *InsertConflictBuilder) DoUpdateSet(columns ...schema.ColumnReference) * return &InsertConflictUpdateBuilder{query: b.query} } +// Ignore configures the INSERT to ignore conflict errors. +// PostgreSQL renders "ON CONFLICT DO NOTHING", while MySQL renders "INSERT IGNORE" +// and SQLite renders "INSERT OR IGNORE". +func (q *InsertQuery) Ignore() *InsertQuery { + q.ignore = true + return q +} + // Returning adds RETURNING expressions when supported by the dialect. func (q *InsertQuery) Returning(exprs ...schema.Expression) *InsertQuery { q.returning = append(q.returning, exprs...) @@ -287,7 +296,7 @@ func (q *InsertQuery) writeValuesSQL(ctx *compileContext) error { if err := q.validateSources(); err != nil { return err } - ctx.writeString("INSERT INTO ") + q.writeInsertInto(ctx) ctx.writeTableName(q.table) if ctx.dialect.Name() == "mysql" { ctx.writeString(" () VALUES ()") @@ -299,7 +308,7 @@ func (q *InsertQuery) writeValuesSQL(ctx *compileContext) error { return err } - ctx.writeString("INSERT INTO ") + q.writeInsertInto(ctx) ctx.writeTableName(q.table) if q.models != nil { @@ -325,6 +334,33 @@ func (q *InsertQuery) writeValuesSQL(ctx *compileContext) error { return ctx.writeReturning(q.returning, q.returningClause()) } +func (q *InsertQuery) useIgnore() bool { + if q.ignore { + return true + } + if q.conflict == nil || q.conflict.action != insertConflictActionDoNothing { + return false + } + // For MySQL and SQLite, OnConflict().DoNothing() with no targets is + // equivalent to INSERT [OR] IGNORE. + return (q.dialect.Name() == "mysql" || q.dialect.Name() == "sqlite") && + len(q.conflict.columns) == 0 && q.conflict.constraint == "" +} + +func (q *InsertQuery) writeInsertInto(ctx *compileContext) { + if q.useIgnore() { + switch q.dialect.Name() { + case "mysql": + ctx.writeString("INSERT IGNORE INTO ") + return + case "sqlite": + ctx.writeString("INSERT OR IGNORE INTO ") + return + } + } + ctx.writeString("INSERT INTO ") +} + func (q *InsertQuery) writeModelsSQL(ctx *compileContext) error { value := reflect.ValueOf(q.models) for value.Kind() == reflect.Pointer { @@ -648,7 +684,7 @@ func (q *InsertQuery) writeSelectSQL(ctx *compileContext) error { selectQuery = selectQuery.withSQLiteInsertSelectConflictWhere() } - ctx.writeString("INSERT INTO ") + q.writeInsertInto(ctx) ctx.writeTableName(q.table) if len(q.columns) > 0 { @@ -765,6 +801,11 @@ func (q *InsertQuery) validateSources() error { } func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { + if q.ignore && q.dialect.Name() == "postgres" { + ctx.writeString(" ON CONFLICT DO NOTHING") + return nil + } + if q.conflict == nil { return nil } @@ -777,6 +818,9 @@ func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { } if q.dialect.Name() == "mysql" { + if q.useIgnore() { + return nil + } if len(q.conflict.columns) > 0 || q.conflict.constraint != "" || len(q.conflict.targetWhere) > 0 { return errors.New("rain: MySQL ON DUPLICATE KEY UPDATE does not support conflict targets (columns, constraints, or WHERE); call OnConflict() without modifiers") } @@ -816,8 +860,8 @@ func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { return nil } - if len(q.conflict.columns) == 0 && q.conflict.constraint == "" { - return errors.New("rain: conflict clause requires at least one target (columns or constraint)") + if q.dialect.Name() == "sqlite" && q.useIgnore() { + return nil } ctx.writeString(" ON CONFLICT") @@ -836,6 +880,8 @@ func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { ctx.writeColumnName(col) } ctx.writeByte(')') + } else if q.conflict.action != insertConflictActionDoNothing { + return errors.New("rain: conflict clause requires at least one target (columns or constraint)") } if len(q.conflict.targetWhere) > 0 { diff --git a/pkg/rain/query_insert_test.go b/pkg/rain/query_insert_test.go index 8e3554d..c1556b5 100644 --- a/pkg/rain/query_insert_test.go +++ b/pkg/rain/query_insert_test.go @@ -269,6 +269,169 @@ func TestInsertOnConflictPostgres(t *testing.T) { }) } +func TestInsertIgnoreToSQL(t *testing.T) { + t.Parallel() + + users, _ := defineTables() + + t.Run("mysql insert ignore", func(t *testing.T) { + db, _ := rain.OpenDialect("mysql") + sqlText, _, err := db.Insert(). + Table(users). + Set(users.Email, "alice@example.com"). + Ignore(). + ToSQL() + if err != nil { + t.Fatalf("ToSQL returned error: %v", err) + } + + wantSQL := "INSERT IGNORE INTO `users` (`email`) VALUES (?)" + if sqlText != wantSQL { + t.Fatalf("unexpected SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) + } + }) + + t.Run("sqlite insert or ignore", func(t *testing.T) { + db, _ := rain.OpenDialect("sqlite") + sqlText, _, err := db.Insert(). + Table(users). + Set(users.Email, "alice@example.com"). + Ignore(). + ToSQL() + if err != nil { + t.Fatalf("ToSQL returned error: %v", err) + } + + wantSQL := `INSERT OR IGNORE INTO "users" ("email") VALUES (?)` + if sqlText != wantSQL { + t.Fatalf("unexpected SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) + } + }) + + t.Run("postgres ignore renders on conflict do nothing", func(t *testing.T) { + db, _ := rain.OpenDialect("postgres") + sqlText, _, err := db.Insert(). + Table(users). + Set(users.Email, "alice@example.com"). + Ignore(). + ToSQL() + if err != nil { + t.Fatalf("ToSQL returned error: %v", err) + } + + wantSQL := `INSERT INTO "users" ("email") VALUES ($1) ON CONFLICT DO NOTHING` + if sqlText != wantSQL { + t.Fatalf("unexpected SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) + } + }) +} + +func TestInsertOnConflictDoNothingTargetless(t *testing.T) { + t.Parallel() + + users, _ := defineTables() + + t.Run("mysql renders insert ignore", func(t *testing.T) { + db, _ := rain.OpenDialect("mysql") + sqlText, _, err := db.Insert(). + Table(users). + Set(users.Email, "alice@example.com"). + OnConflict(). + DoNothing(). + ToSQL() + if err != nil { + t.Fatalf("ToSQL returned error: %v", err) + } + + wantSQL := "INSERT IGNORE INTO `users` (`email`) VALUES (?)" + if sqlText != wantSQL { + t.Fatalf("unexpected SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) + } + }) + + t.Run("sqlite renders insert or ignore", func(t *testing.T) { + db, _ := rain.OpenDialect("sqlite") + sqlText, _, err := db.Insert(). + Table(users). + Set(users.Email, "alice@example.com"). + OnConflict(). + DoNothing(). + ToSQL() + if err != nil { + t.Fatalf("ToSQL returned error: %v", err) + } + + wantSQL := `INSERT OR IGNORE INTO "users" ("email") VALUES (?)` + if sqlText != wantSQL { + t.Fatalf("unexpected SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) + } + }) + + t.Run("postgres renders targetless on conflict do nothing", func(t *testing.T) { + db, _ := rain.OpenDialect("postgres") + sqlText, _, err := db.Insert(). + Table(users). + Set(users.Email, "alice@example.com"). + OnConflict(). + DoNothing(). + ToSQL() + if err != nil { + t.Fatalf("ToSQL returned error: %v", err) + } + + wantSQL := `INSERT INTO "users" ("email") VALUES ($1) ON CONFLICT DO NOTHING` + if sqlText != wantSQL { + t.Fatalf("unexpected SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) + } + }) +} + +func TestInsertSelectIgnoreToSQL(t *testing.T) { + t.Parallel() + + users, _ := defineTables() + + t.Run("mysql insert ignore select", func(t *testing.T) { + db, _ := rain.OpenDialect("mysql") + subquery := db.Select().Table(users).Column(users.Email) + + sqlText, _, err := db.Insert(). + Table(users). + Columns(users.Email). + Select(subquery). + Ignore(). + ToSQL() + if err != nil { + t.Fatalf("ToSQL returned error: %v", err) + } + + wantSQL := "INSERT IGNORE INTO `users` (`email`) SELECT `users`.`email` FROM `users`" + if sqlText != wantSQL { + t.Fatalf("unexpected SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) + } + }) + + t.Run("postgres insert ignore select", func(t *testing.T) { + db, _ := rain.OpenDialect("postgres") + subquery := db.Select().Table(users).Column(users.Email) + + sqlText, _, err := db.Insert(). + Table(users). + Columns(users.Email). + Select(subquery). + Ignore(). + ToSQL() + if err != nil { + t.Fatalf("ToSQL returned error: %v", err) + } + + wantSQL := `INSERT INTO "users" ("email") SELECT "users"."email" FROM "users" ON CONFLICT DO NOTHING` + if sqlText != wantSQL { + t.Fatalf("unexpected SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) + } + }) +} + func TestInsertWithCTEToSQL(t *testing.T) { t.Parallel() @@ -692,7 +855,7 @@ func TestInsertOnConflictMySQL(t *testing.T) { t.Fatalf("insert on conflict mysql do nothing ToSQL returned error: %v", err) } - wantSQL := "INSERT INTO `users` (`email`, `name`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `id` = `id`" + wantSQL := "INSERT IGNORE INTO `users` (`email`, `name`) VALUES (?, ?)" if sqlText != wantSQL { t.Fatalf("unexpected mysql do nothing SQL:\nwant: %s\ngot: %s", wantSQL, sqlText) }