Skip to content

Commit e0d10ef

Browse files
greg-rychlewskiGreg Rychlewski
and
Greg Rychlewski
authored
Configurable datetime type (#84)
Co-authored-by: Greg Rychlewski <[email protected]>
1 parent d5cdfcd commit e0d10ef

File tree

4 files changed

+132
-7
lines changed

4 files changed

+132
-7
lines changed

lib/ecto/adapters/sqlite3.ex

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ defmodule Ecto.Adapters.SQLite3 do
4848
`:binary_id` columns. See the [section on binary ID types](#module-binary-id-types) for more details.
4949
* `:uuid_type` - Defaults to `:string`. Determines the type of `:uuid` columns. Possible values and
5050
column types are the same as for [binary IDs](#module-binary-id-types).
51+
* `:datetime_type` - Defaults to `:iso8601`. Determines how datetime fields are stored in the database.
52+
The allowed values are `:iso8601` and `:text_datetime`. `:iso8601` corresponds to a string of the form
53+
`YYYY-MM-DDThh:mm:ss` and `:text_datetime` corresponds to a string of the form YYYY-MM-DD hh:mm:ss`
5154
5255
For more information about the options above, see [sqlite documentation][1]
5356
@@ -259,6 +262,8 @@ defmodule Ecto.Adapters.SQLite3 do
259262
## Loaders
260263
##
261264

265+
@default_datetime_type :iso8601
266+
262267
@impl Ecto.Adapter
263268
def loaders(:boolean, type) do
264269
[&Codec.bool_decode/1, type]
@@ -390,22 +395,26 @@ defmodule Ecto.Adapters.SQLite3 do
390395

391396
@impl Ecto.Adapter
392397
def dumpers(:utc_datetime, type) do
393-
[type, &Codec.utc_datetime_encode/1]
398+
dt_type = Application.get_env(:ecto_sqlite3, :datetime_type, @default_datetime_type)
399+
[type, &Codec.utc_datetime_encode(&1, dt_type)]
394400
end
395401

396402
@impl Ecto.Adapter
397403
def dumpers(:utc_datetime_usec, type) do
398-
[type, &Codec.utc_datetime_encode/1]
404+
dt_type = Application.get_env(:ecto_sqlite3, :datetime_type, @default_datetime_type)
405+
[type, &Codec.utc_datetime_encode(&1, dt_type)]
399406
end
400407

401408
@impl Ecto.Adapter
402409
def dumpers(:naive_datetime, type) do
403-
[type, &Codec.naive_datetime_encode/1]
410+
dt_type = Application.get_env(:ecto_sqlite3, :datetime_type, @default_datetime_type)
411+
[type, &Codec.naive_datetime_encode(&1, dt_type)]
404412
end
405413

406414
@impl Ecto.Adapter
407415
def dumpers(:naive_datetime_usec, type) do
408-
[type, &Codec.naive_datetime_encode/1]
416+
dt_type = Application.get_env(:ecto_sqlite3, :datetime_type, @default_datetime_type)
417+
[type, &Codec.naive_datetime_encode(&1, dt_type)]
409418
end
410419

411420
@impl Ecto.Adapter

lib/ecto/adapters/sqlite3/codec.ex

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,31 @@ defmodule Ecto.Adapters.SQLite3.Codec do
9898
{:ok, value}
9999
end
100100

101-
# Ecto does check this already, so there should be no need to handle errors
102-
def utc_datetime_encode(%{time_zone: "Etc/UTC"} = value) do
101+
@text_datetime_format "%Y-%m-%d %H:%M:%S"
102+
103+
def utc_datetime_encode(%{time_zone: "Etc/UTC"} = value, :iso8601) do
103104
{:ok, NaiveDateTime.to_iso8601(value)}
104105
end
105106

106-
def naive_datetime_encode(value) do
107+
def utc_datetime_encode(%{time_zone: "Etc/UTC"} = value, :text_datetime) do
108+
{:ok, Calendar.strftime(value, @text_datetime_format)}
109+
end
110+
111+
def utc_datetime_encode(%{time_zone: "Etc/UTC"}, type) do
112+
raise ArgumentError,
113+
"expected datetime type to be either `:iso8601` or `:text_datetime`, but received #{inspect(type)}"
114+
end
115+
116+
def naive_datetime_encode(value, :iso8601) do
107117
{:ok, NaiveDateTime.to_iso8601(value)}
108118
end
119+
120+
def naive_datetime_encode(value, :text_datetime) do
121+
{:ok, Calendar.strftime(value, @text_datetime_format)}
122+
end
123+
124+
def naive_datetime_encode(_value, type) do
125+
raise ArgumentError,
126+
"expected datetime type to be either `:iso8601` or `:text_datetime`, but received `#{inspect(type)}`"
127+
end
109128
end

test/ecto/adapters/sqlite3/codec_test.exs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,54 @@ defmodule Ecto.Adapters.SQLite3.CodecTest do
126126
assert {:ok, ^dt} = Codec.utc_datetime_decode("2021-08-25 10:58:59.111111+02:30")
127127
end
128128
end
129+
130+
describe ".utc_datetime_encode/2" do
131+
setup do
132+
[dt: ~U[2021-08-25 10:58:59Z]]
133+
end
134+
135+
test "iso8601", %{dt: dt} do
136+
dt_str = "2021-08-25T10:58:59"
137+
assert {:ok, ^dt_str} = Codec.utc_datetime_encode(dt, :iso8601)
138+
end
139+
140+
test ":text_datetime", %{dt: dt} do
141+
dt_str = "2021-08-25 10:58:59"
142+
assert {:ok, ^dt_str} = Codec.utc_datetime_encode(dt, :text_datetime)
143+
end
144+
145+
test "unknown datetime type", %{dt: dt} do
146+
msg =
147+
"expected datetime type to be either `:iso8601` or `:text_datetime`, but received `:whatsthis`"
148+
149+
assert_raise ArgumentError, msg, fn ->
150+
Codec.naive_datetime_encode(dt, :whatsthis)
151+
end
152+
end
153+
end
154+
155+
describe ".naive_datetime_encode/2" do
156+
setup do
157+
[dt: ~U[2021-08-25 10:58:59Z], dt_str: "2021-08-25T10:58:59"]
158+
end
159+
160+
test "iso8601", %{dt: dt} do
161+
dt_str = "2021-08-25T10:58:59"
162+
assert {:ok, ^dt_str} = Codec.naive_datetime_encode(dt, :iso8601)
163+
end
164+
165+
test ":text_datetime", %{dt: dt} do
166+
dt_str = "2021-08-25 10:58:59"
167+
assert {:ok, ^dt_str} = Codec.naive_datetime_encode(dt, :text_datetime)
168+
end
169+
170+
test "unknown datetime type", %{dt: dt} do
171+
msg =
172+
"expected datetime type to be either `:iso8601` or `:text_datetime`, but received `:whatsthis`"
173+
174+
assert_raise ArgumentError, msg, fn ->
175+
Codec.naive_datetime_encode(dt, :whatsthis)
176+
end
177+
end
178+
end
129179
end

test/ecto/integration/timestamps_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,28 @@ defmodule Ecto.Integration.TimestampsTest do
3737
end
3838
end
3939

40+
setup do
41+
on_exit(fn -> Application.delete_env(:ecto_sqlite3, :datetime_type) end)
42+
end
43+
4044
test "insert and fetch naive datetime" do
45+
# iso8601 type
46+
{:ok, user} =
47+
%UserNaiveDatetime{}
48+
|> UserNaiveDatetime.changeset(%{name: "Bob"})
49+
|> TestRepo.insert()
50+
51+
user =
52+
UserNaiveDatetime
53+
|> select([u], u)
54+
|> where([u], u.id == ^user.id)
55+
|> TestRepo.one()
56+
57+
assert user
58+
59+
# text_datetime type
60+
Application.put_env(:ecto_sqlite3, :datetime_type, :text_datetime)
61+
4162
{:ok, user} =
4263
%UserNaiveDatetime{}
4364
|> UserNaiveDatetime.changeset(%{name: "Bob"})
@@ -53,13 +74,39 @@ defmodule Ecto.Integration.TimestampsTest do
5374
end
5475

5576
test "max of naive datetime" do
77+
# iso8601 type
78+
datetime = ~N[2014-01-16 20:26:51]
79+
TestRepo.insert!(%UserNaiveDatetime{inserted_at: datetime})
80+
query = from(p in UserNaiveDatetime, select: max(p.inserted_at))
81+
assert [^datetime] = TestRepo.all(query)
82+
83+
# text_datetime type
84+
Application.put_env(:ecto_sqlite3, :datetime_type, :text_datetime)
85+
5686
datetime = ~N[2014-01-16 20:26:51]
5787
TestRepo.insert!(%UserNaiveDatetime{inserted_at: datetime})
5888
query = from(p in UserNaiveDatetime, select: max(p.inserted_at))
5989
assert [^datetime] = TestRepo.all(query)
6090
end
6191

6292
test "insert and fetch utc datetime" do
93+
# iso8601 type
94+
{:ok, user} =
95+
%UserUtcDatetime{}
96+
|> UserUtcDatetime.changeset(%{name: "Bob"})
97+
|> TestRepo.insert()
98+
99+
user =
100+
UserUtcDatetime
101+
|> select([u], u)
102+
|> where([u], u.id == ^user.id)
103+
|> TestRepo.one()
104+
105+
assert user
106+
107+
# text_datetime type
108+
Application.put_env(:ecto_sqlite3, :datetime_type, :text_datetime)
109+
63110
{:ok, user} =
64111
%UserUtcDatetime{}
65112
|> UserUtcDatetime.changeset(%{name: "Bob"})

0 commit comments

Comments
 (0)