Created
April 13, 2019 00:29
-
-
Save kevinkoltz/3b863932553843a7a7c8054b033d72de to your computer and use it in GitHub Desktop.
Datetime2 Type for Ecto
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Ecto.Adapter.MSSQL.DateTime2 do | |
@moduledoc """ | |
Temporary solution to fix microsecond precision in MSSQL UTC datetime2 | |
values. MSSQL defaults to 7-digit microseconds, whereas `Calendar` only | |
supports 6. | |
Note: Ecto 3.0 will remove the microseconds by default with `:utc_datetime`. | |
See https://github.com/livehelpnow/tds_ecto/issues/65 for more info. | |
""" | |
@behaviour Ecto.Type | |
@max_usec_precision 6 | |
def type, do: :utc_datetime_usec | |
def cast(%DateTime{time_zone: "Etc/UTC"} = datetime), do: {:ok, datetime} | |
def cast(_value), do: :error | |
@doc "Load from database, fixes microseconds by ensuring theyre 6-digits" | |
@spec load(any()) :: {:ok, DateTime.t()} | |
def load(%DateTime{} = datetime) do | |
{:ok, fix_usec_precision(datetime)} | |
end | |
def load(%NaiveDateTime{} = naive_datetime) do | |
naive_datetime |> fix_usec_precision() |> DateTime.from_naive("Etc/UTC") | |
end | |
def load({{year, month, day}, {hour, minute, second, microsecond}}) do | |
%DateTime{ | |
year: year, | |
month: month, | |
day: day, | |
hour: hour, | |
minute: minute, | |
second: second, | |
microsecond: {microsecond, 6}, | |
utc_offset: 0, | |
std_offset: 0, | |
time_zone: "Etc/UTC", | |
zone_abbr: "UTC" | |
} | |
|> fix_usec_precision() | |
|> load() | |
end | |
def dump(%DateTime{time_zone: time_zone} = datetime) do | |
if time_zone != "Etc/UTC" do | |
message = | |
":utc_datetime_usec expects the time zone to be \"Etc/UTC\", got `#{inspect(datetime)}`" | |
raise ArgumentError, message | |
end | |
fixed_datetime = | |
datetime | |
|> fix_usec_precision() | |
# tds_ecto expects the following format | |
{ | |
:ok, | |
{ | |
{fixed_datetime.year, fixed_datetime.month, fixed_datetime.day}, | |
{ | |
fixed_datetime.hour, | |
fixed_datetime.minute, | |
fixed_datetime.second, | |
elem(fixed_datetime.microsecond, 0) | |
} | |
} | |
} | |
end | |
defp fix_usec_precision(%{microsecond: {microsecond, _}} = struct) do | |
fixed_microsecond = | |
microsecond | |
|> to_string | |
|> String.split_at(@max_usec_precision) | |
|> elem(0) | |
|> String.to_integer() | |
%{struct | microsecond: {fixed_microsecond, @max_usec_precision}} | |
end | |
defp fix_usec_precision(datetime), do: datetime | |
# `Ecto.Repo.Schema.autogenerate_changes/3` expects this to be defined, not sure why. | |
def from_unix!(microseconds, :microseconds), | |
do: DateTime.from_unix!(microseconds, :microseconds) | |
end | |
defmodule Ecto.Adapter.MSSQL.DateTime2Test do | |
use ExUnit.Case | |
alias Ecto.Adapter.MSSQL.DateTime2, as: Type | |
setup do | |
{ | |
:ok, | |
mssql_datetime: %DateTime{ | |
year: 2018, | |
month: 8, | |
day: 17, | |
hour: 21, | |
minute: 34, | |
second: 14, | |
microsecond: {86_857_499_999_999_999, 6}, | |
time_zone: "Etc/UTC", | |
zone_abbr: "UTC", | |
utc_offset: 0, | |
std_offset: 0 | |
}, | |
datetime: %DateTime{ | |
year: 2018, | |
month: 8, | |
day: 17, | |
hour: 21, | |
minute: 34, | |
second: 14, | |
microsecond: {868_574, 6}, | |
time_zone: "Etc/UTC", | |
zone_abbr: "UTC", | |
utc_offset: 0, | |
std_offset: 0 | |
} | |
} | |
end | |
test "loads datetime", context do | |
assert {:ok, context.datetime} == Type.load(context.datetime) | |
end | |
test "dumps datetime while truncating microseconds", context do | |
assert {:ok, {{2018, 8, 17}, {21, 34, 14, 868_574}}} = Type.dump(context.mssql_datetime) | |
end | |
test "casts datetime", context do | |
assert {:ok, context.datetime} == Type.cast(context.datetime) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment