Skip to content

Instantly share code, notes, and snippets.

@kevinkoltz
Created April 13, 2019 00:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kevinkoltz/3b863932553843a7a7c8054b033d72de to your computer and use it in GitHub Desktop.
Save kevinkoltz/3b863932553843a7a7c8054b033d72de to your computer and use it in GitHub Desktop.
Datetime2 Type for Ecto
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