AshReplicant.Resolver (AshReplicant v0.3.0)

Copy Markdown View Source

Runtime resolution for the AshReplicant sink — the tenant/classification layer that keeps replicant tenant-blind. Pure functions over compiled resource metadata (no DB access):

Summary

Types

The batch-invariant upsert reflection: {skip, cloak_attrs, attribute_names}.

Functions

The declared SCD2 business-key values from a string-keyed source record.

The primary destroy action name (mirrors upsert_action/1 for :create).

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.

A query over resource selecting the CURRENT open version of the business key in record, whose valid_from_lsn is strictly less than lsn (open-path close) or at most lsn (delete/terminal close, inclusive?: true). Uses dynamic ^ref/1 because the window column names are DSL-configured.

The fail-closed bang variant of resolve_tenant/2: returns the per-row tenant, or raises a value-free AshReplicant.Error (reason: :tenant_required) when the row carries no usable tenant. op labels the failing sink operation (:upsert / :destroy / ...) in the structural error. The single tenant-resolution entry point shared by every apply path (Apply, Apply.Scd2), so :tenant_required fails identically everywhere.

The upsert-capable create action name (the resource's primary create action).

The upsert identity name from the DSL (nil → primary-key upsert).

Map one source record to {inputs, upsert_fields} under a precomputed upsert_reflection/1. AshCloak-sensitive columns pass plaintext under the cloak argument while upsert_fields names encrypted_<col>.

Compute the batch-invariant upsert reflection for a resource ONCE — the skip list, the AshCloak cloak attributes, and the attribute-name set. Thread it into upsert_input/2 per row of a column-homogeneous batch (the snapshot bulk path) to avoid re-deriving these for every row. Single-record callers use attrs_for_upsert/2, which computes the reflection inline.

The {inputs, upsert_fields} for OPENING a version: the source data columns (via the existing upsert reflection) PLUS the window columns (valid_from_lsn, optional valid_from_ts, valid_to_lsn: nil, optional is_current: true). upsert_fields names every window column so a same-lsn re-open coalesces in place.

Types

source_key()

@type source_key() :: {schema :: String.t(), table :: String.t()}

upsert_reflection()

@type upsert_reflection() :: {[atom()], [atom()], MapSet.t(atom())}

The batch-invariant upsert reflection: {skip, cloak_attrs, attribute_names}.

Functions

attrs_for_upsert(resource, record)

@spec attrs_for_upsert(module(), map()) :: {map(), [atom()]}

build_index(domains)

@spec build_index([module()]) ::
  {:ok, %{required(source_key()) => module()}}
  | {:error, {:duplicate_source, source_key()}}
  | {:error, {:missing_source_table, module()}}

business_key_values(resource, record)

@spec business_key_values(module(), map()) :: map()

The declared SCD2 business-key values from a string-keyed source record.

destroy_action(resource)

@spec destroy_action(module()) :: atom()

The primary destroy action name (mirrors upsert_action/1 for :create).

lookup(index, schema, table)

@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.

open_version_query(resource, record, lsn, opts \\ [])

@spec open_version_query(module(), map(), integer(), keyword()) :: Ash.Query.t()

A query over resource selecting the CURRENT open version of the business key in record, whose valid_from_lsn is strictly less than lsn (open-path close) or at most lsn (delete/terminal close, inclusive?: true). Uses dynamic ^ref/1 because the window column names are DSL-configured.

pk_values(resource, record)

@spec pk_values(module(), map()) :: map()

primary_key(resource)

@spec primary_key(module()) :: [atom()]

resolve_tenant(resource, record)

@spec resolve_tenant(module(), map()) :: {:ok, term()} | {:error, :tenant_required}

resolve_tenant!(resource, record, op)

@spec resolve_tenant!(module(), map(), atom()) :: term()

The fail-closed bang variant of resolve_tenant/2: returns the per-row tenant, or raises a value-free AshReplicant.Error (reason: :tenant_required) when the row carries no usable tenant. op labels the failing sink operation (:upsert / :destroy / ...) in the structural error. The single tenant-resolution entry point shared by every apply path (Apply, Apply.Scd2), so :tenant_required fails identically everywhere.

upsert_action(resource)

@spec upsert_action(module()) :: atom()

The upsert-capable create action name (the resource's primary create action).

upsert_identity(resource)

@spec upsert_identity(module()) :: atom() | nil

The upsert identity name from the DSL (nil → primary-key upsert).

upsert_input(arg, record)

@spec upsert_input(upsert_reflection(), map()) :: {map(), [atom()]}

Map one source record to {inputs, upsert_fields} under a precomputed upsert_reflection/1. AshCloak-sensitive columns pass plaintext under the cloak argument while upsert_fields names encrypted_<col>.

upsert_reflection(resource)

@spec upsert_reflection(module()) :: upsert_reflection()

Compute the batch-invariant upsert reflection for a resource ONCE — the skip list, the AshCloak cloak attributes, and the attribute-name set. Thread it into upsert_input/2 per row of a column-homogeneous batch (the snapshot bulk path) to avoid re-deriving these for every row. Single-record callers use attrs_for_upsert/2, which computes the reflection inline.

version_open_input(resource, record, window)

@spec version_open_input(module(), map(), map()) :: {map(), [atom()]}

The {inputs, upsert_fields} for OPENING a version: the source data columns (via the existing upsert reflection) PLUS the window columns (valid_from_lsn, optional valid_from_ts, valid_to_lsn: nil, optional is_current: true). upsert_fields names every window column so a same-lsn re-open coalesces in place.

writable_target(resource, source_col)

@spec writable_target(module(), String.t()) :: {:ok, atom()} | :skip