Current section
Files
Jump to
Current section
Files
lib/flowstone/scatter/result.ex
defmodule FlowStone.Scatter.Result do
@moduledoc """
Ecto schema for individual scatter results.
Stores the result of each scattered instance for efficient
gathering by downstream assets.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@statuses [:pending, :executing, :completed, :failed]
schema "flowstone_scatter_results" do
belongs_to :barrier, FlowStone.Scatter.Barrier
field :scatter_key_hash, :string
field :scatter_key, :map
field :scatter_index, :integer
field :status, Ecto.Enum, values: @statuses, default: :pending
field :result, :binary
field :error, :map
field :duration_ms, :integer
field :completed_at, :utc_datetime_usec
# Batch fields
field :batch_index, :integer
field :batch_items, {:array, :map}
field :batch_input, :map
timestamps(type: :utc_datetime_usec)
end
@type t :: %__MODULE__{
id: Ecto.UUID.t(),
barrier_id: Ecto.UUID.t(),
scatter_key_hash: String.t(),
scatter_key: map(),
scatter_index: non_neg_integer(),
status: atom(),
result: binary() | nil,
error: map() | nil,
duration_ms: non_neg_integer() | nil,
completed_at: DateTime.t() | nil,
batch_index: non_neg_integer() | nil,
batch_items: [map()] | nil,
batch_input: map() | nil,
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@required_fields [:barrier_id, :scatter_key_hash, :scatter_key]
@optional_fields [
:scatter_index,
:status,
:result,
:error,
:duration_ms,
:completed_at,
:batch_index,
:batch_items,
:batch_input
]
@doc """
Build a changeset for creating a result.
"""
@spec changeset(t() | %__MODULE__{}, map()) :: Ecto.Changeset.t()
def changeset(result, attrs) do
result
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:status, @statuses)
|> foreign_key_constraint(:barrier_id)
end
@doc """
Build a changeset for recording completion.
"""
@spec complete_changeset(t(), binary()) :: Ecto.Changeset.t()
def complete_changeset(result, compressed_result) do
change(result, %{
status: :completed,
result: compressed_result,
completed_at: DateTime.utc_now()
})
end
@doc """
Build a changeset for recording failure.
"""
@spec fail_changeset(t(), map()) :: Ecto.Changeset.t()
def fail_changeset(result, error_map) do
change(result, %{
status: :failed,
error: error_map,
completed_at: DateTime.utc_now()
})
end
@doc """
Compress a result value for storage.
Uses :erlang.term_to_binary with compression.
"""
@spec compress(term()) :: binary()
def compress(value) do
:erlang.term_to_binary(value, [:compressed])
end
@doc """
Decompress a stored result value.
Uses safe binary_to_term with [:safe] option.
"""
@spec decompress(binary()) :: term()
def decompress(binary) when is_binary(binary) do
:erlang.binary_to_term(binary, [:safe])
end
def decompress(nil), do: nil
end