Skip to content

Commit cffa1f4

Browse files
committed
refactor(appengine): add custom DateTime type
default Ecto types only allow full second or microsecond precision, but cql timestamps use millisecond precision. To avoid precision errors, a new Ecto DateTime type has been created, which allows millisecond precision (althogh no check is made: Xandra handles that itself). this change reverts some changes made to the tests in b27c5a4, restoring the millisecond precision of the device status Signed-off-by: Francesco Noacco <francesco.noacco@secomind.com>
1 parent 36ce3b4 commit cffa1f4

File tree

3 files changed

+53
-8
lines changed

3 files changed

+53
-8
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.AppEngine.API.DateTime do
20+
@moduledoc """
21+
Ecto type for DateTimes with millisecond precision.
22+
"""
23+
use Ecto.Type
24+
25+
@type t :: DateTime.t()
26+
27+
@doc false
28+
def type, do: :utc_datetime_msec
29+
30+
@spec load(t() | any()) :: {:ok, t()} | :error
31+
def load(%DateTime{} = datetime), do: {:ok, datetime}
32+
def load(timestamp) when is_integer(timestamp), do: {:ok, DateTime.from_unix!(timestamp)}
33+
def load(_other), do: :error
34+
35+
# xandra accepts both integers and datetimes, it can do the job for us
36+
@spec dump(t() | any()) :: {:ok, t()} | :error
37+
def dump(%DateTime{} = datetime), do: {:ok, datetime}
38+
def dump(timestamp) when is_integer(timestamp), do: {:ok, timestamp}
39+
def dump(_other), do: :error
40+
41+
@spec cast(t() | any()) :: {:ok, t()} | :error
42+
def cast(datetime_or_timestamp_or_any), do: load(datetime_or_timestamp_or_any)
43+
end

apps/astarte_appengine_api/lib/astarte_appengine_api/devices/device.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
defmodule Astarte.AppEngine.API.Devices.Device do
2020
use TypedEctoSchema
21+
alias Astarte.AppEngine.API.DateTime, as: DateTimeMs
2122
alias Astarte.AppEngine.API.UUID
2223

2324
@primary_key {:device_id, UUID, autogenerate: false}
@@ -39,16 +40,16 @@ defmodule Astarte.AppEngine.API.Devices.Device do
3940
types: [:string, :integer],
4041
value: :integer
4142

42-
field :first_credentials_request, :utc_datetime_usec
43-
field :first_registration, :utc_datetime_usec
43+
field :first_credentials_request, DateTimeMs
44+
field :first_registration, DateTimeMs
4445
field :groups, Exandra.Map, key: :string, value: UUID
4546
field :inhibit_credentials_request, :boolean
4647
field :introspection, Exandra.Map, key: :string, value: :integer
4748
field :introspection_minor, Exandra.Map, key: :string, value: :integer
48-
field :last_connection, :utc_datetime_usec
49+
field :last_connection, DateTimeMs
4950
field :last_credentials_request_ip, Exandra.Inet
5051

51-
field :last_disconnection, :utc_datetime_usec
52+
field :last_disconnection, DateTimeMs
5253
field :last_seen_ip, Exandra.Inet
5354

5455
field :old_introspection,

apps/astarte_appengine_api/lib/astarte_appengine_api/realms/individual_property.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,32 @@
1818

1919
defmodule Astarte.AppEngine.API.Realms.IndividualProperty do
2020
use TypedEctoSchema
21+
alias Astarte.AppEngine.API.DateTime, as: DateTimeMs
2122
alias Astarte.AppEngine.API.UUID
2223

2324
@primary_key false
2425
typed_schema "individual_properties" do
25-
field :reception, :utc_datetime_usec, virtual: true
26+
field :reception, DateTimeMs, virtual: true
2627
field :device_id, UUID, primary_key: true
2728
field :interface_id, UUID, primary_key: true
2829
field :endpoint_id, UUID, primary_key: true
2930
field :path, :string, primary_key: true
30-
field :reception_timestamp, :utc_datetime_usec
31+
field :reception_timestamp, DateTimeMs
3132
field :reception_timestamp_submillis, :integer
3233
field :double_value, :float
3334
field :integer_value, :integer
3435
field :boolean_value, :boolean
3536
field :longinteger_value, :integer
3637
field :string_value, :string
3738
field :binaryblob_value, :binary
38-
field :datetime_value, :utc_datetime_usec
39+
field :datetime_value, DateTimeMs
3940
field :doublearray_value, {:array, :float}
4041
field :integerarray_value, {:array, :integer}
4142
field :booleanarray_value, {:array, :boolean}
4243
field :longintegerarray_value, {:array, :integer}
4344
field :stringarray_value, {:array, :string}
4445
field :binaryblobarray_value, {:array, :binary}
45-
field :datetimearray_value, {:array, :utc_datetime_usec}
46+
field :datetimearray_value, {:array, DateTimeMs}
4647
end
4748

4849
def reception(individual_property) do

0 commit comments

Comments
 (0)