# `Localize.Ecto.TaggedDecimal`
[🔗](https://github.com/elixir-localize/localize_sql/blob/v1.0.1/lib/localize/ecto/tagged_decimal.ex#L1)

Describes a *tagged decimal* — a value that pairs a tag with a decimal
amount — for serialization to a SQL database.

A tagged decimal is the shape shared by a money amount (a currency code
and an amount), a unit (a unit name and a value), and anything else
that is meaningless without the label travelling alongside the number.
Storing the two together in one column keeps them inseparable, and
lets the database itself refuse to add euros to yen.

This module holds the *specification* of one such type: the name of
the composite type, the names of its fields, and the wording of the
error raised when two tags do not match. `Localize.Ecto.TaggedDecimal.DDL`
turns a specification into the SQL that creates the composite type,
its aggregate functions and its operators, and
`Localize.Ecto.TaggedDecimal.Composite` and
`Localize.Ecto.TaggedDecimal.Map` use it to load and dump values
through Ecto.

The specification exists because the SQL is otherwise identical
between domains. `ex_money_sql` describes
`money_with_currency(currency_code, amount)` and `localize_sql`
describes `cldr_unit(unit, value)`; the generated plpgsql differs only
in those identifiers and in the error message.

### Field naming

The field names are part of the specification rather than fixed
because they are already committed to in deployed databases — a
generated aggregate must name the same fields as the composite type it
was created against. PostgreSQL's functional notation makes each field
callable as a function of the composite (`amount(col)` is `(col).amount`),
which is how the generated SQL and the Ecto query API read fields.

### Examples

    iex> Localize.Ecto.TaggedDecimal.new!(
    ...>   type_name: "money_with_currency",
    ...>   tag_field: "currency_code",
    ...>   value_field: "amount",
    ...>   function_prefix: "money",
    ...>   tag_description: "currency code"
    ...> ).type_name
    "money_with_currency"

# `sql_identifier`

```elixir
@type sql_identifier() :: String.t()
```

# `t`

```elixir
@type t() :: %Localize.Ecto.TaggedDecimal{
  argument_name: sql_identifier(),
  errcode: String.t(),
  extra_fields: [{sql_identifier(), String.t()}],
  function_names: %{required(atom()) =&gt; sql_identifier()},
  function_prefix: sql_identifier(),
  mismatch_hint: String.t(),
  mismatch_message: String.t(),
  tag_description: String.t(),
  tag_description_plural: String.t(),
  tag_field: sql_identifier(),
  tag_sql_type: String.t(),
  type_name: sql_identifier(),
  value_field: sql_identifier(),
  value_sql_type: String.t()
}
```

# `fields`

```elixir
@spec fields(t()) :: [{sql_identifier(), String.t()}]
```

Returns the fields of the composite type as `{name, sql_type}` tuples.

The tag and value fields come first, in that order, followed by any
`:extra_fields`.

### Arguments

* `spec` is a `t:t/0`.

### Returns

* A list of `{name, sql_type}` tuples.

### Examples

    iex> spec = Localize.Ecto.TaggedDecimal.new!(
    ...>   type_name: "cldr_unit_with_usage", tag_field: "unit",
    ...>   value_field: "value", function_prefix: "unit_with_usage",
    ...>   tag_description: "unit name", extra_fields: [{"usage", "varchar"}]
    ...> )
    iex> Localize.Ecto.TaggedDecimal.fields(spec)
    [{"unit", "varchar"}, {"value", "numeric"}, {"usage", "varchar"}]

# `function_name`

```elixir
@spec function_name(t(), atom()) :: sql_identifier()
```

Returns the name of a generated function for a specification.

Names follow `<function_prefix>_<role>_function` — for example
`money_sum_state_function` — unless overridden by the
`:function_names` option of `new/1`.

### Arguments

* `spec` is a `t:t/0`.

* `role` is one of `:sum_state`, `:sum_combine`, `:avg_state`,
  `:avg_combine`, `:avg_final`, `:min_state`, `:min_combine`,
  `:max_state`, `:max_combine`, `:add`, `:subtract` or `:negate`.

### Returns

* The function name as a string.

### Examples

    iex> spec = Localize.Ecto.TaggedDecimal.new!(
    ...>   type_name: "money_with_currency", tag_field: "currency_code",
    ...>   value_field: "amount", function_prefix: "money",
    ...>   tag_description: "currency code"
    ...> )
    iex> Localize.Ecto.TaggedDecimal.function_name(spec, :sum_state)
    "money_sum_state_function"
    iex> Localize.Ecto.TaggedDecimal.function_name(spec, :add)
    "money_add"

# `new`

```elixir
@spec new(Keyword.t()) :: {:ok, t()} | {:error, Exception.t()}
```

Returns a tagged decimal specification.

### Arguments

* `options` is a keyword list of options.

### Options

* `:type_name` is the name of the SQL composite type, such as
  `"money_with_currency"`. Required.

* `:tag_field` is the name of the composite field holding the tag, such
  as `"currency_code"`. Required.

* `:value_field` is the name of the composite field holding the decimal,
  such as `"amount"`. Required.

* `:function_prefix` is the prefix given to generated function names,
  such as `"money"` for `money_sum_state_function`. Required.

* `:tag_description` is the human-readable name of the tag used in
  error messages, such as `"currency code"`. Required.

* `:tag_sql_type` is the SQL type of the tag field. The default is
  `"varchar"`.

* `:value_sql_type` is the SQL type of the value field. The default is
  `"numeric"`.

* `:extra_fields` is a list of `{name, sql_type}` tuples appended to
  the composite type after the tag and value, such as
  `[{"usage", "varchar"}]`. The default is `[]`.

* `:argument_name` is the name given to the incoming row in generated
  plpgsql. The default is the `:function_prefix`.

* `:function_names` is a map of overrides for generated function
  names, keyed by `:sum_state`, `:sum_combine`, `:avg_state`,
  `:avg_combine`, `:avg_final`, `:avg_state_type`, `:min_state`,
  `:min_combine`, `:max_state`, `:max_combine`, `:add`, `:subtract`
  and `:negate`. The `:avg_state_type` key names the intermediate
  composite type the `avg` aggregate accumulates into rather than a
  function.
  Deployed databases name these functions in their aggregate
  definitions, so an existing name that does not follow the default
  pattern is preserved here. The default is `%{}`.

* `:tag_description_plural` is the plural of `:tag_description`, used
  in generated error messages. The default appends `"s"`.

* `:mismatch_message` is the message of the exception raised when two
  tags differ. The default is derived from `:tag_description`.

* `:mismatch_hint` is the hint attached to that exception. The default
  is derived from `:tag_description`.

* `:errcode` is the SQLSTATE raised on a tag mismatch. The default is
  `"22033"`, which is the code the aggregates of `ex_money_sql` and
  `localize_units_sql` have always raised and is therefore what
  deployed functions and any consumer rescuing them already use. Note
  that `22033` is `invalid_sql_json_subscript` in PostgreSQL 16 and
  later, not the `invalid_parameter_value` (`22023`) the wording of
  these messages suggests — pass `:errcode` explicitly to choose a
  different code for a new type.

### Returns

* `{:ok, spec}` where `spec` is a `t:t/0`, or

* `{:error, exception}` if a required option is missing or an option
  has an invalid value.

### Examples

    iex> {:ok, spec} = Localize.Ecto.TaggedDecimal.new(
    ...>   type_name: "cldr_unit",
    ...>   tag_field: "unit",
    ...>   value_field: "value",
    ...>   function_prefix: "unit",
    ...>   tag_description: "unit name"
    ...> )
    iex> spec.mismatch_message
    "Incompatible unit names. Expected all unit names to be %"

    iex> {:error, exception} = Localize.Ecto.TaggedDecimal.new(tag_field: "unit")
    iex> is_exception(exception)
    true

# `new!`

```elixir
@spec new!(Keyword.t()) :: t() | no_return()
```

Returns a tagged decimal specification or raises.

### Arguments

* `options` is a keyword list of options. See `new/1`.

### Returns

* A `t:t/0`, or

* raises if an option is missing or invalid.

### Examples

    iex> Localize.Ecto.TaggedDecimal.new!(
    ...>   type_name: "cldr_unit",
    ...>   tag_field: "unit",
    ...>   value_field: "value",
    ...>   function_prefix: "unit",
    ...>   tag_description: "unit name"
    ...> ).tag_field
    "unit"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
