defmodule Plausible.DataMigration do @moduledoc """ Base module for coordinated Clickhouse data migrations run via remote shell or otherwise (TBD). """ defmacro __using__(opts) do dir = Keyword.fetch!(opts, :dir) repo = Keyword.get(opts, :repo, Plausible.DataMigration.ClickhouseRepo) quote bind_quoted: [dir: dir, repo: repo] do @dir dir @repo repo def run_sql_confirm(name, assigns \\ [], options \\ []) do query = unwrap_with_io(name, assigns) message = Keyword.get(options, :prompt_message, "Execute?") default_choice = Keyword.get(options, :prompt_default_choice, :yes) confirm(message, fn -> do_run(name, query) end, default_choice) end def confirm(message, func, default_choice \\ :yes) do choices = case default_choice do :yes -> " [Y/n]: " :no -> " [y/N]: " end prompt = IO.ANSI.white() <> message <> choices <> IO.ANSI.reset() answer = String.downcase(String.trim(IO.gets(prompt))) skip = fn -> IO.puts(" #{IO.ANSI.cyan()}Skipped.#{IO.ANSI.reset()}") {:ok, :skip} end case answer do "y" -> func.() "n" -> skip.() _ -> case default_choice do :yes -> func.() :no -> skip.() end end end def unwrap(name, assigns \\ []) do :plausible |> :code.priv_dir() |> Path.join("data_migrations") |> Path.join(@dir) |> Path.join("sql") |> Path.join(name <> ".sql.eex") |> EEx.eval_file(assigns: assigns) end @doc """ Runs a single SQL query in a file. Valid options: - `quiet` - reduces output from running the SQL - `params` - List of query parameters. - `query_options` - passed to Repo.query """ def run_sql(name, assigns \\ [], options \\ []) do query = unwrap(name, assigns) do_run(name, query, options) end @doc """ Runs multiple SQL queries from a single file. Note that each query must be separated by semicolons. """ def run_sql_multi(name, assigns \\ [], options \\ []) do unwrap(name, assigns) |> String.trim() |> String.split(";", trim: true) |> Enum.with_index(1) |> Enum.reduce_while(:ok, fn {query, index}, _ -> case do_run("#{name}-#{index}", query, options) do {:ok, _} -> {:cont, :ok} error -> {:halt, error} end end) end def do_run(name, query, options \\ []) do params = Keyword.get(options, :params, []) query_options = Keyword.get(options, :query_options, []) case @repo.query(query, params, [timeout: :infinity] ++ query_options) do {:ok, res} -> if not Keyword.get(options, :quiet, false) do IO.puts( " #{IO.ANSI.yellow()}#{name} #{IO.ANSI.green()}Done!#{IO.ANSI.reset()}\n" ) IO.puts(String.duplicate("-", 78)) end {:ok, res} result -> result end end defp unwrap_with_io(name, assigns) do IO.puts("#{IO.ANSI.yellow()}Running #{name}#{IO.ANSI.reset()}") query = unwrap(name, assigns) IO.puts(""" -> Query: #{IO.ANSI.blue()}#{String.trim(query)}#{IO.ANSI.reset()} """) query end end end end