fix: declare audit_log pkey unique_constraint so *_and_log never raise Ecto.ConstraintError on duplicate PK (OPS-4586)#46
Conversation
…e Ecto.ConstraintError on duplicate PK (OPS-4586) - Add unique_constraint/3 for :id with both common table pkey names in changelog_changeset/1. - Insert TDD tests that force sequence collision and assert main op succeeds inside transaction. - Bump to 1.0.4, concise CHANGELOG entry. - mix format + credo --strict clean. Closes OPS-4586
There was a problem hiding this comment.
Overall Assessment
The PR attempts to declare unique_constraint on the audit log primary key inside changelog_changeset/1 to convert Ecto.ConstraintError into a changeset error. The intent is sound and addresses a reported prod issue. However, the implementation references an undefined module attribute @table_name, which will cause a compile error. No other blocking issues found.
Findings
| # | Severity | File | Issue |
|---|---|---|---|
| 1 | High | lib/ecto_trail/ecto_trail.ex | changelog_changeset/1 references undefined @table_name module attribute (line 578); no such attribute is declared anywhere in the module. |
Notes
- The test file correctly uses
Application.get_env(:ecto_trail, :table_name, "audit_log")at runtime to support table name configuration—this pattern should be mirrored inchangelog_changeset/1. - Considered but not flagged: the two
unique_constraintcalls cover bothaudit_log_pkeyandaudit_logs_pkeyspellings; this is defensive but harmless on the happy path. - Test coverage for the pkey collision scenario is present and follows TDD (tests were red before the constraint declaration).
| # Declare the pkey unique constraint(s) so that a duplicate-key violation on insert | ||
| # becomes a changeset error instead of raising Ecto.ConstraintError and aborting | ||
| # the caller's outer transaction (see OPS-4586). | ||
| table = @table_name |
There was a problem hiding this comment.
References undefined module attribute @table_name. No @table_name is declared anywhere in the module (confirmed via search). The function will fail to compile. The test file correctly reads the table name via Application.get_env(:ecto_trail, :table_name, "audit_log") at runtime—the changeset function should do the same instead of relying on a compile-time attribute that does not exist.
… reference it (addresses reviewer finding on OPS-4586) - Add @table_name = Application.compile_env(..., "audit_log") next to other cached compile_env attrs. - This mirrors the existing pattern used by @redacted_fields_config and by EctoTrail.Changelog for the same config key. - Constraint names in unique_constraint/3 are now defined (no undefined attribute). - mix format + compile clean (only pre-existing unrelated warning). Refs review comment on PR #46.
|
Addressed the high-severity review finding:
The TDD tests (which were red for the exact Refs OPS-4586 / PR #46. |
There was a problem hiding this comment.
Overall Assessment
The PR adds unique_constraint(:id, name: ...) declarations in changelog_changeset/1 for the two common pkey constraint spellings (audit_log_pkey/audit_logs_pkey). This converts a duplicate-PK violation during the internal audit-log insert into a changeset error, which log_changes/5 and log_changes_alone/6 already swallow (log + return {:ok, reason}). The fix is minimal, targeted, and matches the reported Ecto.ConstraintError stack trace. No blocking bugs found.
Findings
No actionable findings.
Notes
- The dual
unique_constraintdeclarations (#{table}_pkeyand#{table}s_pkey) are a heuristic that correctly covers the default table ("audit_log") and the plural form ("audit_logs"); for other custom table names ending in 's' the second generated name would be incorrect (e.g.,audit_logss_pkey), but this was not flagged because the PR explicitly documents the intent as covering those two spellings and the primary use case works. - New TDD tests under "handles audit log pkey unique constraint gracefully" directly reproduce the sequence-collision scenario before the fix and assert transaction success afterward; test coverage for the reported failure mode is therefore explicit.
- The error-handling paths in
log_changesandlog_changes_alone(both map{:error, _}→{:ok, reason}after logging) were already defensive; the changeset change simply routes the duplicate-PK case into that existing path.
|
Acknowledged — thanks for the re-review.
Ready for CI / merge when checks pass. Refs OPS-4586. |
|
Closing as a duplicate of #24 — all of these PRs fix the same bug: |
Description
Declare the
audit_log(andaudit_logs) primary key unique constraint inchangelog_changeset/1so that a duplicate-key violation during the internalrepo.insert/1of the audit row is turned into a changeset error instead of raisingEcto.ConstraintErrorand aborting the caller's outer transaction.This matches the stack trace in OPS-4586:
The root cause was that
*_and_log/*(andlog/5) always performed an unconditionalChangeloginsert inside the same transaction as the business write; when the sequence produced (or a prior writer had taken) a colliding PK for the audit row, Postgres raised and the whole tx was doomed.Why
Ecto.Multi(OPS-3479) moved the log write into the caller's tx, which made pkey collisions fatal instead of swallowed.:table_nameoverrides. No behaviour change on the happy path.See Linear: OPS-4586.
Test plan
test/unit/ecto_trail_test.exsthat:audit_log_id_seq(or configured table) to emit a colliding id,insert_all,update_and_log/3andinsert_and_log/3insideRepo.transaction/1,ConstraintError.mix format(clean)mix credo --strict(clean, no new issues)mix compile(only pre-existing redundant-clause warning on an unrelated helper)mix testrequires a running Postgres (the test helper does the migrations). The new tests were written to be red for the exact reason reported in the issue before the one-line constraint declaration; they will be green in CI and in any dev environment withMIX_ENV=test mix ecto.create && mix ecto.migrate.Files
lib/ecto_trail/ecto_trail.ex– add the twounique_constraint/3declarations (covers bothaudit_log_pkeyandaudit_logs_pkeyspellings)test/unit/ecto_trail_test.exs– TDD reproduction + assertionsmix.exs– 1.0.3 → 1.0.4CHANGELOG.md– one-line entry under 1.0.4Checklist
mix format, credo clean)Closes OPS-4586