defmodule Mix.Tasks.Ecto.Ch.Schema do
@moduledoc """
Shows an Ecto schema hint for a ClickHouse table.
Examples:
$ mix ecto.ch.schema
$ mix ecto.ch.schema system.numbers
$ mix ecto.ch.schema system.numbers --repo MyApp.Repo
"""
use Mix.Task
alias Ch.Connection, as: Conn
def run([]) do
IO.puts(@moduledoc)
end
def run(["-" <> _k | _] = kvs) do
run(_source = nil, kvs)
end
def run([source | kvs]) do
run(source, kvs)
end
defp run(source, kvs) do
[_, table] =
source =
case String.split(source, ".") do
[_table] = source -> [nil | source]
[_prefix, _table] = source -> source
end
repos = Mix.Ecto.parse_repo(kvs)
config =
Enum.find_value(repos, fn repo ->
Mix.Ecto.ensure_repo(repo, kvs)
if repo.__adapter__() == Ecto.Adapters.ClickHouse do
repo.config()
end
end)
{where, params} =
case source do
[nil, table] ->
if config do
database = Keyword.fetch!(config, :database)
{"where database = {database:String} and table = {table:String}",
%{"database" => database, "table" => table}}
else
{"where table = {table:String}", %{"table" => table}}
end
[database, table] ->
{"where database = {database:String} and table = {table:String}",
%{"database" => database, "table" => table}}
end
statement = "select database, name, type from system.columns " <> where
conn = connect(config || [])
case query(conn, statement, params) do
{%Ch.Result{rows: [_ | _] = rows}, _conn} ->
ensure_single_table!(rows)
schema = [
"""
@primary_key false
schema "#{table}" do
""",
Enum.map(rows, fn [_db, name, type] -> [" ", build_field(name, type), ?\n] end),
"end"
]
IO.puts(schema)
{%Ch.Result{rows: []}, _conn} ->
raise "table not found"
end
end
defp connect(config) do
case Conn.connect(config) do
{:ok, conn} -> conn
{:error, reason} -> raise reason
end
end
defp query(conn, statement, params, opts \\ []) do
query = Ch.Query.build(statement)
params = DBConnection.Query.encode(query, params, opts)
case Conn.handle_execute(query, params, opts, conn) do
{:ok, query, result, conn} -> {DBConnection.Query.decode(query, result, opts), conn}
{:disconnect, reason, _conn} -> raise reason
{:error, reason, _conn} -> raise reason
end
end
@doc false
def build_field(name, type) do
type = Ch.Types.decode(type)
ecto_type = ecto_type(type)
clickhouse_type = clickhouse_type(type)
name = Code.format_string!(":#{name}")
case {ecto_type, clickhouse_type} do
{ecto_type, nil} ->
~s[field #{name}, #{inspect(ecto_type)}]
{ecto_type, clickhouse_type} ->
~s[field #{name}, #{inspect(ecto_type)}, type: "#{clickhouse_type}"]
end
end
defp ecto_type({:array, type}), do: {:array, ecto_type(type)}
defp ecto_type(type) when type in [:string, :date, :boolean], do: type
defp ecto_type(:uuid), do: Ecto.UUID
defp ecto_type(_type), do: Ch
defp clickhouse_type({:array, type}), do: clickhouse_type(type)
defp clickhouse_type(type) when type in [:uuid, :string, :date, :boolean], do: nil
defp clickhouse_type(type), do: Ch.Types.encode(type)
defp ensure_single_table!(rows) do
rows
|> Enum.group_by(fn [db, _, _] -> db end, fn [_, name, type] -> [name, type] end)
|> Map.keys()
|> case do
[_db] ->
:ok
dbs ->
raise """
table is present in multiple databases: #{Enum.join(dbs, ", ")}
"""
end
end
end