From b4c3ef6f4e3e6e1b9d4cfb3d524efbfd07bc3664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=A4rtschi?= Date: Wed, 25 Feb 2026 10:28:00 +0100 Subject: [PATCH 1/6] fix: bulk_create with upsert now updates update_timestamp fields Ports ash-project/ash_postgres#697 to ash_sqlite. update_timestamp attributes (e.g. updated_at) were never included in the ON CONFLICT DO UPDATE SET clause because they have writable?: false. Now fields with update_defaults are always included when an upsert modifies fields. Can be disabled via context: %{data_layer: %{touch_update_defaults?: false}} --- documentation/dsls/DSL-AshSqlite.DataLayer.md | 2 +- lib/data_layer.ex | 23 +++- .../migration_generator.ex | 32 +++-- lib/migration_generator/operation.ex | 3 +- test/bulk_create_test.exs | 116 ++++++++++++++++++ 5 files changed, 159 insertions(+), 17 deletions(-) diff --git a/documentation/dsls/DSL-AshSqlite.DataLayer.md b/documentation/dsls/DSL-AshSqlite.DataLayer.md index 777fee2..51b0947 100644 --- a/documentation/dsls/DSL-AshSqlite.DataLayer.md +++ b/documentation/dsls/DSL-AshSqlite.DataLayer.md @@ -35,7 +35,7 @@ end | Name | Type | Default | Docs | |------|------|---------|------| -| [`repo`](#sqlite-repo){: #sqlite-repo .spark-required} | `atom` | | The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more | +| [`repo`](#sqlite-repo){: #sqlite-repo .spark-required} | `module \| (any, any -> any)` | | The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more. Can also be a function that takes a resource and a type `:read \| :mutate` and returns the repo. | | [`migrate?`](#sqlite-migrate?){: #sqlite-migrate? } | `boolean` | `true` | Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations` | | [`migration_types`](#sqlite-migration_types){: #sqlite-migration_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults. | | [`migration_defaults`](#sqlite-migration_defaults){: #sqlite-migration_defaults } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\"now()\\")`, or for `nil`, use `\\"nil\\"`. | diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 7fb4549..cd43d30 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -804,8 +804,27 @@ defmodule AshSqlite.DataLayer do fields_to_upsert = case fields_to_upsert do - [] -> keys - fields_to_upsert -> fields_to_upsert + [] -> + keys + + fields_to_upsert -> + # Include fields with update_defaults (e.g. update_timestamp) + # even if they aren't in the changeset attributes or upsert_fields. + # These fields should always be refreshed when an upsert modifies fields. + # Can be disabled via context: %{data_layer: %{touch_update_defaults?: false}} + touch_update_defaults? = + Enum.at(changesets, 0).context[:data_layer][:touch_update_defaults?] != false + + if touch_update_defaults? do + update_default_fields = + update_defaults + |> Keyword.keys() + |> Enum.reject(&(&1 in fields_to_upsert or &1 in keys)) + + fields_to_upsert ++ update_default_fields + else + fields_to_upsert + end end fields_to_upsert diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 809b81f..8b0766d 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -467,11 +467,13 @@ defmodule AshSqlite.MigrationGenerator do defp load_migration!({version, _, file}) when is_binary(file) do loaded_modules = file |> compile_file() |> Enum.map(&elem(&1, 0)) - if mod = Enum.find(loaded_modules, &migration?/1) do - {version, mod} - else - raise Ecto.MigrationError, - "file #{Path.relative_to_cwd(file)} does not define an Ecto.Migration" + case Enum.find(loaded_modules, &migration?/1) do + nil -> + raise Ecto.MigrationError, + "file #{Path.relative_to_cwd(file)} does not define an Ecto.Migration" + + mod -> + {version, mod} end end @@ -834,13 +836,15 @@ defmodule AshSqlite.MigrationGenerator do config = repo.config() app = Keyword.fetch!(config, :otp_app) - if path = opts.migration_path || config[:tenant_migrations_path] do - path - else - priv = - config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" + case opts.migration_path || config[:tenant_migrations_path] do + nil -> + priv = + config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" + + Application.app_dir(app, Path.join(priv, "migrations")) - Application.app_dir(app, Path.join(priv, "migrations")) + path -> + path end end @@ -1634,7 +1638,8 @@ defmodule AshSqlite.MigrationGenerator do identity.name == old_identity.name && Enum.sort(old_identity.keys) == Enum.sort(identity.keys) && old_identity.base_filter == identity.base_filter && - Map.get(old_identity, :nils_distinct?, true) == Map.get(identity, :nils_distinct?, true) + Map.get(old_identity, :nils_distinct?, true) == + Map.get(identity, :nils_distinct?, true) end) end) end @@ -1677,7 +1682,8 @@ defmodule AshSqlite.MigrationGenerator do old_identity.name == identity.name && Enum.sort(old_identity.keys) == Enum.sort(identity.keys) && old_identity.base_filter == identity.base_filter && - Map.get(old_identity, :nils_distinct?, true) == Map.get(identity, :nils_distinct?, true) + Map.get(old_identity, :nils_distinct?, true) == + Map.get(identity, :nils_distinct?, true) end) end) end diff --git a/lib/migration_generator/operation.ex b/lib/migration_generator/operation.ex index a91fb8d..ffcac60 100644 --- a/lib/migration_generator/operation.ex +++ b/lib/migration_generator/operation.ex @@ -484,7 +484,8 @@ defmodule AshSqlite.MigrationGenerator.Operation do import Helper def up(%{ - identity: %{name: name, keys: keys, base_filter: base_filter, index_name: index_name} = identity, + identity: + %{name: name, keys: keys, base_filter: base_filter, index_name: index_name} = identity, table: table, multitenancy: multitenancy }) do diff --git a/test/bulk_create_test.exs b/test/bulk_create_test.exs index a18b310..2378df1 100644 --- a/test/bulk_create_test.exs +++ b/test/bulk_create_test.exs @@ -68,6 +68,122 @@ defmodule AshSqlite.BulkCreateTest do end) end + test "bulk creates with upsert updates update_timestamp" do + past = DateTime.to_iso8601(DateTime.add(DateTime.utc_now(), -60, :second)) + + assert [{:ok, %{title: "fred", uniq_one: "one", uniq_two: "two"} = initial}] = + Ash.bulk_create!( + [%{title: "fred", uniq_one: "one", uniq_two: "two", price: 10}], + Post, + :create, + return_stream?: true, + return_records?: true + ) + |> Enum.to_list() + + # Backdate updated_at via raw SQL so the upsert has something to compare against + AshSqlite.TestRepo.query!("UPDATE posts SET updated_at = ? WHERE id = ?", [ + past, + initial.id + ]) + + assert [%{updated_at: backdated}] = Ash.read!(Post) + assert DateTime.compare(backdated, DateTime.from_iso8601(past) |> elem(1)) == :eq + + assert [{:ok, %{title: "fred", price: 1000} = upserted}] = + Ash.bulk_create!( + [%{title: "something", uniq_one: "one", uniq_two: "two", price: 1000}], + Post, + :create, + upsert?: true, + upsert_identity: :uniq_one_and_two, + upsert_fields: [:price], + return_stream?: true, + return_errors?: true, + return_records?: true + ) + |> Enum.to_list() + + assert DateTime.after?(upserted.updated_at, DateTime.from_iso8601(past) |> elem(1)) + end + + test "bulk creates with empty upsert does not update update_timestamp" do + past = DateTime.to_iso8601(DateTime.add(DateTime.utc_now(), -60, :second)) + + assert [{:ok, %{title: "fred", uniq_one: "one", uniq_two: "two"} = initial}] = + Ash.bulk_create!( + [%{title: "fred", uniq_one: "one", uniq_two: "two", price: 10}], + Post, + :create, + return_stream?: true, + return_records?: true + ) + |> Enum.to_list() + + AshSqlite.TestRepo.query!("UPDATE posts SET updated_at = ? WHERE id = ?", [ + past, + initial.id + ]) + + assert [%{updated_at: backdated}] = Ash.read!(Post) + assert DateTime.compare(backdated, DateTime.from_iso8601(past) |> elem(1)) == :eq + + assert [{:ok, %{title: "fred"} = upserted}] = + Ash.bulk_create!( + [%{title: "something", uniq_one: "one", uniq_two: "two", price: 1000}], + Post, + :create, + upsert?: true, + upsert_identity: :uniq_one_and_two, + upsert_fields: [], + return_stream?: true, + return_errors?: true, + return_records?: true + ) + |> Enum.to_list() + + assert DateTime.compare(upserted.updated_at, DateTime.from_iso8601(past) |> elem(1)) == :eq + end + + test "bulk creates with upsert does not update update_timestamp when touch_update_defaults? is false" do + past = DateTime.to_iso8601(DateTime.add(DateTime.utc_now(), -60, :second)) + + assert [{:ok, %{title: "fred", uniq_one: "one", uniq_two: "two"} = initial}] = + Ash.bulk_create!( + [%{title: "fred", uniq_one: "one", uniq_two: "two", price: 10}], + Post, + :create, + return_stream?: true, + return_records?: true + ) + |> Enum.to_list() + + AshSqlite.TestRepo.query!("UPDATE posts SET updated_at = ? WHERE id = ?", [ + past, + initial.id + ]) + + assert [%{updated_at: backdated}] = Ash.read!(Post) + assert DateTime.compare(backdated, DateTime.from_iso8601(past) |> elem(1)) == :eq + + assert [{:ok, %{title: "fred", price: 1000} = upserted}] = + Ash.bulk_create!( + [%{title: "something", uniq_one: "one", uniq_two: "two", price: 1000}], + Post, + :create, + upsert?: true, + upsert_identity: :uniq_one_and_two, + upsert_fields: [:price], + context: %{data_layer: %{touch_update_defaults?: false}}, + return_stream?: true, + return_errors?: true, + return_records?: true + ) + |> Enum.to_list() + + assert DateTime.compare(upserted.updated_at, DateTime.from_iso8601(past) |> elem(1)) == :eq + end + test "bulk creates can create relationships" do Ash.bulk_create!( [%{title: "fred", rating: %{score: 5}}, %{title: "george", rating: %{score: 0}}], From a3ad0226680925474fa55942383452a533d27452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=A4rtschi?= Date: Wed, 25 Feb 2026 12:40:16 +0100 Subject: [PATCH 2/6] refactor: read touch_update_defaults? from options instead of changeset context Read the touch_update_defaults? flag from the bulk_create options map rather than from changeset context. Depends on ash-project/ash#2590 which adds touch_update_defaults? to the options passed to data layers. Only merge once that PR is merged and released. --- lib/data_layer.ex | 5 ++--- test/bulk_create_test.exs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/data_layer.ex b/lib/data_layer.ex index cd43d30..d8ff80f 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -811,9 +811,8 @@ defmodule AshSqlite.DataLayer do # Include fields with update_defaults (e.g. update_timestamp) # even if they aren't in the changeset attributes or upsert_fields. # These fields should always be refreshed when an upsert modifies fields. - # Can be disabled via context: %{data_layer: %{touch_update_defaults?: false}} - touch_update_defaults? = - Enum.at(changesets, 0).context[:data_layer][:touch_update_defaults?] != false + # Can be disabled via touch_update_defaults?: false option + touch_update_defaults? = Map.get(options, :touch_update_defaults?, true) if touch_update_defaults? do update_default_fields = diff --git a/test/bulk_create_test.exs b/test/bulk_create_test.exs index 2378df1..a036f12 100644 --- a/test/bulk_create_test.exs +++ b/test/bulk_create_test.exs @@ -174,7 +174,7 @@ defmodule AshSqlite.BulkCreateTest do upsert?: true, upsert_identity: :uniq_one_and_two, upsert_fields: [:price], - context: %{data_layer: %{touch_update_defaults?: false}}, + touch_update_defaults?: false, return_stream?: true, return_errors?: true, return_records?: true From 09243c382d71a1220becc5fc06717eafc27619f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=A4rtschi?= Date: Wed, 25 Feb 2026 15:54:04 +0100 Subject: [PATCH 3/6] feat: support touch_update_defaults? option in non-bulk upsert path --- lib/data_layer.ex | 12 ++++++++-- test/upsert_test.exs | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/lib/data_layer.ex b/lib/data_layer.ex index d8ff80f..bfa12c2 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -1282,15 +1282,22 @@ defmodule AshSqlite.DataLayer do end @impl true - def upsert(resource, changeset, keys \\ nil) do + def upsert(resource, changeset, keys \\ nil, _identity \\ nil, opts \\ []) do keys = keys || Ash.Resource.Info.primary_key(keys) + touch_update_defaults? = Keyword.get(opts, :touch_update_defaults?, true) update_defaults = update_defaults(resource) explicitly_changing_attributes = changeset.attributes |> Map.keys() - |> Enum.concat(Keyword.keys(update_defaults)) + |> then(fn attrs -> + if touch_update_defaults? do + Enum.concat(attrs, Keyword.keys(update_defaults)) + else + attrs + end + end) |> Kernel.--(Map.get(changeset, :defaults, [])) |> Kernel.--(keys) @@ -1303,6 +1310,7 @@ defmodule AshSqlite.DataLayer do tenant: changeset.tenant, upsert_keys: keys, upsert_fields: upsert_fields, + touch_update_defaults?: touch_update_defaults?, return_records?: true }) do {:ok, []} -> diff --git a/test/upsert_test.exs b/test/upsert_test.exs index e7dc0cc..ec0f1e6 100644 --- a/test/upsert_test.exs +++ b/test/upsert_test.exs @@ -61,4 +61,58 @@ defmodule AshSqlite.Test.UpsertTest do assert updated_post.id == id assert Decimal.equal?(updated_post.decimal, Decimal.new(5)) end + + test "upsert with touch_update_defaults? false does not update updated_at" do + id = Ash.UUID.generate() + past = DateTime.to_iso8601(DateTime.add(DateTime.utc_now(), -60, :second)) + + Post + |> Ash.Changeset.for_create(:create, %{ + id: id, + title: "title" + }) + |> Ash.create!() + + AshSqlite.TestRepo.query!("UPDATE posts SET updated_at = ? WHERE id = ?", [past, id]) + + assert [%{updated_at: backdated}] = Ash.read!(Post) + assert DateTime.compare(backdated, DateTime.from_iso8601(past) |> elem(1)) == :eq + + upserted = + Post + |> Ash.Changeset.for_create(:create, %{ + id: id, + title: "title2" + }) + |> Ash.create!(upsert?: true, touch_update_defaults?: false) + + assert DateTime.compare(upserted.updated_at, DateTime.from_iso8601(past) |> elem(1)) == :eq + end + + test "upsert with empty upsert_fields does not update updated_at" do + id = Ash.UUID.generate() + past = DateTime.to_iso8601(DateTime.add(DateTime.utc_now(), -60, :second)) + + Post + |> Ash.Changeset.for_create(:create, %{ + id: id, + title: "title" + }) + |> Ash.create!() + + AshSqlite.TestRepo.query!("UPDATE posts SET updated_at = ? WHERE id = ?", [past, id]) + + assert [%{updated_at: backdated}] = Ash.read!(Post) + assert DateTime.compare(backdated, DateTime.from_iso8601(past) |> elem(1)) == :eq + + upserted = + Post + |> Ash.Changeset.for_create(:create, %{ + id: id, + title: "title2" + }) + |> Ash.create!(upsert?: true, upsert_fields: []) + + assert DateTime.compare(upserted.updated_at, DateTime.from_iso8601(past) |> elem(1)) == :eq + end end From 55faed6fe0e696d3cfd142b3402d69bd8aae4593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=A4rtschi?= Date: Thu, 26 Feb 2026 08:46:03 +0100 Subject: [PATCH 4/6] fix: restore backwards compatibility for touch_update_defaults? changeset context --- lib/data_layer.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/data_layer.ex b/lib/data_layer.ex index bfa12c2..6df46f7 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -812,7 +812,10 @@ defmodule AshSqlite.DataLayer do # even if they aren't in the changeset attributes or upsert_fields. # These fields should always be refreshed when an upsert modifies fields. # Can be disabled via touch_update_defaults?: false option - touch_update_defaults? = Map.get(options, :touch_update_defaults?, true) + # or via context: %{data_layer: %{touch_update_defaults?: false}} + touch_update_defaults? = + Map.get(options, :touch_update_defaults?, true) && + Enum.at(changesets, 0).context[:data_layer][:touch_update_defaults?] != false if touch_update_defaults? do update_default_fields = From fd2b26e0ba23d11ac1214807e6afdbadb5534b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=A4rtschi?= Date: Mon, 2 Mar 2026 08:51:19 +0100 Subject: [PATCH 5/6] refactor: read touch_update_defaults? from changeset context instead of upsert opts to remove upsert/5 arity --- lib/data_layer.ex | 11 +++++++---- mix.exs | 2 +- mix.lock | 10 +++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 6df46f7..bf3b9f3 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -811,10 +811,11 @@ defmodule AshSqlite.DataLayer do # Include fields with update_defaults (e.g. update_timestamp) # even if they aren't in the changeset attributes or upsert_fields. # These fields should always be refreshed when an upsert modifies fields. - # Can be disabled via touch_update_defaults?: false option - # or via context: %{data_layer: %{touch_update_defaults?: false}} + # Can be disabled via touch_update_defaults?: false in the changeset + # context (either in [:private] or [:data_layer]) or via options map touch_update_defaults? = Map.get(options, :touch_update_defaults?, true) && + Enum.at(changesets, 0).context[:private][:touch_update_defaults?] != false && Enum.at(changesets, 0).context[:data_layer][:touch_update_defaults?] != false if touch_update_defaults? do @@ -1285,10 +1286,12 @@ defmodule AshSqlite.DataLayer do end @impl true - def upsert(resource, changeset, keys \\ nil, _identity \\ nil, opts \\ []) do + def upsert(resource, changeset, keys \\ nil, _identity \\ nil) do keys = keys || Ash.Resource.Info.primary_key(keys) - touch_update_defaults? = Keyword.get(opts, :touch_update_defaults?, true) + touch_update_defaults? = + changeset.context[:private][:touch_update_defaults?] != false + update_defaults = update_defaults(resource) explicitly_changing_attributes = diff --git a/mix.exs b/mix.exs index f1b3aa0..b06d9a7 100644 --- a/mix.exs +++ b/mix.exs @@ -146,7 +146,7 @@ defmodule AshSqlite.MixProject do {:ecto_sqlite3, "~> 0.12"}, {:ecto, "~> 3.13"}, {:jason, "~> 1.0"}, - {:ash, ash_version("~> 3.9")}, + {:ash, ash_version("~> 3.19")}, {:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.20")}, {:igniter, "~> 0.6 and >= 0.6.14", optional: true}, {:simple_sat, ">= 0.0.0", only: [:dev, :test]}, diff --git a/mix.lock b/mix.lock index c646988..a65444c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "3.16.0", "6389927b322ca7fa7990a75730133db44fcff6368adb63f41cf9eec7a5d38862", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1ea69d932ea2ae6cc2971b92576d8ac2721218a8f2f3599e0e25305edb56949b"}, + "ash": {:hex, :ash, "3.19.1", "b5e933547d948e44d27adaed5737195488292fc2066e7fe60dd3ac83a0c4e54f", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "697ac3e4fc6080cb03b1e4ee9088cb8a313a5299686ba1aa91efc86ec4028b6e"}, "ash_sql": {:hex, :ash_sql, "0.4.5", "30030675ce995570fcedccd3c0671d85beff03cc0c480e7da5002842dccf0277", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "131e06e13ebcf06fc8d050267a5b29f6cc8ef6a781712e61a456f17726a64ea5"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, @@ -43,14 +43,14 @@ "rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"}, "simple_sat": {:hex, :simple_sat, "0.1.4", "39baf72cdca14f93c0b6ce2b6418b72bbb67da98fa9ca4384e2f79bbc299899d", [:mix], [], "hexpm", "3569b68e346a5fd7154b8d14173ff8bcc829f2eb7b088c30c3f42a383443930b"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, - "sourceror": {:hex, :sourceror, "1.10.1", "325753ed460fe9fa34ebb4deda76d57b2e1507dcd78a5eb9e1c41bfb78b7cdfe", [:mix], [], "hexpm", "288f3079d93865cd1e3e20df5b884ef2cb440e0e03e8ae393624ee8a770ba588"}, - "spark": {:hex, :spark, "2.4.0", "f93d3ae6b5f3004e956d52f359fa40670366685447631bc7c058f4fbf250ebf3", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "4e5185f5737cd987bb9ef377ae3462a55b8312f5007c2bc4ad6e850d14ac0111"}, - "spitfire": {:hex, :spitfire, "0.3.5", "6e5a775256fec72d8a4f4bb19eb8b91ad64b6f64dc51b1c8b9689e78b16c6e8b", [:mix], [], "hexpm", "7ffcb11de2f6544868148f8fc996482040eb329a990e1624795e53598934a680"}, + "sourceror": {:hex, :sourceror, "1.11.0", "df2cdaffdc323e804009ff50b50bb31e6f2d6e116d936ccf22981f592594d624", [:mix], [], "hexpm", "6e26f572bdfc21d7ad397f596b4cfbbf31d7112126fe3e902c120947073231a8"}, + "spark": {:hex, :spark, "2.4.1", "d6807291e74b51f6efb6dd4e0d58216ae3729d45c35c456e049556e7e946e364", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "8b065733de9840cac584515f82182ac5ba66a973a47bc5036348dc740662b46b"}, + "spitfire": {:hex, :spitfire, "0.3.7", "d6051f94f554d33d038ab3c1d7e017293ae30429cc6b267b08cb6ad69e35e9a3", [:mix], [], "hexpm", "798ff97db02477b05fa3db8e2810cebda6ed5d90c6de6b21aa65abd577599744"}, "splode": {:hex, :splode, "0.3.0", "ff8effecc509a51245df2f864ec78d849248647c37a75886033e3b1a53ca9470", [:mix], [], "hexpm", "73cfd0892d7316d6f2c93e6e8784bd6e137b2aa38443de52fd0a25171d106d81"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, - "yaml_elixir": {:hex, :yaml_elixir, "2.12.0", "30343ff5018637a64b1b7de1ed2a3ca03bc641410c1f311a4dbdc1ffbbf449c7", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "ca6bacae7bac917a7155dca0ab6149088aa7bc800c94d0fe18c5238f53b313c6"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.12.1", "d74f2d82294651b58dac849c45a82aaea639766797359baff834b64439f6b3f4", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "d9ac16563c737d55f9bfeed7627489156b91268a3a21cd55c54eb2e335207fed"}, "ymlr": {:hex, :ymlr, "5.1.4", "b924d61e1fc1ec371cde6ab3ccd9311110b1e052fc5c2460fb322e8380e7712a", [:mix], [], "hexpm", "75f16cf0709fbd911b30311a0359a7aa4b5476346c01882addefd5f2b1cfaa51"}, } From a8f3f34040f7c716e697ed06ef28bc4554504eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=A4rtschi?= Date: Mon, 2 Mar 2026 08:57:53 +0100 Subject: [PATCH 6/6] refactor(data_layer): revert identity changes to upser/4 --- lib/data_layer.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data_layer.ex b/lib/data_layer.ex index bf3b9f3..99174d2 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -1286,7 +1286,7 @@ defmodule AshSqlite.DataLayer do end @impl true - def upsert(resource, changeset, keys \\ nil, _identity \\ nil) do + def upsert(resource, changeset, keys \\ nil) do keys = keys || Ash.Resource.Info.primary_key(keys) touch_update_defaults? =