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

- `changelog_changeset` now includes `:id` and declares `unique_constraint(:id, name: "#{table}_pkey")` so Ecto.ConstraintError on audit PK during `*_and_log` (inside transaction) becomes a changeset error instead of raising. Closes OPS-4617.

## [1.0.3] - 2026-05-28

### Fixed
Expand Down
8 changes: 7 additions & 1 deletion lib/ecto_trail/ecto_trail.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ defmodule EctoTrail do
# Cache frequently accessed config to avoid repeated lookups
@redacted_fields_config Application.compile_env(:ecto_trail, :redacted_fields, nil)
@default_max_params 65_000
@changelog_fields [:actor_id, :resource, :resource_id, :changeset, :change_type]
@changelog_fields [:id, :actor_id, :resource, :resource_id, :changeset, :change_type]
@not_loaded_pattern "Ecto.Association.NotLoaded"

defmacro __using__(_) do
Expand Down Expand Up @@ -573,5 +573,11 @@ defmodule EctoTrail do

defp changelog_changeset(attrs) do
Changeset.cast(%Changelog{}, attrs, @changelog_fields)
|> Changeset.unique_constraint(:id, name: pkey_constraint_name())
end

defp pkey_constraint_name do
table = Application.get_env(:ecto_trail, :table_name, "audit_log")
"#{table}_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
40 changes: 40 additions & 0 deletions test/unit/ecto_trail_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,44 @@ defmodule EctoTrailTest do
)
end
end

describe "handles pkey constraint violations on audit_log gracefully (no ConstraintError)" do
test "insert_and_log succeeds even if audit PK would collide (e.g. sequence skew)" do
# Prime the audit_log with an explicit high id via insert_all (bypasses our changeset)
colliding_id = 1_000_007

# Insert a "dummy" changelog row claiming that id
{1, _} =
TestRepo.insert_all(
Changelog,
[
%{
id: colliding_id,
actor_id: "seq-collision",
resource: "resources",
resource_id: "0",
changeset: %{},
change_type: :insert,
inserted_at: DateTime.utc_now() |> DateTime.truncate(:second)
}
]
)

# Force the sequence so the next generated id (via default) will collide with the one we inserted.
# setval(seq, n, true) sets last_value to n; next nextval() returns n+1.
# So set to colliding_id-1 to make the next default-generated id == colliding_id.
Ecto.Adapters.SQL.query!(TestRepo, "SELECT setval('audit_log_id_seq', $1, true)", [colliding_id - 1])

# Now perform an insert_and_log; internally it will let PG generate the id for the audit row,
# which will collide. Without unique_constraint handling on the changeset this raises Ecto.ConstraintError.
assert {:ok, %Resource{name: "collide-test"}} =
TestRepo.insert_and_log(%Resource{name: "collide-test"}, "pkey-actor")

# The main resource was inserted
assert TestRepo.exists?(from(r in Resource, where: r.name == "collide-test"))

# The log step did not succeed in inserting an audit row (constraint violation was turned into error)
refute TestRepo.exists?(from(c in Changelog, where: c.actor_id == "pkey-actor"))
end
end
end