Skip to content

Commit 9ae70db

Browse files
Add :duration type (#4479)
1 parent 4f75637 commit 9ae70db

File tree

6 files changed

+94
-7
lines changed

6 files changed

+94
-7
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ jobs:
1919
fail-fast: false
2020
matrix:
2121
include:
22-
- elixir: 1.15.6
23-
otp: 26.1.2
22+
- elixir: 1.17.2
23+
otp: 27.0.1
2424
lint: lint
25-
- elixir: 1.15.6
26-
otp: 24.3.4.13
25+
- elixir: 1.17.2
26+
otp: 25.0.4
2727
- elixir: 1.11.4
2828
otp: 21.3.8.24
2929

@@ -66,8 +66,8 @@ jobs:
6666
fail-fast: false
6767
matrix:
6868
elixirbase:
69-
- "1.15.6-erlang-26.1.2-alpine-3.16.7"
70-
- "1.15.6-erlang-24.3.4.14-alpine-3.16.7"
69+
- "1.17.2-erlang-27.0.1-alpine-3.17.8"
70+
- "1.17.2-erlang-25.0.4-alpine-3.17.8"
7171
- "1.11.4-erlang-21.3.8.24-alpine-3.13.3"
7272
steps:
7373
- uses: earthly/actions-setup@v1

Earthfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ all:
88

99
integration-test-base:
1010
ARG ELIXIR_BASE=1.15.6-erlang-25.3.2.6-alpine-3.18.4
11-
ARG TARGETARCH
11+
ARG TARGETARCH
1212
FROM hexpm/elixir:$ELIXIR_BASE
1313
RUN apk add --no-progress --update git build-base
1414
RUN mix local.rebar --force

integration_test/cases/type.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,53 @@ defmodule Ecto.Integration.TypeTest do
8080
assert [<<42::6>>] = TestRepo.all(from p in Bitstring, limit: 1, select: p.bs_with_default)
8181
end
8282

83+
if Code.ensure_loaded?(Duration) do
84+
@tag :duration_type
85+
test "duration type" do
86+
duration = %Duration{year: 1, month: 1, second: 1, microsecond: {100, 6}}
87+
88+
struct = %Ecto.Integration.Duration{
89+
dur: duration,
90+
dur_with_fields: duration,
91+
dur_with_precision: duration,
92+
dur_with_fields_and_precision: duration
93+
}
94+
95+
TestRepo.insert!(struct)
96+
97+
persisted_duration =
98+
from(d in Ecto.Integration.Duration, where: d.dur == ^duration)
99+
|> TestRepo.one()
100+
101+
assert persisted_duration.dur == duration
102+
103+
# `:field` option set to MONTH so it ignores all units lower than `:month`
104+
assert persisted_duration.dur_with_fields == %Duration{
105+
year: 1,
106+
month: 1,
107+
microsecond: {0, 6}
108+
}
109+
110+
assert persisted_duration.dur_with_precision == %Duration{
111+
year: 1,
112+
month: 1,
113+
second: 1,
114+
microsecond: {100, 4}
115+
}
116+
117+
# `:field` option is set to HOUR TO SECOND so it ignores all units lower than `:second`
118+
assert persisted_duration.dur_with_fields_and_precision == %Duration{
119+
year: 1,
120+
month: 1,
121+
second: 1,
122+
microsecond: {0, 1}
123+
}
124+
125+
# `:default set in migration`
126+
assert persisted_duration.dur_with_default == %Duration{month: 10, microsecond: {0, 6}}
127+
end
128+
end
129+
83130
@tag :select_not
84131
test "primitive types boolean negate" do
85132
TestRepo.insert!(%Post{public: true})

integration_test/support/schemas.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,21 @@ defmodule Ecto.Integration.Bitstring do
403403
field :bs_with_size, :bitstring
404404
end
405405
end
406+
407+
if Code.ensure_loaded?(Duration) do
408+
defmodule Ecto.Integration.Duration do
409+
@moduledoc """
410+
This module is used to test:
411+
* Duration type
412+
"""
413+
use Ecto.Integration.Schema
414+
415+
schema "durations" do
416+
field :dur, :duration
417+
field :dur_with_fields, :duration
418+
field :dur_with_precision, :duration
419+
field :dur_with_fields_and_precision, :duration
420+
field :dur_with_default, :duration
421+
end
422+
end
423+
end

lib/ecto/schema.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ defmodule Ecto.Schema do
268268
`:naive_datetime_usec` | `NaiveDateTime` |
269269
`:utc_datetime` | `DateTime` |
270270
`:utc_datetime_usec` | `DateTime` |
271+
`:duration` | `Duration` |
271272
272273
**Notes:**
273274
@@ -294,6 +295,10 @@ defmodule Ecto.Schema do
294295
where casting has to be done explicitly and is never performed
295296
implicitly when loading from or dumping to the database.
296297
298+
* For `the `:duration` type, you may need to enable `Duration` support in
299+
your adapter. For information on how to enable it in Postgrex, see their
300+
[HexDocs page](https://hexdocs.pm/postgrex/readme.html#data-representation).
301+
297302
### Custom types
298303
299304
Besides providing primitive types, Ecto allows custom types to be

lib/ecto/type.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ defmodule Ecto.Type do
222222
| :utc_datetime_usec
223223
| :naive_datetime_usec
224224
| :time_usec
225+
| :duration
225226

226227
@type composite :: {:array, t} | {:map, t} | private_composite
227228

@@ -231,6 +232,7 @@ defmodule Ecto.Type do
231232
integer float decimal boolean string bitstring map binary id binary_id any
232233
utc_datetime naive_datetime date time
233234
utc_datetime_usec naive_datetime_usec time_usec
235+
duration
234236
)a
235237
@composite ~w(array map try in param)a
236238
@variadic ~w(in splice)a
@@ -564,6 +566,7 @@ defmodule Ecto.Type do
564566
def dump(:naive_datetime_usec, value, _dumper), do: dump_naive_datetime_usec(value)
565567
def dump(:utc_datetime, value, _dumper), do: dump_utc_datetime(value)
566568
def dump(:utc_datetime_usec, value, _dumper), do: dump_utc_datetime_usec(value)
569+
def dump(:duration, value, _dumper), do: same_duration(value)
567570
def dump({:supertype, :datetime}, value, _dumper), do: dump_any_datetime(value)
568571
def dump(mod, value, _dumper) when is_atom(mod), do: mod.dump(value)
569572

@@ -661,6 +664,7 @@ defmodule Ecto.Type do
661664
def load(:naive_datetime_usec, value, _loader), do: load_naive_datetime_usec(value)
662665
def load(:utc_datetime, value, _loader), do: load_utc_datetime(value)
663666
def load(:utc_datetime_usec, value, _loader), do: load_utc_datetime_usec(value)
667+
def load(:duration, value, _loader), do: same_duration(value)
664668
def load(mod, value, _loader), do: mod.load(value)
665669

666670
defp load_float(term) when is_float(term), do: {:ok, term}
@@ -833,6 +837,7 @@ defmodule Ecto.Type do
833837
defp cast_fun(:naive_datetime_usec), do: &maybe_pad_usec(cast_naive_datetime(&1))
834838
defp cast_fun(:utc_datetime), do: &maybe_truncate_usec(cast_utc_datetime(&1))
835839
defp cast_fun(:utc_datetime_usec), do: &maybe_pad_usec(cast_utc_datetime(&1))
840+
defp cast_fun(:duration), do: &cast_duration/1
836841
defp cast_fun({:supertype, :datetime}), do: &cast_any_datetime(&1)
837842
defp cast_fun({:parameterized, {mod, params}}), do: &mod.cast(&1, params)
838843
defp cast_fun({qual, type}) when qual in @variadic, do: cast_fun({:array, type})
@@ -911,6 +916,12 @@ defmodule Ecto.Type do
911916
defp cast_map(term) when is_map(term), do: {:ok, term}
912917
defp cast_map(_), do: :error
913918

919+
if Code.ensure_loaded?(Duration) do
920+
defp cast_duration(%Duration{} = term), do: {:ok, term}
921+
end
922+
923+
defp cast_duration(_), do: :error
924+
914925
@doc """
915926
Casts a value to the given type or raises an error.
916927
@@ -967,6 +978,12 @@ defmodule Ecto.Type do
967978
defp same_date(%Date{} = term), do: {:ok, term}
968979
defp same_date(_), do: :error
969980

981+
if Code.ensure_loaded?(Duration) do
982+
defp same_duration(%Duration{} = term), do: {:ok, term}
983+
end
984+
985+
defp same_duration(_), do: :error
986+
970987
@doc false
971988
def empty_trimmed_string?(value) do
972989
is_binary(value) and String.trim_leading(value) == ""

0 commit comments

Comments
 (0)