Runtime resolution for the AshArcadic.Replicant sink — the tenant/classification
layer over compiled resource metadata. Pure functions (no DB access):
build_index/1— reflect the configured domains into a{source_schema, source_table} => resourceindex, failing closed on a duplicate source key (an ambiguous route: two mirrors claiming one Postgres table).lookup/3— resolve a source{schema, table}to its mirror resource, applying the SAMEnil-schema →"public"default the index keys use.resolve_tenant/2/resolve_tenant!/3— per-row tenant from the resource's replicanttenant_attribute, failing closed with:tenant_requiredon any value Ash would treat as unscoped (nil,false, or blank).resolve_tenant!/3is the raising variant every apply path shares.writable_target/2/attrs_for_upsert/2— map source string columns to their writable target attributes, dropping replicant-skipcolumns and undeclared columns, and HALTING value-free (F5) when a non-skipped column maps to asensitivetarget (never emit plaintext into a classified column).primary_key/1/pk_values/2.
Diverges from the sibling ash_replicant's AshReplicant.Resolver (the shape
template) deliberately:
- Source records are string-keyed (Postgres column names as binaries), so
every lookup is
Map.get(record, to_string(attr)). - No
tenant_mfa— the replicant extension exposes onlytenant_attribute. - No AshCloak cloak-routing. AshArcadic has no key material, so a plaintext value
bound for a
sensitivetarget fails closed (F5) rather than being re-encrypted.
Summary
Functions
Map one string-keyed source record to {inputs, upsert_fields} for the mirror
upsert. Drops replicant-skip columns and undeclared columns; unchanged-TOAST
columns are absent from record (surfaced separately on %Replicant.Change{}'s
unchanged list, never in record) so iterating the map excludes them naturally.
HALTS value-free (F5) on a non-skipped column mapped to a sensitive target.
Look up the mirror resource for a source {schema, table} in an index built by
build_index/1, applying the SAME nil-schema → "public" default the index
keys use (so the convention lives in one place next to the builder). Returns the
resource, or nil for an unmapped table.
The fail-closed bang variant of resolve_tenant/2: returns the per-row tenant, or
raises a value-free AshArcadic.Replicant.Error (reason: :tenant_required) when
the row carries no usable tenant. op labels the failing sink operation
(:upsert / :destroy / ...) in the structural error, which NEVER carries the
record or the tenant value. The single tenant-resolution entry point shared by
every apply path, so :tenant_required fails identically everywhere.
Types
Functions
Map one string-keyed source record to {inputs, upsert_fields} for the mirror
upsert. Drops replicant-skip columns and undeclared columns; unchanged-TOAST
columns are absent from record (surfaced separately on %Replicant.Change{}'s
unchanged list, never in record) so iterating the map excludes them naturally.
HALTS value-free (F5) on a non-skipped column mapped to a sensitive target.
@spec build_index([module()]) :: {:ok, %{required(source_key()) => module()}} | {:error, {:duplicate_source, source_key()}} | {:error, {:missing_source_table, module()}}
@spec lookup(%{required(source_key()) => module()}, String.t() | nil, String.t()) :: module() | nil
Look up the mirror resource for a source {schema, table} in an index built by
build_index/1, applying the SAME nil-schema → "public" default the index
keys use (so the convention lives in one place next to the builder). Returns the
resource, or nil for an unmapped table.
The fail-closed bang variant of resolve_tenant/2: returns the per-row tenant, or
raises a value-free AshArcadic.Replicant.Error (reason: :tenant_required) when
the row carries no usable tenant. op labels the failing sink operation
(:upsert / :destroy / ...) in the structural error, which NEVER carries the
record or the tenant value. The single tenant-resolution entry point shared by
every apply path, so :tenant_required fails identically everywhere.