Ecto query implementations for the Threadline public API.
All functions require an explicit :repo option and return plain lists of
Ecto structs. Database errors propagate as exceptions, matching normal Ecto
repository query behavior.
Timeline filters
timeline/2, timeline_query/1, and Threadline.Export accept the same
filter keyword list. Only these keys are allowed: :repo, :table_schema,
:table, :actor_ref, :from, :to, :correlation_id.
Unknown keys raise ArgumentError (breaking vs
pre-1.0 callers that relied on silent ignores — see CHANGELOG when upgrading).
When :correlation_id is set to a non-empty string (after trimming), results are
limited to changes whose transaction is linked to an audit_actions row with that
correlation_id (strict inner-join semantics; see CHANGELOG). Omit the key to leave
correlation out of the filter.
Use timeline_repo!/2 to resolve :repo from filters and opts with the same
messages as export entrypoints.
See also
Threadline.Export— CSV / JSON export using the same filter vocabulary.audit_changes_for_transaction/2— all changes for oneaudit_transactions.id(transaction drill-down vstimeline/2slices).
Summary
Functions
Returns a keyset page of AuditTransaction records for a given actor, ordered by
occurred_at descending, then id descending.
Returns the latest stored row snapshot for a schema record at or before timestamp.
Returns every %AuditChange{} for a single capture transaction.
Returns one AuditTransaction by id or nil when the row does not exist.
Query returning one row per matching change with change + transaction columns
for export (Threadline.Export).
Returns AuditChange records for a given schema record, ordered by
captured_at descending.
Returns AuditChange records for one schema row using timeline ordering.
Returns one keyset page of row history for a single schema row.
Returns AuditChange records across tables, filtered by the given options,
ordered by captured_at descending, then id descending.
Returns one keyset page of AuditChange records in timeline order.
Builds the shared AuditChange query used by timeline/2 and export.
Resolves Ecto.Repo for timeline/2, export, and related APIs.
Validates row-history helper filters.
Validates that filters contains only timeline filter keys.
Functions
Returns a keyset page of AuditTransaction records for a given actor, ordered by
occurred_at descending, then id descending.
For an anonymous actor, returns every transaction whose actor identity is absent. Anonymous transactions intentionally have no finer-grained actor distinction, so they are equivalent for this query.
Options
:repo— requiredEcto.Repomodule:limit— integer, maximum number of records to return (default 50):after— cursor to fetch older records:before— cursor to fetch newer records:from— inclusive lower bound onoccurred_at:to— inclusive upper bound onoccurred_at
Example
Threadline.actor_history(actor_ref, repo: MyApp.Repo)
Returns the latest stored row snapshot for a schema record at or before timestamp.
Returns {:ok, map} when the latest matching snapshot is an insert/update row,
{:error, :deleted_record} when the latest snapshot is a delete, and
{:error, :before_audit_horizon} when the row has no snapshot at or before the
requested timestamp.
Examples
Threadline.as_of(MyApp.User, 42, ~U[2026-01-01 00:00:00Z], repo: MyApp.Repo)
Threadline.as_of(MyApp.LineItem, [tenant_id: 1, id: 5], ts, repo: MyApp.Repo)id accepts the same scalar, map, and keyword-list shapes as history/3,
keyed by schema field names (or a primary_key: override's declared
columns), and raises the same ArgumentError cases for a missing, extra, or
misnamed key, a nil value, a scalar for a composite table, or a loaded
struct. If :scope_query_fn is configured, it receives id unchanged as
context.params.id.
History of a dropped or renamed table, or a retyped key column, keeps
working via the same Ecto-type fallback as history/3 — including the
char(n) blank-padding caveat.
@spec audit_changes_for_transaction( term(), keyword() ) :: [Threadline.Capture.AuditChange.t()]
Returns every %AuditChange{} for a single capture transaction.
transaction_id is audit_transactions.id — the same column as
AuditChange.transaction_id (:binary_id). Accepts UUID strings or 16-byte
binaries per Ecto.UUID.cast/1.
Ordering
Same total order as timeline/2: captured_at descending, then id descending
(via timeline_order/1). Random binary_id values are not a monotonic
sequence; ordering is only defined on (captured_at, id).
Empty results
Returns [] when the UUID is well-formed but no audit_changes rows match
(whether the parent transaction row exists or not).
Options
:repo— requiredEcto.Repomodule (Keyword.fetch!/2if missing).:preload— optional association list forrepo.preload/3when non-empty (intended:[:transaction]).
Errors
Raises ArgumentError with message containing invalid audit transaction id
when transaction_id fails UUID cast (before hitting Postgrex).
@spec audit_transaction( term(), keyword() ) :: Threadline.Capture.AuditTransaction.t() | nil
Returns one AuditTransaction by id or nil when the row does not exist.
Raises ArgumentError when transaction_id is not a valid UUID.
@spec export_changes_query(keyword()) :: Ecto.Query.t()
Query returning one row per matching change with change + transaction columns
for export (Threadline.Export).
Validates filters, then builds the same predicate stack as timeline/2, adds an
optional LEFT JOIN to audit_actions when :correlation_id is absent (so JSON
can surface linked action metadata without changing filter semantics), and selects
export column maps.
@spec export_changes_query(keyword(), keyword()) :: Ecto.Query.t()
Returns AuditChange records for a given schema record, ordered by
captured_at descending.
Options
:repo— requiredEcto.Repomodule
Examples
Threadline.history(MyApp.User, 42, repo: MyApp.Repo)
Threadline.history(MyApp.LineItem, [tenant_id: 1, id: 5], repo: MyApp.Repo)
Threadline.history(MyApp.LineItem, %{"tenant_id" => 1, "id" => 5}, repo: MyApp.Repo)Each AuditChange loads all table columns mapped on the schema, including
changed_from when the database column is populated (no narrowing select).
id names every key field: the schema's field names, or a primary_key:
override's declared columns when one is configured for the table. A missing,
extra, or misnamed key, a nil value, a scalar for a composite table, or a
loaded struct all raise ArgumentError (via the internal row-key
normalizer).
If :scope_query_fn is configured, it receives id unchanged as
context.params.id — exactly what the caller passed, not the normalized
key list.
History for a table that has since been dropped or renamed keeps working:
each key column's comparison type falls back to the schema field's Ecto
type when the catalog no longer has a live entry. The one inexact case is a
fixed-width char(n) key, whose stored blank-padding the fallback cannot
reproduce — pass the value already padded to the original column width.
@spec row_history(module(), term(), keyword(), keyword()) :: [ Threadline.Capture.AuditChange.t() ]
Returns AuditChange records for one schema row using timeline ordering.
The helper matches the row's full stored key internally so callers do not need to construct low-level row predicates.
@spec row_history_page(module(), term(), keyword(), keyword()) :: Threadline.Query.TimelinePage.t()
Returns one keyset page of row history for a single schema row.
Returns AuditChange records across tables, filtered by the given options,
ordered by captured_at descending, then id descending.
Options
:table— string or atom; filters bytable_name:table_schema— string or atom; filters by captured host table schema:actor_ref—%ActorRef{}; filters by actor via joinedaudit_transactions:from—DateTime; inclusive lower bound oncaptured_at:to—DateTime; inclusive upper bound oncaptured_at:correlation_id— binary; strict filter on linkedAuditAction.correlation_id(see moduledoc / CHANGELOG):repo— requiredEcto.Repomodule (infiltersoropts; seeThreadline.Export):storage_schema— optional Threadline storage schema override
Example
Threadline.timeline(table: "users", from: ~U[2026-01-01 00:00:00Z], repo: MyApp.Repo)See also
Threadline.Export— CSV / JSON export using the same filter vocabulary.Threadline.export_csv/2andThreadline.export_json/2— top-level delegators.
@spec timeline_page(keyword(), keyword()) :: Threadline.Query.TimelinePage.t()
Returns one keyset page of AuditChange records in timeline order.
Paging controls live in opts:
:page_size— positive integer, defaults to1000:cursor—%{captured_at: %DateTime{}, id: binary}ornil
@spec timeline_query(keyword()) :: Ecto.Query.t()
Builds the shared AuditChange query used by timeline/2 and export.
Does not call validate_timeline_filters!/1 — callers must validate first
when accepting external filter lists.
Resolves Ecto.Repo for timeline/2, export, and related APIs.
Checks opts first, then filters. Raises ArgumentError if missing or not an atom module.
@spec validate_row_history_filters!(keyword()) :: :ok
Validates row-history helper filters.
Allowed keys: :repo, :from, :to.
@spec validate_timeline_filters!(keyword()) :: :ok
Validates that filters contains only timeline filter keys.
Allowed keys: :repo, :table_schema, :table,
:actor_ref, :from, :to, :correlation_id.
Returns :ok or raises ArgumentError.