Skip to content

configurable datetime type #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions lib/ecto/adapters/sqlite3.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
25 changes: 22 additions & 3 deletions lib/ecto/adapters/sqlite3/codec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
50 changes: 50 additions & 0 deletions test/ecto/adapters/sqlite3/codec_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
47 changes: 47 additions & 0 deletions test/ecto/integration/timestamps_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand All @@ -53,13 +74,39 @@ 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))
assert [^datetime] = TestRepo.all(query)
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"})
Expand Down