diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bdbb9a..37c1045 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
+
+- `changelog_changeset` now declares `unique_constraint(:id, name: "
_pkey")` so `Ecto.ConstraintError` on `audit_logs_pkey` (or custom table pkey) during `log_changes/5` (called from `*_and_log` inside transactions) is turned into a changeset error instead of crashing the caller. The log failure path already swallows errors and returns `{:ok, reason}`. Closes OPS-4624.
+
## [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..20a15ae 100644
--- a/lib/ecto_trail/ecto_trail.ex
+++ b/lib/ecto_trail/ecto_trail.ex
@@ -52,6 +52,8 @@ defmodule EctoTrail do
# Cache frequently accessed config to avoid repeated lookups
@redacted_fields_config Application.compile_env(:ecto_trail, :redacted_fields, nil)
+ @audit_log_table_name Application.compile_env(:ecto_trail, :table_name, "audit_log")
+ @audit_log_pkey_name "#{@audit_log_table_name}_pkey"
@default_max_params 65_000
@changelog_fields [:actor_id, :resource, :resource_id, :changeset, :change_type]
@not_loaded_pattern "Ecto.Association.NotLoaded"
@@ -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_name)
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..ec978d7 100644
--- a/test/unit/ecto_trail_test.exs
+++ b/test/unit/ecto_trail_test.exs
@@ -329,4 +329,23 @@ defmodule EctoTrailTest do
)
end
end
+
+ describe "audit log primary key constraint handling" do
+ test "does not raise Ecto.ConstraintError when audit log pkey is violated; main operation succeeds and log failure is swallowed" do
+ {:ok, _} = TestRepo.insert_and_log(%Resource{name: "seed"}, "actor-seed")
+
+ %Postgrex.Result{rows: [[max_id]]} = TestRepo.query!("SELECT max(id) FROM audit_log")
+ next_id = max_id + 1
+
+ TestRepo.query!(
+ "INSERT INTO audit_log (id, actor_id, resource, resource_id, changeset, change_type, inserted_at) VALUES ($1, 'conflict', 'resources', '999', $2, 'insert', now())",
+ [next_id, %{}]
+ )
+
+ TestRepo.query!("SELECT setval('audit_log_id_seq', $1, false)", [max_id])
+
+ assert {:ok, %Resource{name: "after"}} =
+ TestRepo.insert_and_log(%Resource{name: "after"}, "actor-after")
+ end
+ end
end