-
Notifications
You must be signed in to change notification settings - Fork 0
Improve InsertQuery with IGNORE and flexible ON CONFLICT #148
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 | ||
| ignore bool | ||
|
|
||
| // OPTIMIZATION: Internal buffers to avoid heap allocations for common | ||
| // query shapes while keeping the struct size reasonable. | ||
|
|
@@ -42,7 +43,7 @@ const ( | |
| ) | ||
|
|
||
| type insertConflictClause struct { | ||
| columns []schema.ColumnReference | ||
| targets []schema.Expression | ||
| constraint string | ||
| targetWhere []schema.Predicate | ||
| action insertConflictAction | ||
|
|
@@ -184,9 +185,15 @@ func (q *InsertQuery) DefaultValues() *InsertQuery { | |
| return q | ||
| } | ||
|
|
||
| // Ignore configures the INSERT to use IGNORE for MySQL. | ||
| func (q *InsertQuery) Ignore() *InsertQuery { | ||
| q.ignore = 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} | ||
| func (q *InsertQuery) OnConflict(targets ...schema.Expression) *InsertConflictBuilder { | ||
| q.conflict = &insertConflictClause{targets: targets} | ||
| return &InsertConflictBuilder{query: q} | ||
| } | ||
|
|
||
|
|
@@ -261,6 +268,10 @@ func (q *InsertQuery) compile() (compiledQuery, error) { | |
| ctx := newCompileContext(q.dialect) | ||
| defer releaseCompileContext(ctx) | ||
|
|
||
| if q.dialect.Name() == "mysql" && q.conflict != nil && q.conflict.action == insertConflictActionDoNothing { | ||
| q.ignore = true | ||
| } | ||
|
|
||
| if q.selectQuery != nil { | ||
| if err := q.writeSelectSQL(ctx); err != nil { | ||
| return compiledQuery{}, err | ||
|
|
@@ -287,7 +298,11 @@ func (q *InsertQuery) writeValuesSQL(ctx *compileContext) error { | |
| if err := q.validateSources(); err != nil { | ||
| return err | ||
| } | ||
| ctx.writeString("INSERT INTO ") | ||
| ctx.writeString("INSERT ") | ||
| if q.ignore && ctx.dialect.Name() == "mysql" { | ||
| ctx.writeString("IGNORE ") | ||
| } | ||
| ctx.writeString("INTO ") | ||
| ctx.writeTableName(q.table) | ||
| if ctx.dialect.Name() == "mysql" { | ||
| ctx.writeString(" () VALUES ()") | ||
|
|
@@ -299,7 +314,11 @@ func (q *InsertQuery) writeValuesSQL(ctx *compileContext) error { | |
| return err | ||
| } | ||
|
|
||
| ctx.writeString("INSERT INTO ") | ||
| ctx.writeString("INSERT ") | ||
| if q.ignore && ctx.dialect.Name() == "mysql" { | ||
| ctx.writeString("IGNORE ") | ||
| } | ||
| ctx.writeString("INTO ") | ||
| ctx.writeTableName(q.table) | ||
|
|
||
| if q.models != nil { | ||
|
|
@@ -574,7 +593,11 @@ func (q *InsertQuery) writeSelectSQL(ctx *compileContext) error { | |
| selectQuery = selectQuery.withSQLiteInsertSelectConflictWhere() | ||
| } | ||
|
|
||
| ctx.writeString("INSERT INTO ") | ||
| ctx.writeString("INSERT ") | ||
| if q.ignore && ctx.dialect.Name() == "mysql" { | ||
| ctx.writeString("IGNORE ") | ||
| } | ||
| ctx.writeString("INTO ") | ||
| ctx.writeTableName(q.table) | ||
|
|
||
| if len(q.columns) > 0 { | ||
|
|
@@ -703,7 +726,11 @@ func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { | |
| } | ||
|
|
||
| if q.dialect.Name() == "mysql" { | ||
| if len(q.conflict.columns) > 0 || q.conflict.constraint != "" || len(q.conflict.targetWhere) > 0 { | ||
| if q.conflict.action == insertConflictActionDoNothing { | ||
| return nil | ||
|
Comment on lines
+729
to
+730
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.
When a MySQL caller uses Context Used: AGENTS.md (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: pkg/rain/query_insert.go
Line: 729-730
Comment:
**Scoped Conflicts Become Global Ignore**
When a MySQL caller uses `OnConflict(users.Email).Where(...).DoNothing()` or `OnConstraint(...).DoNothing()`, this branch returns before rejecting the unsupported target fields and the compiler emits plain `INSERT IGNORE`. That drops the requested conflict scope and can silently ignore unrelated duplicate-key, NOT NULL, foreign-key, or data conversion errors instead of returning them.
**Context Used:** AGENTS.md ([source](https://app.greptile.com/hyperlocalise/github/hyperlocalise/rain-orm/-/custom-context?memory=3b24bd19-4fb7-4328-8fc3-52795c121ec6))
How can I resolve this? If you propose a fix, please make it concise. |
||
| } | ||
|
|
||
| if len(q.conflict.targets) > 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") | ||
| } | ||
| if len(q.conflict.updateWhere) > 0 { | ||
|
|
@@ -742,24 +769,30 @@ func (q *InsertQuery) writeConflictClause(ctx *compileContext) error { | |
| return nil | ||
| } | ||
|
|
||
| if len(q.conflict.columns) == 0 && q.conflict.constraint == "" { | ||
| if len(q.conflict.targets) == 0 && q.conflict.constraint == "" && q.conflict.action != insertConflictActionDoNothing { | ||
| return errors.New("rain: conflict clause requires at least one target (columns or constraint)") | ||
| } | ||
|
|
||
| ctx.writeString(" ON CONFLICT") | ||
| if q.conflict.constraint != "" { | ||
| ctx.writeString(" ON CONSTRAINT ") | ||
| ctx.writeQuotedIdentifier(q.conflict.constraint) | ||
| } else if len(q.conflict.columns) > 0 { | ||
| } else if len(q.conflict.targets) > 0 { | ||
| ctx.writeString(" (") | ||
| for idx, col := range q.conflict.columns { | ||
| if err := validateColumnBelongsToTable(q.table, col.ColumnDef()); err != nil { | ||
| return err | ||
| } | ||
| for idx, target := range q.conflict.targets { | ||
| if idx > 0 { | ||
| ctx.writeString(", ") | ||
| } | ||
| ctx.writeColumnName(col) | ||
| if col, ok := target.(schema.ColumnReference); ok { | ||
| if err := validateColumnBelongsToTable(q.table, col.ColumnDef()); err != nil { | ||
| return err | ||
| } | ||
| ctx.writeColumnName(col) | ||
| } else { | ||
| if err := ctx.writeExpression(target); err != nil { | ||
|
Comment on lines
+790
to
+792
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.
Non-column conflict targets are rendered with the general expression writer, which can add bind parameters for expressions such as Context Used: AGENTS.md (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: pkg/rain/query_insert.go
Line: 790-792
Comment:
**Conflict Targets Accept Parameters**
Non-column conflict targets are rendered with the general expression writer, which can add bind parameters for expressions such as `schema.Raw("lower(email COLLATE ?)", locale)`, placeholders, values, comparisons, or subqueries. PostgreSQL and SQLite conflict targets must name columns or index expressions, so this can return SQL like `ON CONFLICT (lower(email COLLATE $2))` with an extra argument that the database rejects instead of failing during query construction.
**Context Used:** AGENTS.md ([source](https://app.greptile.com/hyperlocalise/github/hyperlocalise/rain-orm/-/custom-context?memory=3b24bd19-4fb7-4328-8fc3-52795c121ec6))
How can I resolve this? If you propose a fix, please make it concise. |
||
| return err | ||
| } | ||
| } | ||
| } | ||
| ctx.writeByte(')') | ||
| } | ||
|
|
||
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.
ToSQL,Exec, andPreparenow mutate the builder by settingq.ignorewhen MySQLDoNothingis compiled. If the sameInsertQueryis compiled once forDoNothingand then reused with a different conflict action, the stale flag still emitsINSERT IGNORE, so a laterDoUpdateSetquery can compile asINSERT IGNORE ... ON DUPLICATE KEY UPDATE ...without the caller asking for ignore semantics.Context Used: AGENTS.md (source)
Prompt To Fix With AI