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

- `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: "<table>_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
Expand Down
6 changes: 5 additions & 1 deletion lib/ecto_trail/ecto_trail.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
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
29 changes: 29 additions & 0 deletions test/unit/ecto_trail_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down