From 7478895a33518d8bacd63a587a1e58624ff989c2 Mon Sep 17 00:00:00 2001 From: Aggelos Karalias Date: Mon, 15 Dec 2025 18:09:27 +0200 Subject: [PATCH 1/2] MyXQL: Use INSERT IGNORE for on_conflict: :nothing 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 --- integration_test/myxql/upsert_all_test.exs | 61 ++++++++++++++++++++-- lib/ecto/adapters/myxql.ex | 14 +++-- lib/ecto/adapters/myxql/connection.ex | 12 +++-- test/ecto/adapters/myxql_test.exs | 7 ++- 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/integration_test/myxql/upsert_all_test.exs b/integration_test/myxql/upsert_all_test.exs index 0f15f1d8..606e5d72 100644 --- a/integration_test/myxql/upsert_all_test.exs +++ b/integration_test/myxql/upsert_all_test.exs @@ -13,10 +13,63 @@ defmodule Ecto.Integration.UpsertAllTest do test "on conflict ignore" do post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"] - assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == - {1, nil} - assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == - {1, nil} + # First insert succeeds - 1 row inserted + assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == {1, nil} + # Second insert is ignored due to duplicate - 0 rows inserted (INSERT IGNORE behavior) + assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == {0, nil} + end + + test "on conflict ignore with mixed records (some conflicts, some new)" do + # Insert an existing post + existing_uuid = "6fa459ea-ee8a-3ca4-894e-db77e160355e" + existing_post = [title: "existing", uuid: existing_uuid] + assert TestRepo.insert_all(Post, [existing_post], on_conflict: :nothing) == {1, nil} + + # Now insert a batch with one duplicate and two new records + new_uuid1 = "7fa459ea-ee8a-3ca4-894e-db77e160355f" + new_uuid2 = "8fa459ea-ee8a-3ca4-894e-db77e160355a" + + posts = [ + [title: "new post 1", uuid: new_uuid1], # new - should be inserted + [title: "duplicate", uuid: existing_uuid], # duplicate - should be ignored + [title: "new post 2", uuid: new_uuid2] # new - should be inserted + ] + + # With INSERT IGNORE, only 2 rows should be inserted (the non-duplicates) + assert TestRepo.insert_all(Post, posts, on_conflict: :nothing) == {2, nil} + + # Verify the data - should have 3 posts total (1 existing + 2 new) + assert length(TestRepo.all(Post)) == 3 + + # Verify the existing post was not modified + [original] = TestRepo.all(from p in Post, where: p.uuid == ^existing_uuid) + assert original.title == "existing" # title unchanged + + # Verify new posts were inserted + assert TestRepo.exists?(from p in Post, where: p.uuid == ^new_uuid1) + assert TestRepo.exists?(from p in Post, where: p.uuid == ^new_uuid2) + end + + test "on conflict ignore with all duplicates" do + # Insert initial posts + uuid1 = "1fa459ea-ee8a-3ca4-894e-db77e160355e" + uuid2 = "2fa459ea-ee8a-3ca4-894e-db77e160355e" + initial_posts = [ + [title: "first", uuid: uuid1], + [title: "second", uuid: uuid2] + ] + assert TestRepo.insert_all(Post, initial_posts, on_conflict: :nothing) == {2, nil} + + # Try to insert all duplicates + duplicate_posts = [ + [title: "dup1", uuid: uuid1], + [title: "dup2", uuid: uuid2] + ] + # All are duplicates, so 0 rows inserted + assert TestRepo.insert_all(Post, duplicate_posts, on_conflict: :nothing) == {0, nil} + + # Verify count unchanged + assert length(TestRepo.all(Post)) == 2 end test "on conflict keyword list" do diff --git a/lib/ecto/adapters/myxql.ex b/lib/ecto/adapters/myxql.ex index dc595d06..75eb0180 100644 --- a/lib/ecto/adapters/myxql.ex +++ b/lib/ecto/adapters/myxql.ex @@ -330,9 +330,17 @@ defmodule Ecto.Adapters.MyXQL do case Ecto.Adapters.SQL.query(adapter_meta, sql, values ++ query_params, opts) do {:ok, %{num_rows: 0}} -> - raise "insert operation failed to insert any row in the database. " <> - "This may happen if you have trigger or other database conditions rejecting operations. " <> - "The emitted SQL was: #{sql}" + # With INSERT IGNORE (on_conflict: :nothing), 0 rows means the row was + # ignored due to a conflict, which is expected behavior + case on_conflict do + {:nothing, _, _} -> + {:ok, []} + + _ -> + raise "insert operation failed to insert any row in the database. " <> + "This may happen if you have trigger or other database conditions rejecting operations. " <> + "The emitted SQL was: #{sql}" + end # We were used to check if num_rows was 1 or 2 (in case of upserts) # but MariaDB supports tables with System Versioning, and in those diff --git a/lib/ecto/adapters/myxql/connection.ex b/lib/ecto/adapters/myxql/connection.ex index ae378beb..6ab887a5 100644 --- a/lib/ecto/adapters/myxql/connection.ex +++ b/lib/ecto/adapters/myxql/connection.ex @@ -181,9 +181,10 @@ if Code.ensure_loaded?(MyXQL) do @impl true def insert(prefix, table, header, rows, on_conflict, [], []) do fields = quote_names(header) + insert_keyword = insert_keyword(on_conflict) [ - "INSERT INTO ", + insert_keyword, quote_table(prefix, table), " (", fields, @@ -192,6 +193,9 @@ if Code.ensure_loaded?(MyXQL) do ] end + defp insert_keyword({:nothing, _, []}), do: "INSERT IGNORE INTO " + defp insert_keyword(_), do: "INSERT INTO " + def insert(_prefix, _table, _header, _rows, _on_conflict, _returning, []) do error!(nil, ":returning is not supported in insert/insert_all by MySQL") end @@ -208,9 +212,9 @@ if Code.ensure_loaded?(MyXQL) do [] end - defp on_conflict({:nothing, _, []}, [field | _]) do - quoted = quote_name(field) - [" ON DUPLICATE KEY UPDATE ", quoted, " = " | quoted] + defp on_conflict({:nothing, _, []}, _header) do + # Handled by INSERT IGNORE + [] end defp on_conflict({fields, _, []}, _header) when is_list(fields) do diff --git a/test/ecto/adapters/myxql_test.exs b/test/ecto/adapters/myxql_test.exs index 34641e0d..df5261f6 100644 --- a/test/ecto/adapters/myxql_test.exs +++ b/test/ecto/adapters/myxql_test.exs @@ -1465,11 +1465,10 @@ defmodule Ecto.Adapters.MyXQLTest do end end - test "insert with on duplicate key" do + test "insert with on conflict" do + # Using INSERT IGNORE for :nothing on_conflict query = insert(nil, "schema", [:x, :y], [[:x, :y]], {:nothing, [], []}, []) - - assert query == - ~s{INSERT INTO `schema` (`x`,`y`) VALUES (?,?) ON DUPLICATE KEY UPDATE `x` = `x`} + assert query == ~s{INSERT IGNORE INTO `schema` (`x`,`y`) VALUES (?,?)} update = from("schema", update: [set: [z: "foo"]]) |> plan(:update_all) query = insert(nil, "schema", [:x, :y], [[:x, :y]], {update, [], []}, []) From cb3f237085abb8ec90e41e11e6449c98b94a34f7 Mon Sep 17 00:00:00 2001 From: Aggelos Karalias Date: Tue, 16 Dec 2025 11:41:15 +0200 Subject: [PATCH 2/2] Add opt-in INSERT IGNORE support for MySQL via insert_mode option 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. --- integration_test/myxql/upsert_all_test.exs | 64 +++++++++++++++------- lib/ecto/adapters/myxql.ex | 42 ++++++++++---- lib/ecto/adapters/myxql/connection.ex | 48 ++++++++++------ lib/ecto/adapters/postgres/connection.ex | 2 +- lib/ecto/adapters/sql.ex | 2 +- lib/ecto/adapters/sql/connection.ex | 3 +- lib/ecto/adapters/tds/connection.ex | 2 +- test/ecto/adapters/myxql_test.exs | 28 ++++++++-- 8 files changed, 135 insertions(+), 56 deletions(-) diff --git a/integration_test/myxql/upsert_all_test.exs b/integration_test/myxql/upsert_all_test.exs index 606e5d72..c02f393e 100644 --- a/integration_test/myxql/upsert_all_test.exs +++ b/integration_test/myxql/upsert_all_test.exs @@ -13,60 +13,84 @@ defmodule Ecto.Integration.UpsertAllTest do test "on conflict ignore" do post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"] - # First insert succeeds - 1 row inserted + # Default :nothing behavior uses ON DUPLICATE KEY UPDATE x = x workaround + # which always reports rows as affected + assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == {1, nil} assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == {1, nil} + end + + test "insert_mode: :ignore_errors" do + post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"] + # First insert succeeds - 1 row inserted + assert TestRepo.insert_all(Post, [post], + on_conflict: :nothing, + insert_mode: :ignore_errors + ) == {1, nil} + # Second insert is ignored due to duplicate - 0 rows inserted (INSERT IGNORE behavior) - assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == {0, nil} + assert TestRepo.insert_all(Post, [post], + on_conflict: :nothing, + insert_mode: :ignore_errors + ) == {0, nil} end - test "on conflict ignore with mixed records (some conflicts, some new)" do + test "insert_mode: :ignore_errors with mixed records (some conflicts, some new)" do # Insert an existing post existing_uuid = "6fa459ea-ee8a-3ca4-894e-db77e160355e" existing_post = [title: "existing", uuid: existing_uuid] - assert TestRepo.insert_all(Post, [existing_post], on_conflict: :nothing) == {1, nil} + + assert TestRepo.insert_all(Post, [existing_post], + on_conflict: :nothing, + insert_mode: :ignore_errors + ) == {1, nil} # Now insert a batch with one duplicate and two new records new_uuid1 = "7fa459ea-ee8a-3ca4-894e-db77e160355f" new_uuid2 = "8fa459ea-ee8a-3ca4-894e-db77e160355a" posts = [ - [title: "new post 1", uuid: new_uuid1], # new - should be inserted - [title: "duplicate", uuid: existing_uuid], # duplicate - should be ignored - [title: "new post 2", uuid: new_uuid2] # new - should be inserted + [title: "new post 1", uuid: new_uuid1], + [title: "duplicate", uuid: existing_uuid], + [title: "new post 2", uuid: new_uuid2] ] # With INSERT IGNORE, only 2 rows should be inserted (the non-duplicates) - assert TestRepo.insert_all(Post, posts, on_conflict: :nothing) == {2, nil} + assert TestRepo.insert_all(Post, posts, + on_conflict: :nothing, + insert_mode: :ignore_errors + ) == {2, nil} # Verify the data - should have 3 posts total (1 existing + 2 new) assert length(TestRepo.all(Post)) == 3 # Verify the existing post was not modified [original] = TestRepo.all(from p in Post, where: p.uuid == ^existing_uuid) - assert original.title == "existing" # title unchanged + assert original.title == "existing" # Verify new posts were inserted assert TestRepo.exists?(from p in Post, where: p.uuid == ^new_uuid1) assert TestRepo.exists?(from p in Post, where: p.uuid == ^new_uuid2) end - test "on conflict ignore with all duplicates" do + test "insert_mode: :ignore_errors with all duplicates" do # Insert initial posts uuid1 = "1fa459ea-ee8a-3ca4-894e-db77e160355e" uuid2 = "2fa459ea-ee8a-3ca4-894e-db77e160355e" - initial_posts = [ - [title: "first", uuid: uuid1], - [title: "second", uuid: uuid2] - ] - assert TestRepo.insert_all(Post, initial_posts, on_conflict: :nothing) == {2, nil} + initial_posts = [[title: "first", uuid: uuid1], [title: "second", uuid: uuid2]] + + assert TestRepo.insert_all(Post, initial_posts, + on_conflict: :nothing, + insert_mode: :ignore_errors + ) == {2, nil} # Try to insert all duplicates - duplicate_posts = [ - [title: "dup1", uuid: uuid1], - [title: "dup2", uuid: uuid2] - ] + duplicate_posts = [[title: "dup1", uuid: uuid1], [title: "dup2", uuid: uuid2]] + # All are duplicates, so 0 rows inserted - assert TestRepo.insert_all(Post, duplicate_posts, on_conflict: :nothing) == {0, nil} + assert TestRepo.insert_all(Post, duplicate_posts, + on_conflict: :nothing, + insert_mode: :ignore_errors + ) == {0, nil} # Verify count unchanged assert length(TestRepo.all(Post)) == 2 diff --git a/lib/ecto/adapters/myxql.ex b/lib/ecto/adapters/myxql.ex index 75eb0180..9c68262d 100644 --- a/lib/ecto/adapters/myxql.ex +++ b/lib/ecto/adapters/myxql.ex @@ -103,6 +103,25 @@ defmodule Ecto.Adapters.MyXQL do automatically commits after some commands like CREATE TABLE. Therefore MySQL migrations does not run inside transactions. + ### Upserts + + When using `on_conflict: :nothing`, the adapter uses the + `ON DUPLICATE KEY UPDATE x = x` workaround to simulate "do nothing" + behavior. This always reports 1 affected row regardless of whether + the row was actually inserted or ignored. + + If you need accurate row counts (0 when ignored, 1 when inserted), + you can opt into MySQL's `INSERT IGNORE` by specifying: + + Repo.insert_all(Post, posts, + on_conflict: :nothing, + insert_mode: :ignore_errors) + + Note that `INSERT IGNORE` has broader semantics in MySQL - it also + ignores certain type conversion errors, not just duplicate key conflicts. + The `insert_mode: :ignore_errors` option only affects the behavior of + `on_conflict: :nothing`. + ## Old MySQL versions ### JSON support @@ -319,7 +338,10 @@ defmodule Ecto.Adapters.MyXQL do key = primary_key!(schema_meta, returning) {fields, values} = :lists.unzip(params) - sql = @conn.insert(prefix, source, fields, [fields], on_conflict, [], []) + + # Extract insert_mode and pass it to the connection's insert function + insert_opts = if opts[:insert_mode], do: [insert_mode: opts[:insert_mode]], else: [] + sql = @conn.insert(prefix, source, fields, [fields], on_conflict, [], [], insert_opts) opts = if is_nil(Keyword.get(opts, :cache_statement)) do @@ -330,16 +352,14 @@ defmodule Ecto.Adapters.MyXQL do case Ecto.Adapters.SQL.query(adapter_meta, sql, values ++ query_params, opts) do {:ok, %{num_rows: 0}} -> - # With INSERT IGNORE (on_conflict: :nothing), 0 rows means the row was - # ignored due to a conflict, which is expected behavior - case on_conflict do - {:nothing, _, _} -> - {:ok, []} - - _ -> - raise "insert operation failed to insert any row in the database. " <> - "This may happen if you have trigger or other database conditions rejecting operations. " <> - "The emitted SQL was: #{sql}" + # With INSERT IGNORE (insert_mode: :ignore_errors), 0 rows means the row + # was ignored due to a conflict, which is expected behavior + if opts[:insert_mode] == :ignore_errors do + {:ok, []} + else + raise "insert operation failed to insert any row in the database. " <> + "This may happen if you have trigger or other database conditions rejecting operations. " <> + "The emitted SQL was: #{sql}" end # We were used to check if num_rows was 1 or 2 (in case of upserts) diff --git a/lib/ecto/adapters/myxql/connection.ex b/lib/ecto/adapters/myxql/connection.ex index 6ab887a5..e6ceb02c 100644 --- a/lib/ecto/adapters/myxql/connection.ex +++ b/lib/ecto/adapters/myxql/connection.ex @@ -179,9 +179,11 @@ if Code.ensure_loaded?(MyXQL) do end @impl true - def insert(prefix, table, header, rows, on_conflict, [], []) do + def insert(prefix, table, header, rows, on_conflict, returning, placeholders, opts \\ []) + + def insert(prefix, table, header, rows, on_conflict, [], [], opts) do fields = quote_names(header) - insert_keyword = insert_keyword(on_conflict) + insert_keyword = insert_keyword(on_conflict, opts) [ insert_keyword, @@ -189,35 +191,49 @@ if Code.ensure_loaded?(MyXQL) do " (", fields, ") ", - insert_all(rows) | on_conflict(on_conflict, header) + insert_all(rows) | on_conflict(on_conflict, header, opts) ] end - defp insert_keyword({:nothing, _, []}), do: "INSERT IGNORE INTO " - defp insert_keyword(_), do: "INSERT INTO " - - def insert(_prefix, _table, _header, _rows, _on_conflict, _returning, []) do + def insert(_prefix, _table, _header, _rows, _on_conflict, _returning, [], _opts) do error!(nil, ":returning is not supported in insert/insert_all by MySQL") end - def insert(_prefix, _table, _header, _rows, _on_conflict, _returning, _placeholders) do + def insert(_prefix, _table, _header, _rows, _on_conflict, _returning, _placeholders, _opts) do error!(nil, ":placeholders is not supported by MySQL") end - defp on_conflict({_, _, [_ | _]}, _header) do + # INSERT IGNORE when insert_mode: :ignore_errors is passed + defp insert_keyword({:nothing, _, _}, opts) do + if Keyword.get(opts, :insert_mode) == :ignore_errors do + "INSERT IGNORE INTO " + else + "INSERT INTO " + end + end + + defp insert_keyword(_, _opts), do: "INSERT INTO " + + defp on_conflict({_, _, [_ | _]}, _header, _opts) do error!(nil, ":conflict_target is not supported in insert/insert_all by MySQL") end - defp on_conflict({:raise, _, []}, _header) do + defp on_conflict({:raise, _, []}, _header, _opts) do [] end - defp on_conflict({:nothing, _, []}, _header) do - # Handled by INSERT IGNORE - [] + # With insert_mode: :ignore_errors, INSERT IGNORE handles conflicts - no ON DUPLICATE KEY needed + defp on_conflict({:nothing, _, []}, [field | _], opts) do + if Keyword.get(opts, :insert_mode) == :ignore_errors do + [] + else + # Default :nothing - uses workaround to simulate "do nothing" behavior + quoted = quote_name(field) + [" ON DUPLICATE KEY UPDATE ", quoted, " = " | quoted] + end end - defp on_conflict({fields, _, []}, _header) when is_list(fields) do + defp on_conflict({fields, _, []}, _header, _opts) when is_list(fields) do [ " ON DUPLICATE KEY UPDATE " | Enum.map_intersperse(fields, ?,, fn field -> @@ -227,11 +243,11 @@ if Code.ensure_loaded?(MyXQL) do ] end - defp on_conflict({%{wheres: []} = query, _, []}, _header) do + defp on_conflict({%{wheres: []} = query, _, []}, _header, _opts) do [" ON DUPLICATE KEY " | update_all(query, "UPDATE ")] end - defp on_conflict({_query, _, []}, _header) do + defp on_conflict({_query, _, []}, _header, _opts) do error!( nil, "Using a query with :where in combination with the :on_conflict option is not supported by MySQL" diff --git a/lib/ecto/adapters/postgres/connection.ex b/lib/ecto/adapters/postgres/connection.ex index 45c177cd..d8f063f7 100644 --- a/lib/ecto/adapters/postgres/connection.ex +++ b/lib/ecto/adapters/postgres/connection.ex @@ -229,7 +229,7 @@ if Code.ensure_loaded?(Postgrex) do end @impl true - def insert(prefix, table, header, rows, on_conflict, returning, placeholders) do + def insert(prefix, table, header, rows, on_conflict, returning, placeholders, _opts \\ []) do counter_offset = length(placeholders) + 1 values = diff --git a/lib/ecto/adapters/sql.ex b/lib/ecto/adapters/sql.ex index e0c8cd15..879f39ea 100644 --- a/lib/ecto/adapters/sql.ex +++ b/lib/ecto/adapters/sql.ex @@ -970,7 +970,7 @@ defmodule Ecto.Adapters.SQL do rows -> unzip_inserts(header, rows) end - sql = conn.insert(prefix, source, header, rows, on_conflict, returning, placeholders) + sql = conn.insert(prefix, source, header, rows, on_conflict, returning, placeholders, opts) opts = if is_nil(Keyword.get(opts, :cache_statement)) do diff --git a/lib/ecto/adapters/sql/connection.ex b/lib/ecto/adapters/sql/connection.ex index 0b311754..19eee158 100644 --- a/lib/ecto/adapters/sql/connection.ex +++ b/lib/ecto/adapters/sql/connection.ex @@ -94,7 +94,8 @@ defmodule Ecto.Adapters.SQL.Connection do rows :: [[atom | nil]], on_conflict :: Ecto.Adapter.Schema.on_conflict(), returning :: [atom], - placeholders :: [term] + placeholders :: [term], + opts :: Keyword.t() ) :: iodata @doc """ diff --git a/lib/ecto/adapters/tds/connection.ex b/lib/ecto/adapters/tds/connection.ex index c4053b76..d2e18902 100644 --- a/lib/ecto/adapters/tds/connection.ex +++ b/lib/ecto/adapters/tds/connection.ex @@ -220,7 +220,7 @@ if Code.ensure_loaded?(Tds) do end @impl true - def insert(prefix, table, header, rows, on_conflict, returning, placeholders) do + def insert(prefix, table, header, rows, on_conflict, returning, placeholders, _opts \\ []) do counter_offset = length(placeholders) + 1 [] = on_conflict(on_conflict, header) returning = returning(returning, "INSERTED") diff --git a/test/ecto/adapters/myxql_test.exs b/test/ecto/adapters/myxql_test.exs index df5261f6..c707b7e2 100644 --- a/test/ecto/adapters/myxql_test.exs +++ b/test/ecto/adapters/myxql_test.exs @@ -56,8 +56,8 @@ defmodule Ecto.Adapters.MyXQLTest do defp delete_all(query), do: query |> SQL.delete_all() |> IO.iodata_to_binary() defp execute_ddl(query), do: query |> SQL.execute_ddl() |> Enum.map(&IO.iodata_to_binary/1) - defp insert(prefx, table, header, rows, on_conflict, returning) do - IO.iodata_to_binary(SQL.insert(prefx, table, header, rows, on_conflict, returning, [])) + defp insert(prefx, table, header, rows, on_conflict, returning, opts \\ []) do + IO.iodata_to_binary(SQL.insert(prefx, table, header, rows, on_conflict, returning, [], opts)) end defp update(prefx, table, fields, filter, returning) do @@ -1465,10 +1465,12 @@ defmodule Ecto.Adapters.MyXQLTest do end end - test "insert with on conflict" do - # Using INSERT IGNORE for :nothing on_conflict + test "insert with on duplicate key" do + # Default :nothing uses ON DUPLICATE KEY UPDATE workaround query = insert(nil, "schema", [:x, :y], [[:x, :y]], {:nothing, [], []}, []) - assert query == ~s{INSERT IGNORE INTO `schema` (`x`,`y`) VALUES (?,?)} + + assert query == + ~s{INSERT INTO `schema` (`x`,`y`) VALUES (?,?) ON DUPLICATE KEY UPDATE `x` = `x`} update = from("schema", update: [set: [z: "foo"]]) |> plan(:update_all) query = insert(nil, "schema", [:x, :y], [[:x, :y]], {update, [], []}, []) @@ -1498,6 +1500,22 @@ defmodule Ecto.Adapters.MyXQLTest do end end + test "insert with insert_mode: :ignore_errors" do + # INSERT IGNORE via insert_mode: :ignore_errors option + query = + insert( + nil, + "schema", + [:x, :y], + [[:x, :y]], + {:nothing, [], []}, + [], + insert_mode: :ignore_errors + ) + + assert query == ~s{INSERT IGNORE INTO `schema` (`x`,`y`) VALUES (?,?)} + end + test "insert with query" do select_query = from("schema", select: [:id]) |> plan(:all)