fix MatchError in decode_datetimeoffset#111
Conversation
I've been trying to get `datetimeoffset` columns working but kept getting this error when retrieving data:
``` elixir
iex(7)> Repo.all(DTO3)
[debug] QUERY ERROR source="dto3" db=16.0ms idle=890.0ms
SELECT d0.[id], d0.[dto3] FROM [dto3] AS d0 []
** (MatchError) no match of right hand side value: {:ok, ~U[2020-08-17 23:53:07.075200Z], 36000}
(ecto_sql 3.4.5) lib/ecto/adapters/sql.ex:593: Ecto.Adapters.SQL.raise_sql_call_error/1
(ecto_sql 3.4.5) lib/ecto/adapters/sql.ex:526: Ecto.Adapters.SQL.execute/5
(ecto 3.4.6) lib/ecto/repo/queryable.ex:192: Ecto.Repo.Queryable.execute/4
(ecto 3.4.6) lib/ecto/repo/queryable.ex:17: Ecto.Repo.Queryable.all/3
```
Not the most accurate stack trace. One must descend the ladder of functions to end up at [the root cause](https://github.com/livehelpnow/tds/blob/ecd77026a69b45c25e562e11bc950e4095664966/lib/tds/types.ex#L1751), that `{:ok, datetime, ^offset_min} = DateTime.from_iso8601("#{str}+#{h}:#{m}")` is trying to match seconds (what [from_iso860](https://hexdocs.pm/elixir/DateTime.html#from_iso8601/2) returns) and minutes. It needs to be:
``` elixir
offset = offset_min * 60
{:ok, datetime, ^offset} = DateTime.from_iso8601("#{str}+#{h}:#{m}")`
|
It's confusing or a bug that you need to dump to the database using offset = offset_min * 60
:ok, datetime, ^offset} = DateTime.from_iso8601("#{str}+#{h}:#{m}")
DateTime.add(datetime, offset)Migrationdefmodule Datetimeoffset.Repo.Migrations.CreateDto3Table do
use Ecto.Migration
def change do
create table(:dto3) do
add :dto3, :datetimeoffset
end
end
endSchemadefmodule Datetimeoffset.DTO3 do
use Ecto.Schema
alias Types.DateTimeOffset
schema "dto3" do
field :dto3, DateTimeOffset
end
endCustom Typedefmodule Types.DateTimeOffset do
use Ecto.Type
use Timex
require Logger
@doc """
type returned to Schema
"""
def type, do: :datetimeoffset
@doc """
cast to DateTime to be used at runtime
TODO: add other casts...
"""
def cast(%DateTime{} = dt) do
Logger.debug(fn ->
"casting DateTime: #{inspect(dt)}"
end)
{:ok, dt}
end
@doc """
handle loading data from the database
( db ) --> load()
"""
def load({%DateTime{} = dt, offset_mins}) do
Logger.debug(fn ->
"loading tuple of DateTime and offset" <>
"{#{inspect(dt)}, #{offset_mins}}"
end)
{:ok, dt}
end
def load(%DateTime{} = dt) do
Logger.debug(fn ->
"loading DateTime " <>
"#{inspect(dt)}"
end)
{:ok, dt}
end
def load({{y, mth, d}, {h, min, s}, offset_mins}) do
Logger.debug(fn ->
"loading tuple format without microseconds supplied."
end)
load({{y, mth, d}, {h, min, s, 0}, offset_mins})
end
def load({{y, mth, d}, {h, min, s, us}, offset_mins}) do
Logger.debug(fn ->
"loading tuple format " <>
"{{#{y}, #{mth}, #{d}}, {#{h}, #{min}, #{s}, #{us}}, #{offset_mins}}"
end)
secs = :calendar.datetime_to_gregorian_seconds({{y, mth, d}, {h, min, s}})
tzname = Timezone.name_of(offset_mins * 60)
case Timezone.resolve(tzname, secs) do
{:error, _} ->
:error
%TimezoneInfo{} = tz ->
dt = %DateTime{
:year => y,
:month => mth,
:day => d,
:hour => h,
:minute => min,
:second => s,
:microsecond => Timex.DateTime.Helpers.construct_microseconds({us, -1}),
:time_zone => tz.full_name,
:zone_abbr => tz.abbreviation,
:utc_offset => tz.offset_utc,
:std_offset => tz.offset_std
}
{:ok, dt}
%AmbiguousTimezoneInfo{after: a} ->
dt = %DateTime{
:year => y,
:month => mth,
:day => d,
:hour => h,
:minute => min,
:second => s,
:microsecond => Timex.DateTime.Helpers.construct_microseconds({us, -1}),
:time_zone => a.full_name,
:zone_abbr => a.abbreviation,
:utc_offset => a.offset_utc,
:std_offset => a.offset_std
}
{:ok, dt}
end
end
def load(_) do
:error
end
@doc """
convert to native ecto representation
dump() -> ( db )
"""
def dump(%DateTime{utc_offset: utc_offset} = dt) do
%DateTime{
year: y,
month: mth,
day: d,
hour: h,
minute: min,
second: s,
microsecond: {us, _}
} = Timex.to_datetime(dt)
offset_mins = div(utc_offset, 60)
Logger.debug(fn ->
"dumping #{inspect(dt)} to database as " <>
"{{#{y}, #{mth}, #{d}}, {#{h}, #{min}, #{s}, #{us}}, #{offset_mins}}"
end)
{:ok, {{y, mth, d}, {h, min, s, us}, offset_mins}}
end
endIExiex> now = Timex.local
iex> dto3 = %Datetimeoffset.DTO3{dto3: now}
iex> Datetimeoffset.Repo.insert(dto3)
iex> Datetimeoffset.Repo.all(Datetimeoffset.DTO3) |
|
Hi @simonmcconnell, I will check PR tomorrow, and if it is all good I will make new release. Thanks! |
|
I'll add some tests specific to datetimeoffset and a custom type as per uuid if you're happy with that? Will try get to it over the weekend. |
|
That would be awesome |
|
What is the story with the def encode_datetime({date, {h, m, s, us}}) do
days = :calendar.date_to_gregorian_days(date) - @year_1900_days
milliseconds = ((h * 60 + m) * 60 + s) * 1_000 + us / 1_000
secs_300 = round(milliseconds / (10 / 3))
{days, secs_300} =
if secs_300 == 25_920_000 do
{days + 1, 0}
else
{days, secs_300}
end
<<days::little-signed-32, secs_300::little-unsigned-32>>
enddef encode_time({hour, min, sec, fsec}, scale) do
# 10^scale fs in 1 sec
fs_per_sec = trunc(:math.pow(10, scale))
fsec =
hour * 3600 * fs_per_sec + min * 60 * fs_per_sec + sec * fs_per_sec + fsec
bin =
cond do
scale < 3 ->
<<fsec::little-unsigned-24>>
scale < 5 ->
<<fsec::little-unsigned-32>>
:else ->
<<fsec::little-unsigned-40>>
end
{bin, scale}
end |
|
|
|
Sorry about the commit mess, I'm not well versed in git yet 🤭. I believe I've fixed the encode/decode issue for Added a This type might need the |
|
@mjaric have you had a chance to go over this? |
|
Yes, except bug fix, I think we need to create new mix lib project for Another reason that third-party dependencies are not so well fit in this case is that this library is also dependency in Now, I don't want your work to vanish just because I said this repo is not good place for this. There is potential especially because you added full precision that sql server can support and builtin elixir types lack of. It would be best to create new github repository and move custom types there and we can add a note in README referencing that repository for those who may concern. If possible, try not to use So action plan is that you move custom type in separate repo and we can merge offset handling fix. Don't forget to put in the readme file reference to the new repo or hex, it is valuable info. |
|
Ok, will do. I think if someone wants to use |
|
bug fix moved to #114 |
I've been trying to get
datetimeoffsetcolumns working but kept getting this error when retrieving data:Not the most accurate stack trace. One must descend the ladder of functions to end up at the root cause, that
{:ok, datetime, ^offset_min} = DateTime.from_iso8601("#{str}+#{h}:#{m}")is trying to match seconds (what from_iso860 returns) and minutes. It needs to be: