lib/migration.ex

defmodule Ecto.Migration.Timescaledb do
  @moduledoc """
  Provides migration related functions in [TimescaleDB](https://www.timescale.com/) extended SQL for `Ecto.Query`
  """

  defmacro __using__(_options) do
    quote do
      use Ecto.Migration
      import Ecto.Migration.Timescaledb
    end
  end

  @doc """
  [create_hypertable](https://docs.timescale.com/api/latest/hypertable/create_hypertable/#create-hypertable)

  ## Examples

  ```
  use Ecto.Migration.Timescaledb

  def change do
    create table(:user, primary_key: false) do
      add :joined_at, :naive_datetime, null: false
      add :name, :string
    end

    create_hypertable(:user, :joined_at)
  end
  ```
  """
  defmacro create_hypertable(relation, time_column_name, opts \\ []) do
    sql =
      "SELECT create_hypertable('#{Atom.to_string(relation)}', '#{Atom.to_string(time_column_name)}'"

    {_, sql} =
      options_for_sql(
        sql,
        [
          :partitioning_column,
          :number_partitions,
          :chunk_time_interval,
          :create_default_indexes,
          :if_not_exists,
          :partitioning_func,
          :associated_schema_name,
          :associated_table_prefix,
          :migrate_data,
          :time_partitioning_func,
          :replication_factor,
          :data_nodes
        ],
        opts
      )

    sql = sql <> ")"

    quote do
      execute(unquote(sql))
    end
  end

  @doc """
  [add_dimension](https://docs.timescale.com/api/latest/hypertable/add_dimension/#sample-usage)

  ## Examples

  ```
  use Ecto.Migration.Timescaledb

  def change do
    create_hypertable(:conditions, :time)
    add_dimension(:conditions, :location, number_partitions: 4)
  end
  ```
  """
  defmacro add_dimension(relation, column_name, opts \\ []) do
    sql = "SELECT add_dimension('#{Atom.to_string(relation)}', '#{Atom.to_string(column_name)}'"

    {_, sql} =
      options_for_sql(
        sql,
        [:number_partitions, :chunk_time_interval, :partitioning_func, :if_not_exists],
        opts
      )

    sql = sql <> ")"

    quote do
      execute(unquote(sql))
    end
  end

  defp options_for_sql(sql, keys, opts) do
    Enum.flat_map_reduce(
      keys,
      sql,
      fn argument, sql ->
        if opts[argument] do
          {:halt, sql <> ", #{Atom.to_string(argument)} => #{opts[argument]}"}
        else
          {:halt, sql}
        end
      end
    )
  end
end