# Temporal subjects

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.

```elixir
Repo.transaction(fn ->
  shareholder =
    %Shareholder{}
    |> Shareholder.changeset(attrs)
    |> Repo.insert!()

  subject =
    AuditLog.Subject.from_schema!(shareholder,
      type: "shareholder"
    )

  AuditLog.record!(
    Repo,
    "shareholder.created",
    context,
    subject
  )

  shareholder
end)
```

## Update

The updated struct contains the post-change version boundary:

```elixir
Repo.transaction(fn ->
  shareholder =
    shareholder
    |> Shareholder.changeset(attrs)
    |> Repo.update!()

  subject =
    AuditLog.Subject.from_schema!(shareholder,
      type: "shareholder"
    )

  AuditLog.record!(
    Repo,
    "shareholder.address_corrected",
    context,
    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:

```elixir
Repo.transaction(fn ->
  shareholder = Repo.delete!(shareholder)

  subject =
    AuditLog.Subject.from_schema!(shareholder,
      type: "shareholder"
    )

  AuditLog.record!(
    Repo,
    "shareholder.deleted",
    context,
    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:

```elixir
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.
