Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion lib/ecto_trail/ecto_trail.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule EctoTrail.Mixfile do
use Mix.Project

@version "1.0.3"
@version "1.0.4"

def project do
[
Expand Down
31 changes: 31 additions & 0 deletions test/unit/ecto_trail_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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