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"
Summary
Functions
Returns the fields of the composite type as {name, sql_type} tuples.
Returns the name of a generated function for a specification.
Returns a tagged decimal specification.
Returns a tagged decimal specification or raises.
Types
@type sql_identifier() :: String.t()
@type t() :: %Localize.Ecto.TaggedDecimal{ argument_name: sql_identifier(), errcode: String.t(), extra_fields: [{sql_identifier(), String.t()}], function_names: %{required(atom()) => 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() }
Functions
@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
specis at/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"}]
@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
specis at/0.roleis one of:sum_state,:sum_combine,:avg_state,:avg_combine,:avg_final,:min_state,:min_combine,:max_state,:max_combine,:add,:subtractor: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"
@spec new(Keyword.t()) :: {:ok, t()} | {:error, Exception.t()}
Returns a tagged decimal specification.
Arguments
optionsis a keyword list of options.
Options
:type_nameis the name of the SQL composite type, such as"money_with_currency". Required.:tag_fieldis the name of the composite field holding the tag, such as"currency_code". Required.:value_fieldis the name of the composite field holding the decimal, such as"amount". Required.:function_prefixis the prefix given to generated function names, such as"money"formoney_sum_state_function. Required.:tag_descriptionis the human-readable name of the tag used in error messages, such as"currency code". Required.:tag_sql_typeis the SQL type of the tag field. The default is"varchar".:value_sql_typeis the SQL type of the value field. The default is"numeric".:extra_fieldsis 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_nameis the name given to the incoming row in generated plpgsql. The default is the:function_prefix.:function_namesis 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,:subtractand:negate. The:avg_state_typekey names the intermediate composite type theavgaggregate 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_pluralis the plural of:tag_description, used in generated error messages. The default appends"s".:mismatch_messageis the message of the exception raised when two tags differ. The default is derived from:tag_description.:mismatch_hintis the hint attached to that exception. The default is derived from:tag_description.:errcodeis the SQLSTATE raised on a tag mismatch. The default is"22033", which is the code the aggregates ofex_money_sqlandlocalize_units_sqlhave always raised and is therefore what deployed functions and any consumer rescuing them already use. Note that22033isinvalid_sql_json_subscriptin PostgreSQL 16 and later, not theinvalid_parameter_value(22023) the wording of these messages suggests — pass:errcodeexplicitly to choose a different code for a new type.
Returns
{:ok, spec}wherespecis at/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
Returns a tagged decimal specification or raises.
Arguments
optionsis a keyword list of options. Seenew/1.
Returns
A
t/0, orraises 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"