Current section
Files
Jump to
Current section
Files
lib/ecto/time_series_query.ex
defmodule Fast.Ecto.TimeSeriesQuery do
# -- Imports
import Ecto.Query
@default_time_zone_name "US/Pacific"
def time_series_counts_query(queryable, opts) when is_list(opts) do
start = Access.fetch!(opts, :start)
finish = Access.fetch!(opts, :finish)
granularity = unit_for_range(start, finish)
case granularity do
:hourly -> hourly_counts_query(queryable, opts)
:daily -> daily_counts_query(queryable, opts)
:weekly -> weekly_counts_query(queryable, opts)
:monthly -> monthly_counts_query(queryable, opts)
end
end
def hourly_counts_query(queryable, opts) do
field_name = Access.get(opts, :field, :inserted_at)
start = Access.fetch!(opts, :start)
finish = Access.fetch!(opts, :finish)
time_zone_name = Access.get(opts, :time_zone_name, @default_time_zone_name)
iana_time_zone_name = Tzdata.links() |> Map.get(time_zone_name)
from(row in queryable,
where:
is_nil(field(row, ^field_name)) or
(field(row, ^field_name) >= ^start and field(row, ^field_name) < ^finish),
right_join:
h in fragment(
"select generate_series(
date_trunc('day', (?)::timestamp AT TIME ZONE 'UTC' AT TIME ZONE (?))::timestamptz,
date_trunc('day', (?)::timestamp AT TIME ZONE 'UTC' AT TIME ZONE (?))::timestamptz,
'1 hour'::interval
)::timestamp as hour",
^start,
^iana_time_zone_name,
^finish,
^iana_time_zone_name
),
on:
h.hour ==
fragment(
"to_timestamp(to_char(?, 'YYYY-MM-DD HH24'), 'YYYY-MM-DD HH24')",
field(row, ^field_name)
),
group_by: h.hour,
order_by: h.hour,
select: %{datetime: h.hour, value: count(row.id)}
)
end
def daily_counts_query(queryable, opts) do
field_name = Access.get(opts, :field, :inserted_at)
start = Access.fetch!(opts, :start) |> Timex.to_datetime("Etc/UTC")
finish = Access.fetch!(opts, :finish) |> Timex.to_datetime("Etc/UTC")
time_zone_name = Access.get(opts, :time_zone_name, @default_time_zone_name)
iana_time_zone_name = Tzdata.links() |> Map.get(time_zone_name)
from(row in queryable,
where:
is_nil(field(row, ^field_name)) or
(field(row, ^field_name) >= ^start and field(row, ^field_name) < ^finish),
right_join:
d in fragment(
"select generate_series(
date_trunc('day', (?)::timestamp AT TIME ZONE 'UTC' AT TIME ZONE (?))::timestamp,
date_trunc('day', (?)::timestamp AT TIME ZONE 'UTC' AT TIME ZONE (?))::timestamp,
'1 day'::interval
) as day",
^start,
^iana_time_zone_name,
^finish,
^iana_time_zone_name
),
on:
d.day ==
fragment(
"date_trunc('day', (?)::timestamp AT TIME ZONE 'UTC' AT TIME ZONE (?))::timestamp",
field(row, ^field_name),
^iana_time_zone_name
),
group_by: d.day,
order_by: d.day,
select: %{datetime: d.day, value: count(row.id)}
)
end
def weekly_counts_query(queryable, opts) do
field_name = Access.get(opts, :field, :inserted_at)
start = Access.fetch!(opts, :start)
finish = Access.fetch!(opts, :finish)
from(row in queryable,
where:
is_nil(field(row, ^field_name)) or
(field(row, ^field_name) >= ^start and field(row, ^field_name) < ^finish),
right_join:
w in fragment(
"""
select generate_series(
date_trunc('week', (?)::timestamp AT TIME ZONE 'America/Los_Angeles'),
date_trunc('week', (?)::timestamp AT TIME ZONE 'America/Los_Angeles'),
'1 week'::interval
) as week
""",
fragment("(?)::timestamp AT TIME ZONE 'UTC'", ^start),
fragment("(?)::timestamp AT TIME ZONE 'UTC'", ^finish)
),
on:
w.week ==
fragment(
"date_trunc('week', (?)::timestamp AT TIME ZONE 'America/Los_Angeles')",
field(row, ^field_name)
),
group_by: w.week,
order_by: w.week,
select: %{datetime: w.week, value: count(row.id)}
)
end
def monthly_counts_query(queryable, opts) do
field_name = Access.get(opts, :field, :inserted_at)
start = Access.fetch!(opts, :start)
finish = Access.fetch!(opts, :finish)
from(row in queryable,
where:
is_nil(field(row, ^field_name)) or
(field(row, ^field_name) >= ^start and field(row, ^field_name) < ^finish),
right_join:
m in fragment(
"""
select generate_series(
date_trunc('month', (?)::timestamp AT TIME ZONE 'America/Los_Angeles'),
date_trunc('month', (?)::timestamp AT TIME ZONE 'America/Los_Angeles'),
'1 month'::interval
) as month
""",
fragment("(?)::timestamp AT TIME ZONE 'UTC'", ^start),
fragment("(?)::timestamp AT TIME ZONE 'UTC'", ^finish)
),
on:
m.month ==
fragment(
"date_trunc('month', (?)::timestamp AT TIME ZONE 'America/Los_Angeles')",
field(row, ^field_name)
),
group_by: m.month,
order_by: m.month,
select: %{datetime: m.month, value: count(row.id)}
)
end
def unit_for_range(%DateTime{} = start, %DateTime{} = finish) do
unit_for_range({start, finish})
end
def unit_for_range(%{start: %DateTime{} = start, finish: %DateTime{} = finish}) do
unit_for_range({start, finish})
end
def unit_for_range({%DateTime{} = start, %DateTime{} = finish}) do
num_days = abs(Timex.diff(finish, start, :days))
cond do
# Less than 3 days -> hourly
num_days in 0..3 ->
:hourly
# Between 3 and 90 days -> daily
num_days in 4..90 ->
:daily
# Between 90 and 180 days -> weekly
num_days in 91..180 ->
:weekly
# Over 180 days -> monthly
num_days > 180 ->
:monthly
end
end
end