diff --git a/lib/ecto/adapters/sqlite3.ex b/lib/ecto/adapters/sqlite3.ex index 3d29f81..6c66e56 100644 --- a/lib/ecto/adapters/sqlite3.ex +++ b/lib/ecto/adapters/sqlite3.ex @@ -48,6 +48,9 @@ defmodule Ecto.Adapters.SQLite3 do `:binary_id` columns. See the [section on binary ID types](#module-binary-id-types) for more details. * `:uuid_type` - Defaults to `:string`. Determines the type of `:uuid` columns. Possible values and column types are the same as for [binary IDs](#module-binary-id-types). + * `:datetime_type` - Defaults to `:iso8601`. Determines how datetime fields are stored in the database. + The allowed values are `:iso8601` and `:text_datetime`. `:iso8601` corresponds to a string of the form + `YYYY-MM-DDThh:mm:ss` and `:text_datetime` corresponds to a string of the form YYYY-MM-DD hh:mm:ss` For more information about the options above, see [sqlite documentation][1] @@ -259,6 +262,8 @@ defmodule Ecto.Adapters.SQLite3 do ## Loaders ## + @default_datetime_type :iso8601 + @impl Ecto.Adapter def loaders(:boolean, type) do [&Codec.bool_decode/1, type] @@ -390,22 +395,26 @@ defmodule Ecto.Adapters.SQLite3 do @impl Ecto.Adapter def dumpers(:utc_datetime, type) do - [type, &Codec.utc_datetime_encode/1] + dt_type = Application.get_env(:ecto_sqlite3, :datetime_type, @default_datetime_type) + [type, &Codec.utc_datetime_encode(&1, dt_type)] end @impl Ecto.Adapter def dumpers(:utc_datetime_usec, type) do - [type, &Codec.utc_datetime_encode/1] + dt_type = Application.get_env(:ecto_sqlite3, :datetime_type, @default_datetime_type) + [type, &Codec.utc_datetime_encode(&1, dt_type)] end @impl Ecto.Adapter def dumpers(:naive_datetime, type) do - [type, &Codec.naive_datetime_encode/1] + dt_type = Application.get_env(:ecto_sqlite3, :datetime_type, @default_datetime_type) + [type, &Codec.naive_datetime_encode(&1, dt_type)] end @impl Ecto.Adapter def dumpers(:naive_datetime_usec, type) do - [type, &Codec.naive_datetime_encode/1] + dt_type = Application.get_env(:ecto_sqlite3, :datetime_type, @default_datetime_type) + [type, &Codec.naive_datetime_encode(&1, dt_type)] end @impl Ecto.Adapter diff --git a/lib/ecto/adapters/sqlite3/codec.ex b/lib/ecto/adapters/sqlite3/codec.ex index dc33758..fae10a4 100644 --- a/lib/ecto/adapters/sqlite3/codec.ex +++ b/lib/ecto/adapters/sqlite3/codec.ex @@ -98,12 +98,31 @@ defmodule Ecto.Adapters.SQLite3.Codec do {:ok, value} end - # Ecto does check this already, so there should be no need to handle errors - def utc_datetime_encode(%{time_zone: "Etc/UTC"} = value) do + @text_datetime_format "%Y-%m-%d %H:%M:%S" + + def utc_datetime_encode(%{time_zone: "Etc/UTC"} = value, :iso8601) do {:ok, NaiveDateTime.to_iso8601(value)} end - def naive_datetime_encode(value) do + def utc_datetime_encode(%{time_zone: "Etc/UTC"} = value, :text_datetime) do + {:ok, Calendar.strftime(value, @text_datetime_format)} + end + + def utc_datetime_encode(%{time_zone: "Etc/UTC"}, type) do + raise ArgumentError, + "expected datetime type to be either `:iso8601` or `:text_datetime`, but received #{inspect(type)}" + end + + def naive_datetime_encode(value, :iso8601) do {:ok, NaiveDateTime.to_iso8601(value)} end + + def naive_datetime_encode(value, :text_datetime) do + {:ok, Calendar.strftime(value, @text_datetime_format)} + end + + def naive_datetime_encode(_value, type) do + raise ArgumentError, + "expected datetime type to be either `:iso8601` or `:text_datetime`, but received `#{inspect(type)}`" + end end diff --git a/test/ecto/adapters/sqlite3/codec_test.exs b/test/ecto/adapters/sqlite3/codec_test.exs index 33a73e8..922ddf0 100644 --- a/test/ecto/adapters/sqlite3/codec_test.exs +++ b/test/ecto/adapters/sqlite3/codec_test.exs @@ -126,4 +126,54 @@ defmodule Ecto.Adapters.SQLite3.CodecTest do assert {:ok, ^dt} = Codec.utc_datetime_decode("2021-08-25 10:58:59.111111+02:30") end end + + describe ".utc_datetime_encode/2" do + setup do + [dt: ~U[2021-08-25 10:58:59Z]] + end + + test "iso8601", %{dt: dt} do + dt_str = "2021-08-25T10:58:59" + assert {:ok, ^dt_str} = Codec.utc_datetime_encode(dt, :iso8601) + end + + test ":text_datetime", %{dt: dt} do + dt_str = "2021-08-25 10:58:59" + assert {:ok, ^dt_str} = Codec.utc_datetime_encode(dt, :text_datetime) + end + + test "unknown datetime type", %{dt: dt} do + msg = + "expected datetime type to be either `:iso8601` or `:text_datetime`, but received `:whatsthis`" + + assert_raise ArgumentError, msg, fn -> + Codec.naive_datetime_encode(dt, :whatsthis) + end + end + end + + describe ".naive_datetime_encode/2" do + setup do + [dt: ~U[2021-08-25 10:58:59Z], dt_str: "2021-08-25T10:58:59"] + end + + test "iso8601", %{dt: dt} do + dt_str = "2021-08-25T10:58:59" + assert {:ok, ^dt_str} = Codec.naive_datetime_encode(dt, :iso8601) + end + + test ":text_datetime", %{dt: dt} do + dt_str = "2021-08-25 10:58:59" + assert {:ok, ^dt_str} = Codec.naive_datetime_encode(dt, :text_datetime) + end + + test "unknown datetime type", %{dt: dt} do + msg = + "expected datetime type to be either `:iso8601` or `:text_datetime`, but received `:whatsthis`" + + assert_raise ArgumentError, msg, fn -> + Codec.naive_datetime_encode(dt, :whatsthis) + end + end + end end diff --git a/test/ecto/integration/timestamps_test.exs b/test/ecto/integration/timestamps_test.exs index eccb131..3c01fa6 100644 --- a/test/ecto/integration/timestamps_test.exs +++ b/test/ecto/integration/timestamps_test.exs @@ -37,7 +37,28 @@ defmodule Ecto.Integration.TimestampsTest do end end + setup do + on_exit(fn -> Application.delete_env(:ecto_sqlite3, :datetime_type) end) + end + test "insert and fetch naive datetime" do + # iso8601 type + {:ok, user} = + %UserNaiveDatetime{} + |> UserNaiveDatetime.changeset(%{name: "Bob"}) + |> TestRepo.insert() + + user = + UserNaiveDatetime + |> select([u], u) + |> where([u], u.id == ^user.id) + |> TestRepo.one() + + assert user + + # text_datetime type + Application.put_env(:ecto_sqlite3, :datetime_type, :text_datetime) + {:ok, user} = %UserNaiveDatetime{} |> UserNaiveDatetime.changeset(%{name: "Bob"}) @@ -53,6 +74,15 @@ defmodule Ecto.Integration.TimestampsTest do end test "max of naive datetime" do + # iso8601 type + datetime = ~N[2014-01-16 20:26:51] + TestRepo.insert!(%UserNaiveDatetime{inserted_at: datetime}) + query = from(p in UserNaiveDatetime, select: max(p.inserted_at)) + assert [^datetime] = TestRepo.all(query) + + # text_datetime type + Application.put_env(:ecto_sqlite3, :datetime_type, :text_datetime) + datetime = ~N[2014-01-16 20:26:51] TestRepo.insert!(%UserNaiveDatetime{inserted_at: datetime}) query = from(p in UserNaiveDatetime, select: max(p.inserted_at)) @@ -60,6 +90,23 @@ defmodule Ecto.Integration.TimestampsTest do end test "insert and fetch utc datetime" do + # iso8601 type + {:ok, user} = + %UserUtcDatetime{} + |> UserUtcDatetime.changeset(%{name: "Bob"}) + |> TestRepo.insert() + + user = + UserUtcDatetime + |> select([u], u) + |> where([u], u.id == ^user.id) + |> TestRepo.one() + + assert user + + # text_datetime type + Application.put_env(:ecto_sqlite3, :datetime_type, :text_datetime) + {:ok, user} = %UserUtcDatetime{} |> UserUtcDatetime.changeset(%{name: "Bob"})