-
Notifications
You must be signed in to change notification settings - Fork 334
MyXQL: allow use of INSERT IGNORE for on_conflict: :nothing #703
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
base: master
Are you sure you want to change the base?
MyXQL: allow use of INSERT IGNORE for on_conflict: :nothing #703
Conversation
The previous implementation used `ON DUPLICATE KEY UPDATE col = col` which
had incorrect semantics:
1. It reported 1 row affected even when a row was skipped due to a duplicate
key conflict, because the UPDATE clause still matched the existing row
2. This caused insert_all to return {1, nil} instead of {0, nil} for ignored
duplicates, misrepresenting the actual number of inserted records
3. The UPDATE clause could trigger unnecessary row-level operations
This change:
- Uses `INSERT IGNORE INTO` which properly ignores duplicate key conflicts
without affecting existing rows or incrementing the affected row count
- Handles num_rows: 0 in the adapter by returning {:ok, []} for
on_conflict: :nothing, since 0 rows is expected behavior when all rows
are duplicates
- Updates tests to verify correct row counts: {0, nil} for all-duplicates,
{N, nil} for N successfully inserted non-duplicate rows
|
I guess this means at least a PATCH version. This should be considered a breaking change. Or we can somehow put it behind a configuration with the default being the existing behavior. |
|
I don't believe it is as simple as this. Please take a read here: https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#ignore-effect-on-execution For example this part:
The issue seems to be that MySQL doesn't have any equivalent to |
|
Yes, I read the docs but for some reason the data conversion part didn't trigger. There are 3 parts in this section:
So if we have to choose our poison here we're indeed between 2 suboptimal solutions when we provide
I think preventing 2 is more important for a core piece like Thanks for pointing that out @greg-rychlewski. |
on_conflict: :nothing, confict_target: {:unsafe_fragment, "insert_ignore"}}
on_conflict: :nothing, confict_target: {:unsafe_fragment, "insert_ignore"}}|
@greg-rychlewski did a last try to make this opt-in under a certain I understand it looks like a magic value but check this out and let me know what you think. |
|
Yeah, |
Yeah, tried that but if I read this correctly it requires change to the Connection behavior since it doesn't take opts there. I'll give it another try. |
|
You can probably add a default arguments |
When using on_conflict: :nothing, the MySQL adapter uses the
ON DUPLICATE KEY UPDATE x = x workaround which always reports
1 affected row regardless of actual insert.
This adds insert_mode: :ignore_errors option for insert_all that
uses INSERT IGNORE instead, providing accurate row counts (0 when
ignored, 1 when inserted).
Usage:
Repo.insert_all(Post, posts,
on_conflict: :nothing,
insert_mode: :ignore_errors)
The Connection behavior is updated to accept opts in insert/8,
allowing adapter-specific options to flow through.
8c7955d to
cb3f237
Compare
I couldn't really do that because insert_all is coming from SQL. Tried to make this as straight forward as possible and add opts to the behavior and then just ignore it on PG and TDS implementations. |
| The `insert_mode: :ignore_errors` option only affects the behavior of | ||
| `on_conflict: :nothing`. |
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.
I think this can be a little bit misleading. It can effect the behaviour of any insert query because we are injecting it without looking at what conflict option was chosen.
So maybe rewriting this to say it changes the default sql for :nothing would be clearer. And probably having a specific example showing it with the option on and off to really drive the point home.
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.
Oh wait you are checking the conflict option when injecting it. Then I think there is a separate question if we should do that. Because IGNORE is an independent modifier in MySQL. We should probably allow it to be added whenever the user wants or raise if we want to only allow this one use case.
Personally I like the former. What do you think @josevalim ?
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.
Good call. We should probably have a section exclusively on the :insert_mode option and in there say it affects the behaviour of on_conflct: :nothing as well, as the mean feature here is no longer the upsert itself.
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.
We should probably allow it to be added whenever the user wants or raise if we want to only allow this one use case.
Yes, exactly. We should allow insert_mode everywhere, regardless of :on_conflict, and then document that it affects the on_conflict behaviour too.
The default on_conflict: :nothing behavior uses ON DUPLICATE KEY UPDATE x = x which has the following characteristics:
For users who need accurate row counts (0 when ignored, 1 when inserted), INSERT IGNORE can be explicitly enabled via:
With INSERT IGNORE:
Implementation
Following @josevalim's suggestion, this adds an opts parameter to the Connection behavior's insert/8 callback. The SQL.insert_all passes opts through to the connection, and the MyXQL adapter uses
insert_mode: :ignore_errorsto generate INSERT IGNORE. Other adapters (Postgres, TDS) accept and ignore this option.Note
INSERT IGNORE has broader semantics in MySQL - it ignores certain type conversion errors in addition to duplicate key conflicts - which is why it's not the default behavior.
ORIGINAL POST