From 3dd40995ceb2f937a46b42be7491188752f6431d Mon Sep 17 00:00:00 2001 From: "palantir-valiot[bot]" <279094399+palantir-valiot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:41:36 +0000 Subject: [PATCH] fix: declare unique_constraint on audit_log pkey in changelog_changeset Prevents Ecto.ConstraintError on audit_logs_pkey (or configured table pkey) when concurrent/default-task-queue upserts call upsert_and_log (and siblings) from inside a transaction. - changelog_changeset now chains unique_constraint(:id, name: "_pkey") using the same :table_name compile_env as the schema. - Added TDD regression test that forces a pkey collision via sequence rewind and asserts upsert_and_log still succeeds (audit logging is best-effort). - Bump to 1.0.4, update CHANGELOG. Closes OPS-4571 --- CHANGELOG.md | 6 ++++++ lib/ecto_trail/ecto_trail.ex | 6 +++++- mix.exs | 2 +- test/unit/ecto_trail_test.exs | 29 +++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bdbb9a..476d7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.4] - 2026-06-13 + +### Fixed + +- `upsert_and_log` (and other `*_and_log`) no longer raise `Ecto.ConstraintError` on `audit_log_pkey` under concurrent upserts. `changelog_changeset` now declares `unique_constraint(:id, name: "
_pkey")` so Ecto turns the violation into a changeset error that the logging paths swallow. Closes OPS-4571. + ## [1.0.3] - 2026-05-28 ### Fixed diff --git a/lib/ecto_trail/ecto_trail.ex b/lib/ecto_trail/ecto_trail.ex index 74d7075..ecdd2cf 100644 --- a/lib/ecto_trail/ecto_trail.ex +++ b/lib/ecto_trail/ecto_trail.ex @@ -55,6 +55,8 @@ defmodule EctoTrail do @default_max_params 65_000 @changelog_fields [:actor_id, :resource, :resource_id, :changeset, :change_type] @not_loaded_pattern "Ecto.Association.NotLoaded" + @audit_log_table Application.compile_env(:ecto_trail, :table_name, "audit_log") + @audit_log_pkey "#{@audit_log_table}_pkey" defmacro __using__(_) do quote do @@ -572,6 +574,8 @@ defmodule EctoTrail do defp map_custom_ecto_type(value), do: value defp changelog_changeset(attrs) do - Changeset.cast(%Changelog{}, attrs, @changelog_fields) + %Changelog{} + |> Changeset.cast(attrs, @changelog_fields) + |> Changeset.unique_constraint(:id, name: @audit_log_pkey) end end diff --git a/mix.exs b/mix.exs index c895a54..e08c811 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule EctoTrail.Mixfile do use Mix.Project - @version "1.0.3" + @version "1.0.4" def project do [ diff --git a/test/unit/ecto_trail_test.exs b/test/unit/ecto_trail_test.exs index 1a44768..5c92dd8 100644 --- a/test/unit/ecto_trail_test.exs +++ b/test/unit/ecto_trail_test.exs @@ -225,6 +225,35 @@ defmodule EctoTrailTest do change_type: :upsert } = TestRepo.one(Changelog) end + + test "upsert_and_log does not raise Ecto.ConstraintError on audit_log pkey collision (concurrent upsert case)" do + {:ok, schema} = TestRepo.insert(%Resource{name: "uq-base"}) + + # Seed one audit row so the pkey sequence exists and has a value, then rewind it + # so the *next* autogenerated PK will collide (serial id in test DB). + {:ok, _seed} = + TestRepo.insert(%Changelog{ + actor_id: "seed", + resource: "resources", + resource_id: "999", + changeset: %{}, + change_type: :insert + }) + + max_id = TestRepo.one(from(c in Changelog, select: max(c.id))) + seq_name = "audit_log_id_seq" + Ecto.Adapters.SQL.query!(TestRepo, "SELECT setval($1, $2, true)", [seq_name, max_id - 1]) + + # Before the fix this raises Ecto.ConstraintError because log_changes did a bare insert + # without unique_constraint(:id, name: "audit_log_pkey"). After fix the violation becomes + # a normal {:error, changeset} which the *_and_log paths swallow (audit is best-effort). + result = + schema + |> Changeset.change(%{name: "uq-after"}) + |> TestRepo.upsert_and_log("concurrency-actor") + + assert {:ok, %Resource{name: "uq-after"}} = result + end end describe "use inside Ecto.Multi (prevents nested multi tx RuntimeError)" do