| defmodule Plausible.Stats.SQL.Fragments do |
| @moduledoc """ |
| Various macros and common SQL fragments used in Stats code. |
| """ |
|
|
| defmacro __using__(_) do |
| quote do |
| import Plausible.Stats.SQL.Fragments |
| require Plausible.Stats.SQL.Fragments |
| end |
| end |
|
|
| defmacro scale_sample(sampled_fragment) do |
| quote do |
| fragment("toUInt64(round(? * any(_sample_factor)))", unquote(sampled_fragment)) |
| end |
| end |
|
|
| defmacro uniq(user_id) do |
| quote do |
| scale_sample(fragment("uniq(?)", unquote(user_id))) |
| end |
| end |
|
|
| defmacro total() do |
| quote do |
| scale_sample(fragment("count()")) |
| end |
| end |
|
|
| defmacro sample_percent() do |
| quote do |
| fragment("if(any(_sample_factor) > 1, round(100 / any(_sample_factor)), 100)") |
| end |
| end |
|
|
| defmacro bounce_rate() do |
| quote do |
| fragment( |
| |
| |
| |
| "toUInt32(greatest(ifNotFinite(round(sum(is_bounce * sign) / sum(sign) * 100), 0), 0))" |
| ) |
| end |
| end |
|
|
| defmacro visit_duration() do |
| quote do |
| fragment("toUInt32(ifNotFinite(round(avg(duration * sign)), 0))") |
| end |
| end |
|
|
| defmacro coalesce_string(fieldA, fieldB) do |
| quote do |
| fragment("if(empty(?), ?, ?)", unquote(fieldA), unquote(fieldB), unquote(fieldA)) |
| end |
| end |
|
|
| @doc """ |
| Converts time or date and time to the specified timezone. |
| |
| Reference: https://clickhouse.com/docs/en/sql-reference/functions/date-time-functions/#totimezone |
| """ |
| defmacro to_timezone(date, timezone) do |
| quote do |
| fragment("toTimeZone(?, ?)", unquote(date), unquote(timezone)) |
| end |
| end |
|
|
| @doc """ |
| Returns the weekstart for `date`. If the weekstart is before the `not_before` |
| boundary, `not_before` is returned. |
| |
| ## Examples |
| |
| In this pseudo-code example, the fragment returns the weekstart. The |
| `not_before` boundary is set to the past Saturday, which is before the |
| weekstart, therefore the cap does not apply. |
| |
| ``` |
| > this_wednesday = ~D[2022-11-09] |
| > past_saturday = ~D[2022-11-05] |
| > weekstart_not_before(this_wednesday, past_saturday) |
| ~D[2022-11-07] |
| ``` |
| |
| In this other example, the fragment returns Tuesday and not the weekstart. |
| The `not_before` boundary is set to Tuesday, which is past the weekstart, |
| therefore the cap applies. |
| |
| ``` |
| > this_wednesday = ~D[2022-11-09] |
| > this_tuesday = ~D[2022-11-08] |
| > weekstart_not_before(this_wednesday, this_tuesday) |
| ~D[2022-11-08] |
| ``` |
| """ |
| defmacro weekstart_not_before(date, not_before) do |
| quote do |
| fragment( |
| "if(toMonday(?) < toDate(?), toDate(?), toMonday(?))", |
| unquote(date), |
| unquote(not_before), |
| unquote(not_before), |
| unquote(date) |
| ) |
| end |
| end |
|
|
| @doc """ |
| Returns whether a key (usually property) exists under `meta.key` array or similar. |
| |
| This macro is used for operating on custom properties. |
| |
| ## Examples |
| |
| `has_key(e, :meta, "some_property_name")` expands to SQL `has(meta.key, "some_property_name")` |
| """ |
| defmacro has_key(table, meta_column, key) do |
| quote do |
| fragment( |
| "has(?, ?)", |
| field(unquote(table), ^meta_key_column(unquote(meta_column))), |
| unquote(key) |
| ) |
| end |
| end |
|
|
| @doc """ |
| Returns value of a key (usually property) under `meta.value` array or similar. |
| |
| This macro is used for operating on custom properties. |
| Callsites should also check whether key exists first in SQL via `has_key` macro. |
| |
| ## Examples |
| |
| `get_by_key(e, :meta, "some_property_name")` expands to SQL `meta.value[indexOf(meta.key, "some_property")]` |
| """ |
| defmacro get_by_key(table, meta_column, key) do |
| quote do |
| fragment( |
| "?[indexOf(?, ?)]", |
| field(unquote(table), ^meta_value_column(unquote(meta_column))), |
| field(unquote(table), ^meta_key_column(unquote(meta_column))), |
| unquote(key) |
| ) |
| end |
| end |
|
|
| def meta_key_column(:meta), do: :"meta.key" |
| def meta_key_column(:entry_meta), do: :"entry_meta.key" |
|
|
| def meta_value_column(:meta), do: :"meta.value" |
| def meta_value_column(:entry_meta), do: :"entry_meta.value" |
|
|
| defmacro time_on_page(total_time_on_page, total_time_on_page_visits) do |
| quote do |
| fragment( |
| "if(? > 0, toInt32(round((?) / (?))), NULL)", |
| unquote(total_time_on_page_visits), |
| unquote(total_time_on_page), |
| unquote(total_time_on_page_visits) |
| ) |
| end |
| end |
|
|
| @doc """ |
| Convenience Ecto macro for wrapping a map passed to select_merge_as such that each |
| expression gets wrapped in dynamic and set as selected_as. |
| |
| ### Examples |
| |
| iex> wrap_alias([t], %{ foo: t.column }) |> expand_macro_once |
| "%{foo: dynamic([t], selected_as(t.column, :foo))}" |
| """ |
| defmacro wrap_alias(binding, map_literal) do |
| update_literal_map_values(map_literal, fn {key, expr} -> |
| key_expr = |
| if Macro.quoted_literal?(key) do |
| key |
| else |
| quote(do: ^unquote(key)) |
| end |
|
|
| quote(do: dynamic(unquote(binding), selected_as(unquote(expr), unquote(key_expr)))) |
| end) |
| end |
|
|
| @doc """ |
| Convenience Ecto macro for wrapping select_merge where each value gets in turn passed to selected_as. |
| |
| ### Examples |
| |
| iex> select_merge_as(q, [t], %{ foo: t.column }) |> expand_macro_once |
| "select_merge(q, [], ^wrap_alias([t], %{foo: t.column}))" |
| """ |
| defmacro select_merge_as(q, binding, map_literal) do |
| quote do |
| select_merge(unquote(q), [], ^wrap_alias(unquote(binding), unquote(map_literal))) |
| end |
| end |
|
|
| @doc """ |
| Macro that helps join two Ecto queries by selecting fields from either one |
| """ |
| defmacro select_join_fields(q, query, list, table_name) do |
| quote do |
| Enum.reduce(unquote(list), unquote(q), fn metric_or_dimension, q -> |
| key = shortname(unquote(query), metric_or_dimension) |
|
|
| select_merge_as(q, [e, s], %{ |
| key => field(unquote(table_name), ^key) |
| }) |
| end) |
| end |
| end |
|
|
| defp update_literal_map_values({:%{}, ctx, keyword_list}, mapper_fn) do |
| { |
| :%{}, |
| ctx, |
| Enum.map(keyword_list, fn {key, expr} -> |
| {key, mapper_fn.({key, expr})} |
| end) |
| } |
| end |
|
|
| defp update_literal_map_values(ast, _), do: ast |
| end |
|
|