| defmodule Plausible.Stats.Query.QueryOptimizerTest do |
| use Plausible.DataCase, async: true |
|
|
| alias Plausible.Stats.{Query, QueryOptimizer, DateTimeRange, ApiQueryParser} |
|
|
| @default_params %{metrics: [:visitors]} |
|
|
| def perform(params) do |
| params = Map.merge(@default_params, params) |> Map.to_list() |
| struct!(Query, params) |> QueryOptimizer.optimize() |
| end |
|
|
| describe "add_missing_order_by" do |
| test "does nothing if order_by passed" do |
| assert perform(%{order_by: [visitors: :desc]}).order_by == [{:visitors, :desc}] |
| end |
|
|
| test "adds first metric to order_by if order_by not specified" do |
| assert perform(%{metrics: [:pageviews, :visitors]}).order_by == [{:pageviews, :desc}] |
|
|
| assert perform(%{metrics: [:pageviews, :visitors], dimensions: ["event:page"]}).order_by == |
| [{:pageviews, :desc}] |
| end |
|
|
| test "adds time and first metric to order_by if order_by not specified" do |
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-01-31], "UTC"), |
| metrics: [:pageviews, :visitors], |
| dimensions: ["time", "event:page"] |
| }).order_by == |
| [{"time:day", :asc}, {:pageviews, :desc}] |
| end |
| end |
|
|
| describe "update_group_by_time" do |
| test "does nothing if `time` dimension not passed" do |
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-01-04], "UTC"), |
| dimensions: ["time:month"] |
| }).dimensions == ["time:month"] |
| end |
|
|
| test "updating time dimension" do |
| assert perform(%{ |
| utc_time_range: |
| DateTimeRange.new!( |
| DateTime.new!(~D[2022-01-01], ~T[00:00:00], "UTC"), |
| DateTime.new!(~D[2022-01-01], ~T[05:00:00], "UTC") |
| ), |
| dimensions: ["time"] |
| }).dimensions == ["time:hour"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-01-01], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:hour"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-01-02], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:hour"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-01-03], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:day"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-01-10], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:day"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-01-16], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:day"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-02-16], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:week"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-03-16], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:week"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2022-03-16], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:week"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2023-11-16], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:month"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2024-01-16], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:month"] |
|
|
| assert perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2022-01-01], ~D[2026-01-01], "UTC"), |
| dimensions: ["time"] |
| }).dimensions == ["time:month"] |
| end |
| end |
|
|
| describe "update_time_in_order_by" do |
| test "updates explicit time dimension in order_by" do |
| assert perform(%{ |
| utc_time_range: |
| DateTimeRange.new!( |
| DateTime.new!(~D[2022-01-01], ~T[00:00:00], "UTC"), |
| DateTime.new!(~D[2022-01-01], ~T[05:00:00], "UTC") |
| ), |
| dimensions: ["time:hour"], |
| order_by: [{"time", :asc}] |
| }).order_by == [{"time:hour", :asc}] |
| end |
| end |
|
|
| describe "extend_hostname_filters_to_visit" do |
| test "updates filters it filtering by event:hostname and visit:referrer and visit:exit_page dimensions" do |
| assert perform(%{ |
| utc_time_range: Date.range(~N[2022-01-01 00:00:00], ~N[2022-01-01 05:00:00]), |
| filters: [ |
| [:is, "event:hostname", ["example.com"]], |
| [:matches_wildcard, "event:hostname", ["*.com"]] |
| ], |
| dimensions: ["visit:referrer", "visit:exit_page"] |
| }).filters == [ |
| [:is, "event:hostname", ["example.com"]], |
| [:matches_wildcard, "event:hostname", ["*.com"]], |
| [:is, "visit:entry_page_hostname", ["example.com"]], |
| [:matches_wildcard, "visit:entry_page_hostname", ["*.com"]], |
| [:is, "visit:exit_page_hostname", ["example.com"]], |
| [:matches_wildcard, "visit:exit_page_hostname", ["*.com"]] |
| ] |
| end |
|
|
| test "does not update filters if not needed" do |
| assert perform(%{ |
| utc_time_range: Date.range(~N[2022-01-01 00:00:00], ~N[2022-01-01 05:00:00]), |
| filters: [ |
| [:is, "event:hostname", ["example.com"]] |
| ], |
| dimensions: ["time", "event:hostname"] |
| }).filters == [ |
| [:is, "event:hostname", ["example.com"]] |
| ] |
| end |
| end |
|
|
| describe "trim_relative_date_range" do |
| test "trims current month period when flag is set" do |
| now = DateTime.new!(~D[2024-01-15], ~T[12:00:00], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2024-01-01], ~D[2024-01-31], "UTC"), |
| input_date_range: :month, |
| now: now, |
| timezone: "UTC", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, true) |
| }) |
|
|
| assert result.utc_time_range.first == ~U[2024-01-01 00:00:00Z] |
| assert result.utc_time_range.last == ~U[2024-01-15 23:59:59Z] |
| end |
|
|
| test "trims current year period when flag is set" do |
| now = DateTime.new!(~D[2024-03-15], ~T[12:00:00], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2024-01-01], ~D[2024-12-31], "UTC"), |
| input_date_range: :year, |
| now: now, |
| timezone: "UTC", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, true) |
| }) |
|
|
| assert result.utc_time_range.first == ~U[2024-01-01 00:00:00Z] |
| assert result.utc_time_range.last == ~U[2024-03-15 23:59:59Z] |
| end |
|
|
| test "trims current day period to current hour when flag is set" do |
| now = DateTime.new!(~D[2024-01-15], ~T[14:30:00], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2024-01-15], ~D[2024-01-15], "UTC"), |
| input_date_range: :day, |
| now: now, |
| timezone: "UTC", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, true) |
| }) |
|
|
| assert result.utc_time_range.first == ~U[2024-01-15 00:00:00Z] |
| assert result.utc_time_range.last == ~U[2024-01-15 14:59:59Z] |
| end |
|
|
| test "does not trim historical month periods even when flag is set" do |
| now = DateTime.new!(~D[2024-01-15], ~T[12:00:00], "UTC") |
| original_range = DateTimeRange.new!(~D[2023-06-01], ~D[2023-06-30], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: original_range, |
| input_date_range: :month, |
| now: now, |
| timezone: "UTC", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, true) |
| }) |
|
|
| assert result.utc_time_range == original_range |
| end |
|
|
| test "does not trim historical year periods even when flag is set" do |
| now = DateTime.new!(~D[2024-03-15], ~T[12:00:00], "UTC") |
| original_range = DateTimeRange.new!(~D[2023-01-01], ~D[2023-12-31], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: original_range, |
| input_date_range: :year, |
| now: now, |
| timezone: "UTC", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, true) |
| }) |
|
|
| assert result.utc_time_range == original_range |
| end |
|
|
| test "does not trim historical day periods even when flag is set" do |
| now = DateTime.new!(~D[2024-01-15], ~T[14:30:00], "UTC") |
| original_range = DateTimeRange.new!(~D[2024-01-10], ~D[2024-01-10], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: original_range, |
| input_date_range: :day, |
| now: now, |
| timezone: "UTC", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, true) |
| }) |
|
|
| assert result.utc_time_range == original_range |
| end |
|
|
| test "does not trim day when comparisons are set" do |
| now = DateTime.new!(~D[2024-01-15], ~T[12:00:00], "UTC") |
| original_range = DateTimeRange.new!(~D[2024-01-01], ~D[2024-01-31], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: original_range, |
| input_date_range: :day, |
| now: now, |
| timezone: "UTC", |
| include: |
| Map.merge( |
| ApiQueryParser.default_include(), |
| %{comparisons: %{mode: "previous_period"}, trim_relative_date_range: true} |
| ) |
| }) |
|
|
| assert result.utc_time_range == original_range |
| end |
|
|
| test "trims month when comparisons are set" do |
| now = DateTime.new!(~D[2024-01-15], ~T[12:00:00], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: DateTimeRange.new!(~D[2024-01-01], ~D[2024-01-31], "UTC"), |
| input_date_range: :month, |
| now: now, |
| timezone: "UTC", |
| include: |
| Map.merge( |
| ApiQueryParser.default_include(), |
| %{comparisons: %{mode: "previous_period"}, trim_relative_date_range: true} |
| ) |
| }) |
|
|
| assert result.utc_time_range.first == ~U[2024-01-01 00:00:00Z] |
| assert result.utc_time_range.last == ~U[2024-01-15 23:59:59Z] |
| end |
|
|
| test "does not trim when flag is false" do |
| now = DateTime.new!(~D[2024-01-15], ~T[12:00:00], "UTC") |
| original_range = DateTimeRange.new!(~D[2024-01-01], ~D[2024-01-31], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: original_range, |
| input_date_range: :month, |
| now: now, |
| timezone: "UTC", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, false) |
| }) |
|
|
| assert result.utc_time_range == original_range |
| end |
|
|
| test "does not trim when flag is not set" do |
| now = DateTime.new!(~D[2024-01-15], ~T[12:00:00], "UTC") |
| original_range = DateTimeRange.new!(~D[2024-01-01], ~D[2024-01-31], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: original_range, |
| input_date_range: :month, |
| now: now, |
| timezone: "UTC", |
| include: ApiQueryParser.default_include() |
| }) |
|
|
| assert result.utc_time_range == original_range |
| end |
|
|
| test "does not trim non-current periods like custom date ranges" do |
| now = DateTime.new!(~D[2024-01-15], ~T[12:00:00], "UTC") |
|
|
| original_range = DateTimeRange.new!(~D[2024-01-10], ~D[2024-01-16], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: original_range, |
| input_date_range: {:last_n_days, 7}, |
| now: now, |
| timezone: "UTC", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, true) |
| }) |
|
|
| assert result.utc_time_range == original_range |
| end |
|
|
| test "handles timezone correctly when trimming year periods" do |
| now = DateTime.new!(~D[2024-03-15], ~T[12:00:00], "UTC") |
|
|
| result = |
| perform(%{ |
| utc_time_range: |
| DateTimeRange.new!(~D[2024-01-01], ~D[2024-12-31], "America/New_York") |
| |> DateTimeRange.to_timezone("Etc/UTC"), |
| input_date_range: :year, |
| now: now, |
| timezone: "America/New_York", |
| include: Map.put(ApiQueryParser.default_include(), :trim_relative_date_range, true) |
| }) |
|
|
| nyc_mar_15_end = |
| DateTimeRange.new!(~D[2024-03-15], ~D[2024-03-15], "America/New_York") |
| |> DateTimeRange.to_timezone("Etc/UTC") |
| |> Map.get(:last) |
|
|
| assert result.utc_time_range.last == nyc_mar_15_end |
| end |
| end |
|
|
| describe "set_sql_join_type" do |
| test "updates sql_join_type to :full if time:minute or time:hour dimension is present" do |
| assert perform(%{dimensions: ["time:minute"]}).sql_join_type == :full |
| assert perform(%{dimensions: ["time:hour"]}).sql_join_type == :full |
| end |
|
|
| test "keeps default sql_join_type otherwise" do |
| assert perform(%{dimensions: ["time:day"]}).sql_join_type == :left |
| assert perform(%{dimensions: []}).sql_join_type == :left |
| end |
| end |
| end |
|
|