From c62a6b62e23a7bdfc6aa69af67e553ffd4862440 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Sat, 10 Aug 2024 13:02:18 -0400 Subject: [PATCH] Add support for `:duration` type (#631) --- .github/workflows/ci.yml | 8 ++++---- integration_test/myxql/test_helper.exs | 1 + integration_test/pg/test_helper.exs | 7 ++++++- integration_test/support/migration.exs | 12 ++++++++++++ integration_test/tds/test_helper.exs | 1 + lib/ecto/adapters/postgres/connection.ex | 18 ++++++++++++++++++ lib/ecto/migration.ex | 2 ++ mix.exs | 2 +- mix.lock | 8 ++++---- test/ecto/adapters/postgres_test.exs | 2 ++ 10 files changed, 51 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a93ec68..343c49e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,10 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.15.6 - otp: 24.3.4.13 - - elixir: 1.15.6 - otp: 26.1.2 + - elixir: 1.17.2 + otp: 25.0.4 + - elixir: 1.17.2 + otp: 27.0.1 lint: lint steps: - name: Checkout diff --git a/integration_test/myxql/test_helper.exs b/integration_test/myxql/test_helper.exs index 4bede7b8..fafeb700 100644 --- a/integration_test/myxql/test_helper.exs +++ b/integration_test/myxql/test_helper.exs @@ -95,6 +95,7 @@ version = excludes = [ # not sure how to support this yet :bitstring_type, + :duration_type, # MySQL does not have an array type :array_type, # The next two features rely on RETURNING, which MySQL does not support diff --git a/integration_test/pg/test_helper.exs b/integration_test/pg/test_helper.exs index def0b572..0a21ace3 100644 --- a/integration_test/pg/test_helper.exs +++ b/integration_test/pg/test_helper.exs @@ -12,6 +12,10 @@ Application.put_env(:ecto_sql, :pg_test_url, Code.require_file "../support/repo.exs", __DIR__ +# Define type module +opts = if Code.ensure_loaded?(Duration), do: [interval_decode_type: Duration], else: [] +Postgrex.Types.define(Postgrex.EctoTypes, [], opts) + # Pool repo for async, safe tests alias Ecto.Integration.TestRepo @@ -19,7 +23,8 @@ Application.put_env(:ecto_sql, TestRepo, url: Application.get_env(:ecto_sql, :pg_test_url) <> "/ecto_test", pool: Ecto.Adapters.SQL.Sandbox, show_sensitive_data_on_connection_error: true, - log: false + log: false, + types: Postgrex.EctoTypes ) defmodule Ecto.Integration.TestRepo do diff --git a/integration_test/support/migration.exs b/integration_test/support/migration.exs index c2a3e417..f4f0229a 100644 --- a/integration_test/support/migration.exs +++ b/integration_test/support/migration.exs @@ -116,6 +116,18 @@ defmodule Ecto.Integration.Migration do end end + if Code.ensure_loaded?(Duration) do + unless :duration_type in ExUnit.configuration()[:exclude] do + create table(:durations) do + add :dur, :duration + add :dur_with_fields, :duration, fields: "MONTH" + add :dur_with_precision, :duration, precision: 4 + add :dur_with_fields_and_precision, :duration, fields: "HOUR TO SECOND", precision: 1 + add :dur_with_default, :duration, default: "10 MONTH" + end + end + end + create table(:composite_pk, primary_key: false) do add :a, :integer, primary_key: true add :b, :integer, primary_key: true diff --git a/integration_test/tds/test_helper.exs b/integration_test/tds/test_helper.exs index 9e0788b6..213329dd 100644 --- a/integration_test/tds/test_helper.exs +++ b/integration_test/tds/test_helper.exs @@ -5,6 +5,7 @@ ExUnit.start( # not sure how to support this yet :aggregate_filters, :bitstring_type, + :duration_type, # subquery contains ORDER BY and that is not supported :subquery_aggregates, # sql don't have array type diff --git a/lib/ecto/adapters/postgres/connection.ex b/lib/ecto/adapters/postgres/connection.ex index b875725d..579275e3 100644 --- a/lib/ecto/adapters/postgres/connection.ex +++ b/lib/ecto/adapters/postgres/connection.ex @@ -1797,6 +1797,23 @@ if Code.ensure_loaded?(Postgrex) do end end + defp column_type(:duration, opts) do + precision = Keyword.get(opts, :precision) + fields = Keyword.get(opts, :fields) + generated = Keyword.get(opts, :generated) + type_name = ecto_to_db(:duration) + + type = + cond do + fields && precision -> [type_name, " ", fields, ?(, to_string(precision), ?)] + precision -> [type_name, ?(, to_string(precision), ?)] + fields -> [type_name, " ", fields] + true -> [type_name] + end + + [type, generated_expr(generated)] + end + defp column_type(type, opts) do size = Keyword.get(opts, :size) precision = Keyword.get(opts, :precision) @@ -2016,6 +2033,7 @@ if Code.ensure_loaded?(Postgrex) do defp ecto_to_db(:utc_datetime_usec), do: "timestamp" defp ecto_to_db(:naive_datetime), do: "timestamp" defp ecto_to_db(:naive_datetime_usec), do: "timestamp" + defp ecto_to_db(:duration), do: "interval" defp ecto_to_db(atom) when is_atom(atom), do: Atom.to_string(atom) defp ecto_to_db(type) do diff --git a/lib/ecto/migration.ex b/lib/ecto/migration.ex index 7093d38f..7ddc64f6 100644 --- a/lib/ecto/migration.ex +++ b/lib/ecto/migration.ex @@ -1145,6 +1145,8 @@ defmodule Ecto.Migration do generation. Default is defined by the database. * `:increment` - option for `:identity` key, represents increment value for sequence generation. Default is defined by the database. + * `:fields` - option for `:duration` type. Restricts the set of stored interval fields + in the database. """ def add(column, type, opts \\ []) when is_atom(column) and is_list(opts) do diff --git a/mix.exs b/mix.exs index a722130c..2d85efd1 100644 --- a/mix.exs +++ b/mix.exs @@ -84,7 +84,7 @@ defmodule EctoSQL.MixProject do if path = System.get_env("POSTGREX_PATH") do {:postgrex, path: path} else - {:postgrex, "~> 0.16 or ~> 1.0", optional: true} + {:postgrex, "~> 0.19 or ~> 1.0", optional: true} end end diff --git a/mix.lock b/mix.lock index 2daca590..02934b04 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,18 @@ %{ "benchee": {:hex, :benchee, "1.2.0", "afd2f0caec06ce3a70d9c91c514c0b58114636db9d83c2dc6bfd416656618353", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "ee729e53217898b8fd30aaad3cce61973dab61574ae6f48229fe7ff42d5e4457"}, - "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, - "ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "a898915d2f16dbf1257b8c83a11a1ae07193de42", []}, + "ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "9ae70db7946223f36e62c94fa8cf9124bf219bc6", []}, "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "myxql": {:hex, :myxql, "0.7.1", "7c7b75aa82227cd2bc9b7fbd4de774fb19a1cdb309c219f411f82ca8860f8e01", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a491cdff53353a09b5850ac2d472816ebe19f76c30b0d36a43317a67c9004936"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"}, + "postgrex": {:hex, :postgrex, "0.19.0", "f7d50e50cb42e0a185f5b9a6095125a9ab7e4abccfbe2ab820ab9aa92b71dbab", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "dba2d2a0a8637defbf2307e8629cb2526388ba7348f67d04ec77a5d6a72ecfae"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "tds": {:hex, :tds, "2.3.4", "534749dd9ef61af960fcafa9cbb7186d6d7b9f92ea0133fb25da07b121c8295c", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "bb9a53d4688a85fd566f342f76b50d39adfc4b410062886ef908365ead24ba3f"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, diff --git a/test/ecto/adapters/postgres_test.exs b/test/ecto/adapters/postgres_test.exs index 68854a68..9fa26a35 100644 --- a/test/ecto/adapters/postgres_test.exs +++ b/test/ecto/adapters/postgres_test.exs @@ -2085,6 +2085,7 @@ defmodule Ecto.Adapters.PostgresTest do {:add, :flags, :bitstring, [null: false]}, {:add, :flags_with_default, :bitstring, [default: <<42::10>>]}, {:add, :flags_with_size, :bitstring, [size: 10]}, + {:add, :dur, :duration, [fields: "YEAR TO MONTH", precision: 2, default: "1 MONTH"]}, {:add, :tags, {:array, :string}, [default: []]}, {:add, :languages, {:array, :string}, [default: ["pt", "es"]]}, {:add, :limits, {:array, :integer}, [default: [100, 30_000]]} @@ -2100,6 +2101,7 @@ defmodule Ecto.Adapters.PostgresTest do "flags" varbit NOT NULL, "flags_with_default" varbit DEFAULT b'0000101010', "flags_with_size" varbit(10), + "dur" interval YEAR TO MONTH(2) DEFAULT '1 MONTH', "tags" varchar(255)[] DEFAULT ARRAY[]::varchar[], "languages" varchar(255)[] DEFAULT ARRAY['pt','es']::varchar[], "limits" integer[] DEFAULT ARRAY[100,30000]::integer[])