{"id":28430954,"url":"https://github.com/elixir-cldr/cldr_units_sql","last_synced_at":"2026-01-20T17:59:59.253Z","repository":{"id":43332324,"uuid":"264686748","full_name":"elixir-cldr/cldr_units_sql","owner":"elixir-cldr","description":"Ecto support for ex_cldr_units","archived":false,"fork":false,"pushed_at":"2023-08-27T23:05:20.000Z","size":259,"stargazers_count":1,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-05T14:39:40.583Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elixir-cldr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-17T14:34:36.000Z","updated_at":"2024-09-06T19:14:23.000Z","dependencies_parsed_at":"2022-07-21T22:17:57.383Z","dependency_job_id":null,"html_url":"https://github.com/elixir-cldr/cldr_units_sql","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/elixir-cldr/cldr_units_sql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_units_sql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_units_sql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_units_sql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_units_sql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elixir-cldr","download_url":"https://codeload.github.com/elixir-cldr/cldr_units_sql/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_units_sql/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263587996,"owners_count":23484843,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-05T14:30:37.668Z","updated_at":"2026-01-20T17:59:59.236Z","avatar_url":"https://github.com/elixir-cldr.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Introduction to Cldr Units SQL\n![Build Status](http://sweatbox.noexpectations.com.au:8080/buildStatus/icon?job=cldr_units_sql)\n[![Hex pm](http://img.shields.io/hexpm/v/ex_cldr_units_sql.svg?style=flat)](https://hex.pm/packages/ex_cldr_units_sql)\n[![License](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://github.com/elixir-cldr/cldr_units_sql/blob/master/LICENSE)\n\n`ex_cldr_units_sql` implements a set of functions to store and retrieve data structured as a `Cldr.Unit.t()` type that represents a unit of measure and a value. See [ex_cldr_units](https://hex.pm/packages/ex_cldr_units) for details of using `Cldr.Unit`.  Note that `ex_cldr_units_sql` depends on `ex_cldr_units`.\n\n## Prerequisities\n\n* `ex_cldr_units_sql` is supported on Elixir 1.11 and later only.\n\n\u003e #### Make sure the Ecto type and the database type match! {: .warning}\n\u003e\n\u003e It's important that the Ecto type `Cldr.Unit.Ecto.Composite.Type` is matched with the correct database type in the migration: `:cldr_unit` or `:cldr_unit_with_usage`.  Similarly `Cldr.Unit.Ecto.Map.Type` must be matched with the database type `:map` in the migration.\n\n## Serializing to a Postgres database with Ecto\n\n`ex_cldr_units_sql` provides custom Ecto data types and two custom Postgres data types to provide serialization of `Cldr.Unit.t` types without losing precision whilst also maintaining the integrity of the `{unit, value}` relationship.  To serialise and retrieve unit types from a database the following steps should be followed:\n\n1. First generate the migration to create the custom type:\n\n```elixir\nmix units.gen.postgres.cldr_units_migration\n* creating priv/repo/migrations\n* creating priv/repo/migrations/20161007234652_add_cldr_unit_type_to_postgres.exs\n```\n\n2. Then migrate the database:\n\n```elixir\nmix ecto.migrate\n21:01:29.527 [info]  == Running 20200517121207 Cldr.Unit.SQL.Repo.Migrations.AddCldrUnitTypeToPostgres.up/0 forward\n\n21:01:29.529 [info]  execute \"CREATE TYPE public.cldr_unit AS (unit varchar, value numeric);\"\n\n21:01:29.532 [info]  execute \"CREATE TYPE public.cldr_unit_with_usage AS (unit varchar, value numeric, usage varchar);\"\n\n21:01:29.546 [info]  == Migrated 20200517121207 in 0.0s\n```\n\n3. Create your database migration with the new type (don't forget to `mix ecto.migrate` as well):\n\n```elixir\ndefmodule Cldr.Unit.Repo.Migrations.CreateProduct do\n  use Ecto.Migration\n\n  def change do\n    create table(:products) do\n      add :weight, :cldr_unit\n      add :length, :cldr_unit_with_usage\n      timestamps()\n    end\n  end\nend\n```\n\n4. Create your schema using the `Cldr.Unit.Ecto.Composite.Type` ecto type:\n\n```elixir\ndefmodule Product do\n  use Ecto.Schema\n\n  schema \"products\" do\n    field :weight, Cldr.Unit.Ecto.Composite.Type\n    field :length, Cldr.UnitWithUsage.Ecto.Composite.Type\n\n    timestamps()\n  end\nend\n```\n\n5. Insert into the database:\n\n```elixir\niex\u003e Repo.insert %Product{weight: Cldr.Unit.new(:kilogram, Decimal.new(100))}\n[debug] QUERY OK db=4.5ms\nINSERT INTO \"products\" (\"value\",\"inserted_at\",\"updated_at\") VALUES ($1,$2,$3)\n[{\"meter\", #Decimal\u003c100\u003e}, {{2016, 10, 7}, {23, 12, 13, 0}}, {{2016, 10, 7}, {23, 12, 13, 0}}]\n```\n\n6. Retrieve from the database:\n\n```elixir\niex\u003e Repo.all Product\n[debug] QUERY OK source=\"products\" db=5.3ms decode=0.1ms queue=0.1ms\nSELECT p0.\"amount\", p0.\"inserted_at\", p0.\"updated_at\" FROM \"products\" AS p0 []\n[%Product{__meta__: #Ecto.Schema.Metadata\u003c:loaded, \"products\"\u003e, weight: #Cldr.Unit\u003c:meter, 100\u003e,\n  inserted_at: ~N[2017-02-21 00:15:40.979576],\n  updated_at: ~N[2017-02-21 00:15:40.991391]}]\n```\n\n## Serializing to a MySQL (or other non-Postgres) database with Ecto\n\nSince MySQL does not support composite types, the `:map` type is used which in MySQL is implemented as a `JSON` column.  The currency code and amount are serialised into this column.\n```elixir\ndefmodule Cldr.Unit.Repo.Migrations.CreateProduct do\n  use Ecto.Migration\n\n  def change do\n    create table(:products) do\n      add :weight_map, :map\n      add :length_map, :map\n      timestamps()\n    end\n  end\nend\n```\n\nCreate your schema using the `Cldr.Unit.Ecto.Map.Type` ecto type:\n```elixir\ndefmodule Product do\n  use Ecto.Schema\n\n  schema \"products\" do\n    field :weight_map, Cldr.Unit.Ecto.Map.Type\n    field :length_map, Cldr.UnitWithUsage.Ecto.Map.Type\n\n    timestamps()\n  end\nend\n```\n\nInsert into the database:\n```elixir\niex\u003e Repo.insert %Product{weight_map: Cldr.Unit.new!(:kilogram, 100)}\n[debug] QUERY OK db=25.8ms\nINSERT INTO \"products\" (\"weight_map\",\"inserted_at\",\"updated_at\") VALUES ($1,$2,$3)\nRETURNING \"id\" [%{value: \"100\", unit: \"kilogram\"},\n{{2017, 2, 21}, {0, 15, 40, 979576}}, {{2017, 2, 21}, {0, 15, 40, 991391}}]\n\n{:ok,\n %Product{__meta__: #Ecto.Schema.Metadata\u003c:loaded, \"products\"\u003e,\n  amount: nil, weight_map: #Cldr.Unit\u003c:kilogram, 100\u003e, id: 3,\n  inserted_at: ~N[2017-02-21 00:15:40.979576],\n  updated_at: ~N[2017-02-21 00:15:40.991391]}}\n```\n\nRetrieve from the database:\n```elixir\niex\u003e Repo.all Product\n[debug] QUERY OK source=\"products\" db=16.1ms decode=0.1ms\nSELECT t0.\"id\", t0.\"weight_map\", t0.\"inserted_at\", t0.\"updated_at\" FROM \"products\" AS t0 []\n[%Ledger{__meta__: #Ecto.Schema.Metadata\u003c:loaded, \"products\"\u003e,\n  weight_map: #Cldr.Unit\u003c:kilogram, 100\u003e, id: 3,\n  inserted_at: ~N[2017-02-21 00:15:40.979576],\n  updated_at: ~N[2017-02-21 00:15:40.991391]}]\n```\n\n### Notes:\n\n1.  In order to preserve precision of the decimal amount, the amount part of the `Cldr.Unit.t()` struct is serialised as a string. This is done because JSON serializes numeric values as either `integer` or `float`, neither of which would preserve precision of a decimal value.\n\n2.  The precision of the serialized string value is affected by the setting of `Decimal.get_context`.  The default is 28 digits which should cater for your requirements.\n\n3.  Serializing the amount as a string means that SQL query arithmetic and equality operators will not work as expected.  You may find that `CAST`ing the string value will restore some of that functionality.  For example:\n\n```sql\nCAST(JSON_EXTRACT(amount_map, '$.value') AS DECIMAL(20, 8)) AS amount;\n```\n\n## Postgres Database functions\n\nSince the datatype used to store `Cldr.Unit` in Postgres is a composite type (called `:cldr_unit`), the standard aggregation functions like `sum` and `average` are not supported and the `order_by` clause doesn't perform as expected.  `ex_cldr_units_sql` provides mechanisms to provide these functions.\n\n### Aggregate functions: sum()\n\n`ex_cldr_unit_sql` provides a migration generator which, when migrated to the database with `mix ecto.migrate`, supports performing `sum()` aggregation on `:cldr_unit` types. The steps are:\n\n1. Generate the migration by executing `mix units.gen.postgres.aggregate_functions`\n\n2. Migrate the database by executing `mix ecto.migrate`\n\n3. Formulate an Ecto query to use the aggregate function `sum()`\n\n```elixir\n  # Formulate the query.  Note the required use of the type()\n  # expression which is needed to inform Ecto of the return\n  # type of the function\n  iex\u003e q = Ecto.Query.select Product, [p], type(sum(p.weight), p.weight)\n  #Ecto.Query\u003cfrom p in Product select: type(sum(p.weight), p.weight)\u003e\n  iex\u003e Repo.all q\n  [debug] QUERY OK source=\"products\" db=6.1ms\n  SELECT sum(p0.\"weight\")::cldr_unit_with_usage FROM \"products\" AS l0 []\n  [#Cldr.Unit\u003c:meter, 600\u003e]\n```\n\nThe function `Repo.aggregate/3` can also be used. However at least [ecto version 3.2.4](https://hex/pm/packages/ecto/3.2.4) is required for this to work correctly for custom ecto types such as `:cldr_unit`.\n\n```elixir\n  iex\u003e Repo.aggregate(Product, :sum, :weight)\n  #Cldr.Unit\u003c:kilogram, 100\u003e\n```\n\n**Note** that to preserve the integrity of `Cldr.Unit` it is not permissible to aggregate units that has different unit types.  If you attempt to aggregate unit with different unit types the query will abort and an exception will be raised:\n```elixir\n  iex\u003e Repo.all q\n  [debug] QUERY ERROR source=\"products\" db=4.5ms\n  SELECT sum(p0.\"weight\")::cldr_unit_with_usage FROM \"products\" AS p0 []\n  ** (Postgrex.Error) ERROR 22033 (): Incompatible units. Expected all units to be :kilogram\n```\n\n### Order_by with cldr_unit type\n\nSince `:cldr_unit` is a composite type, the default `order_by` results may surprise since the ordering is based upon the type structure, not the unit value.  Postgres defines a means to access the components of a composite type and therefore sorting can be done in a more predictable fashion.  For example:\n```elixir\n  # In this example we are decomposing the the composite column called\n  # `price` and using the sub-field `value` to perform the ordering.\n  iex\u003e q = from p in Product, select: p.weight, order_by: fragment(\"value(weight)\")\n  #Ecto.Query\u003cfrom p in Product, order_by: [asc: fragment(\"value(weight)\")],\n   select: p.weight\u003e\n  iex\u003e Repo.all q\n  [debug] QUERY OK source=\"products\" db=2.0ms\n  SELECT p0.\"weight\" FROM \"products\" AS p0 ORDER BY value(weight) []\n  [#Cldr.Unit\u003c:kilogram, 100\u003e, #Cldr.Unit\u003c:pound, 200\u003e,\n   #Cldr.Unit\u003c:pound, 300\u003e, #Cldr.Unit\u003c:kilogram, 300\u003e]\n```\n**Note** that the results may still be unexpected.  The example above shows the correct ascending ordering by `value(weight)` however the ordering is not unit aware and therefore mixed units will return a largely meaningless order.\n\n## Installation\n\n`ex_cldr_units_sql` can be installed by adding `ex_cldr_units_sql` to your list of dependencies in `mix.exs` and then executing `mix deps.get`\n\n```elixir\ndef deps do\n  [\n    {:ex_cldr_units_sql, \"~\u003e 1.0\"},\n    ...\n  ]\nend\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-cldr%2Fcldr_units_sql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felixir-cldr%2Fcldr_units_sql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-cldr%2Fcldr_units_sql/lists"}