diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bdbb9a..cd4a302 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 + +- Register `unique_constraint` on the audit log primary key (using the configured table name) inside `changelog_changeset/1`. This prevents `Ecto.ConstraintError` on `audit_log_pkey` (or custom_table_pkey) when `*_and_log/*` or `log/*` perform their internal insert while a transaction is open (including from `update_and_log` inside `Ecto.Multi` or app tx). The violation is now turned into a changeset error (existing call sites log and continue). Closes OPS-4615. + ## [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..f1fd69b 100644 --- a/lib/ecto_trail/ecto_trail.ex +++ b/lib/ecto_trail/ecto_trail.ex @@ -55,6 +55,7 @@ defmodule EctoTrail do @default_max_params 65_000 @changelog_fields [:actor_id, :resource, :resource_id, :changeset, :change_type] @not_loaded_pattern "Ecto.Association.NotLoaded" + @table_name Application.compile_env(:ecto_trail, :table_name, "audit_log") defmacro __using__(_) do quote do @@ -572,6 +573,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: "#{@table_name}_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..2377935 100644 --- a/test/unit/ecto_trail_test.exs +++ b/test/unit/ecto_trail_test.exs @@ -329,4 +329,35 @@ defmodule EctoTrailTest do ) end end + + describe "audit_log pkey unique_constraint handling (prevents Ecto.ConstraintError)" do + setup do + {:ok, schema} = TestRepo.insert(%Resource{name: "pkey-collision-subject"}) + {:ok, %{schema: schema}} + end + + test "update_and_log succeeds (does not raise) when internal audit log insert hits pkey unique violation", + %{ + schema: schema + } do + # Seed one committed audit row so we have a real id, then force the sequence to hand out the same id on next insert. + {:ok, _seed} = TestRepo.insert_and_log(%Resource{name: "pkey-seed"}, "pkey-seed-actor") + + %{rows: [[used_id]]} = + TestRepo.query!("SELECT id FROM audit_log ORDER BY id DESC LIMIT 1") + + # Make the next nextval return the id we just observed → the subsequent insert inside update_and_log will collide on audit_log_pkey. + TestRepo.query!("SELECT setval('audit_log_id_seq', $1, true)", [used_id]) + + # This used to raise Ecto.ConstraintError because changelog_changeset/1 never declared unique_constraint for the pkey. + # With the fix it converts the violation to a changeset error; the existing error path in log_changes swallows it (logged) + # and the tx still commits the user operation. + result = + schema + |> Changeset.change(%{name: "post-pkey-collision"}) + |> TestRepo.update_and_log("pkey-test-actor") + + assert {:ok, %Resource{name: "post-pkey-collision"}} = result + end + end end