-
Notifications
You must be signed in to change notification settings - Fork 0
Implement InsertQuery.Ignore() and improve targetless DoNothing() #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ type InsertQuery struct { | |
| conflict *insertConflictClause | ||
| ctes []cteDefinition | ||
| defaultValues bool | ||
| useIgnore bool | ||
|
|
||
| // OPTIMIZATION: Internal buffers to avoid heap allocations for common | ||
| // query shapes while keeping the struct size reasonable. | ||
|
|
@@ -184,6 +185,15 @@ func (q *InsertQuery) DefaultValues() *InsertQuery { | |
| return q | ||
| } | ||
|
|
||
| // Ignore configures the INSERT to ignore conflict errors. | ||
| // MySQL renders "INSERT IGNORE", SQLite renders "INSERT OR IGNORE". | ||
| // PostgreSQL does not support this and will return an error during compilation; | ||
| // use .OnConflict().DoNothing() instead for PostgreSQL. | ||
| func (q *InsertQuery) Ignore() *InsertQuery { | ||
| q.useIgnore = true | ||
| return q | ||
| } | ||
|
|
||
| // OnConflict starts an upsert clause for PostgreSQL and SQLite dialects. | ||
| func (q *InsertQuery) OnConflict(columns ...schema.ColumnReference) *InsertConflictBuilder { | ||
| q.conflict = &insertConflictClause{columns: columns} | ||
|
|
@@ -287,8 +297,9 @@ func (q *InsertQuery) writeValuesSQL(ctx *compileContext) error { | |
| if err := q.validateSources(); err != nil { | ||
| return err | ||
| } | ||
| ctx.writeString("INSERT INTO ") | ||
| ctx.writeTableName(q.table) | ||
| if err := q.writeInsertInto(ctx); err != nil { | ||
| return err | ||
| } | ||
| if ctx.dialect.Name() == "mysql" { | ||
| ctx.writeString(" () VALUES ()") | ||
| } else { | ||
|
|
@@ -299,8 +310,9 @@ func (q *InsertQuery) writeValuesSQL(ctx *compileContext) error { | |
| return err | ||
| } | ||
|
|
||
| ctx.writeString("INSERT INTO ") | ||
| ctx.writeTableName(q.table) | ||
| if err := q.writeInsertInto(ctx); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if q.models != nil { | ||
| if err := q.writeModelsSQL(ctx); err != nil { | ||
|
|
@@ -648,8 +660,9 @@ func (q *InsertQuery) writeSelectSQL(ctx *compileContext) error { | |
| selectQuery = selectQuery.withSQLiteInsertSelectConflictWhere() | ||
| } | ||
|
|
||
| ctx.writeString("INSERT INTO ") | ||
| ctx.writeTableName(q.table) | ||
| if err := q.writeInsertInto(ctx); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if len(q.columns) > 0 { | ||
| ctx.writeString(" (") | ||
|
|
@@ -729,6 +742,43 @@ func (q *InsertQuery) Scan(ctx context.Context, dest any) error { | |
| return err | ||
| } | ||
|
|
||
| func (q *InsertQuery) writeInsertInto(ctx *compileContext) error { | ||
| dialectName := ctx.dialect.Name() | ||
|
|
||
| renderIgnore := q.useIgnore | ||
| if q.conflict != nil && q.conflict.action == insertConflictActionDoNothing { | ||
| if dialectName == "mysql" { | ||
| renderIgnore = true | ||
| } else if dialectName == "sqlite" && len(q.conflict.columns) == 0 && q.conflict.constraint == "" { | ||
| renderIgnore = true | ||
| } | ||
| } | ||
|
|
||
| if renderIgnore { | ||
| switch dialectName { | ||
| case "mysql": | ||
| ctx.writeString("INSERT IGNORE INTO ") | ||
| case "sqlite": | ||
| ctx.writeString("INSERT OR IGNORE INTO ") | ||
| case "postgres": | ||
| if q.useIgnore { | ||
| return errors.New("rain: .Ignore() is not supported for PostgreSQL; use .OnConflict().DoNothing() instead") | ||
| } | ||
| ctx.writeString("INSERT INTO ") | ||
| default: | ||
| if q.useIgnore { | ||
| return fmt.Errorf("rain: .Ignore() is not implemented for %s dialect", dialectName) | ||
| } | ||
| ctx.writeString("INSERT INTO ") | ||
| } | ||
| } else { | ||
| ctx.writeString("INSERT INTO ") | ||
| } | ||
|
|
||
| ctx.writeTableName(q.table) | ||
| return nil | ||
| } | ||
|
|
||
| func (q *InsertQuery) validateSources() error { | ||
| if q.table == nil { | ||
| return errors.New("rain: insert query requires a table") | ||
|
|
@@ -784,14 +834,7 @@ func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { | |
| return errors.New("rain: MySQL ON DUPLICATE KEY UPDATE does not support WHERE filters") | ||
| } | ||
| if q.conflict.action == insertConflictActionDoNothing { | ||
| noopColumn, err := mysqlConflictNoopColumn(q.table) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| ctx.writeString(" ON DUPLICATE KEY UPDATE ") | ||
| ctx.writeQuotedIdentifier(noopColumn.Name) | ||
| ctx.writeString(" = ") | ||
| ctx.writeQuotedIdentifier(noopColumn.Name) | ||
| // Handled by INSERT IGNORE in writeInsertInto | ||
| return nil | ||
| } | ||
|
Comment on lines
836
to
839
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The old Prompt To Fix With AIThis is a comment left during a code review.
Path: pkg/rain/query_insert.go
Line: 836-839
Comment:
**MySQL `INSERT IGNORE` silences more errors than the old no-op idiom**
The old `ON DUPLICATE KEY UPDATE id = id` approach surfaces every error _except_ a duplicate-key violation. `INSERT IGNORE` converts **all** MySQL errors — data truncation, out-of-range values, FK violations, generated-column failures, etc. — into warnings and silently discards the affected row. Any existing caller of `OnConflict().DoNothing()` on MySQL that relied on non-duplicate-key errors being propagated will now silently swallow those errors without any indication that something went wrong. The PR description does not call out this behavioral difference, and there are no tests that verify non-duplicate-key errors still surface.
How can I resolve this? If you propose a fix, please make it concise. |
||
| if q.conflict.action == insertConflictActionDoUpdateSet { | ||
|
|
@@ -817,6 +860,16 @@ func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { | |
| } | ||
|
|
||
| if len(q.conflict.columns) == 0 && q.conflict.constraint == "" { | ||
| if q.conflict.action == insertConflictActionDoNothing { | ||
| if q.dialect.Name() == "postgres" { | ||
| ctx.writeString(" ON CONFLICT DO NOTHING") | ||
| return nil | ||
| } | ||
| if q.dialect.Name() == "sqlite" { | ||
| // Handled by INSERT OR IGNORE in writeInsertInto | ||
| return nil | ||
| } | ||
| } | ||
| return errors.New("rain: conflict clause requires at least one target (columns or constraint)") | ||
| } | ||
|
|
||
|
|
@@ -876,28 +929,3 @@ func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { | |
|
|
||
| return nil | ||
| } | ||
|
|
||
| func mysqlConflictNoopColumn(table *schema.TableDef) (*schema.ColumnDef, error) { | ||
| if table == nil { | ||
| return nil, errors.New("rain: insert query requires a table") | ||
| } | ||
|
|
||
| if primaryKey, err := tablePrimaryKeyConstraint(table); err != nil { | ||
| return nil, err | ||
| } else if primaryKey != nil && len(primaryKey.Columns) > 0 { | ||
| return primaryKey.Columns[0], nil | ||
| } | ||
|
|
||
| if primaryKeys := primaryKeyColumns(table); len(primaryKeys) > 0 { | ||
| return primaryKeys[0], nil | ||
| } | ||
|
|
||
| if len(table.Columns) == 0 { | ||
| return nil, fmt.Errorf("rain: table %q has no columns for MySQL conflict DO NOTHING", table.Name) | ||
| } | ||
|
|
||
| // MySQL requires an assignment after ON DUPLICATE KEY UPDATE. A table without | ||
| // primary key metadata can still conflict on a unique index, so use the first | ||
| // declared column only as a visible no-op target. | ||
| return table.Columns[0], nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignore()+OnConflict().DoUpdateSet()combination is not validated on MySQLWhen
useIgnoreistrueand the conflict action isinsertConflictActionDoUpdateSet,writeInsertIntowritesINSERT IGNORE INTO, thenwriteConflictClauseappendsON DUPLICATE KEY UPDATE .... The resulting SQL —INSERT IGNORE INTO … ON DUPLICATE KEY UPDATE col = ?— is syntactically valid in MySQL but semantically contradictory: theON DUPLICATE KEY UPDATEclause specifically handles duplicate-key conflicts, whileINSERT IGNOREsilently discards all other errors. A user who calls.Ignore().OnConflict().DoUpdateSet(col)likely expects an error, not silently contradictory SQL. Consider adding a validation thatuseIgnoreandDoUpdateSetare mutually exclusive for MySQL.Prompt To Fix With AI