A temporal-table library stores database row states over system time. AuditLog stores the actor and intent behind a business action. Keep those facts separate and link an event to one exact row version.
AuditLog has no compile-time dependency on a temporal-table package. A
compatible Ecto schema has a sys_period field whose loaded value matches
%{from: %DateTime{}}. AuditLog.Subject.from_schema!/2 copies that from
boundary into the event's subject_version_at.
Insert
Build the subject from the struct returned by the repository write. The
temporal integration must load sys_period after writes so the returned struct
contains the new version's lower boundary.
Repo.transaction(fn ->
shareholder =
%Shareholder{}
|> Shareholder.changeset(attrs)
|> Repo.insert!()
subject =
AuditLog.Subject.from_schema!(shareholder,
type: "shareholder"
)
AuditLog.record!(
Repo,
context,
"shareholder.created",
subject
)
shareholder
end)Update
The updated struct contains the post-change version boundary:
Repo.transaction(fn ->
shareholder =
shareholder
|> Shareholder.changeset(attrs)
|> Repo.update!()
subject =
AuditLog.Subject.from_schema!(shareholder,
type: "shareholder"
)
AuditLog.record!(
Repo,
context,
"shareholder.address_corrected",
subject,
reason: reason
)
shareholder
end)Delete
Build the subject from the deleted struct. With a compatible temporal integration, it retains the outgoing version's lower boundary, which identifies the row in history:
Repo.transaction(fn ->
shareholder = Repo.delete!(shareholder)
subject =
AuditLog.Subject.from_schema!(shareholder,
type: "shareholder"
)
AuditLog.record!(
Repo,
context,
"shareholder.deleted",
subject,
reason: reason
)
shareholder
end)Load the exact version
Subject IDs are stored as text. Cast the ID through the application's schema before querying the temporal history:
shareholder_id =
AuditLog.Subject.cast_id!(
Shareholder,
event.subject_id
)
# Pass shareholder_id and event.subject_version_at to the temporal store's
# exact-version query.The cast matters for integer and custom Ecto primary-key types. UUID and string schemas often accept the stored value directly, but the helper keeps the query correct if the key type changes.
Why occurred_at is not the version
subject_version_at is the lower boundary of one exact temporal version.
occurred_at is the audit event transaction's start time. Timestamp proximity
does not make them interchangeable:
- An update boundary resolves to the post-change version.
- A deleted row's outgoing boundary resolves to its history version.
transaction_timestamp()records transaction start, not commit.
Half-open temporal periods mean that looking up a deleted row at the audit
event's occurrence time can return no row or the wrong row. When the event has
a version boundary, use event.subject_version_at.
Record one event per affected subject when a business action changes several records. Give the events one correlation ID. This version deliberately has no event-subject join table.