defmodule Ecto.Adapters.ClickHouse.Migration do @moduledoc false alias Ecto.Migration.{Table, Reference, Index, Constraint} @conn Ecto.Adapters.ClickHouse.Connection @dialyzer :no_improper_lists @spec execute_ddl(Ecto.Adapter.Migration.command()) :: iodata def execute_ddl({command, %Table{} = table, columns}) when command in [:create, :create_if_not_exists] do %Table{prefix: prefix, name: name, options: options, engine: engine} = table create = case command do :create -> "CREATE TABLE " :create_if_not_exists -> "CREATE TABLE IF NOT EXISTS " end engine = engine || "TinyLog" pk = if engine in ["TinyLog", "Memory"], do: [], else: pk_definition(columns) columns = @conn.intersperse_map(columns, ?,, &column_definition/1) options = options_expr(options) [ [ create, @conn.quote_table(prefix, name), ?(, columns, pk, ?), " ENGINE=", engine, options ] ] end def execute_ddl({command, %Table{} = table, _mode}) when command in [:drop, :drop_if_exists] do drop = case command do :drop_if_exists -> "DROP TABLE IF EXISTS " :drop -> "DROP TABLE " end [ [drop | @conn.quote_table(table.prefix, table.name)] ] end def execute_ddl({:alter, %Table{} = table, changes}) do Enum.map(changes, fn change -> [ "ALTER TABLE ", @conn.quote_table(table.prefix, table.name), ?\s | column_change(change) ] end) end def execute_ddl({command, %Index{} = index}) when command in [:create, :create_if_not_exists] do if index.unique do raise ArgumentError, "ClickHouse does not support UNIQUE INDEX" end # TODO or :using? type = index.options[:type] type || raise ArgumentError, "expected :type in options, got: #{inspect(type)}" granularity = index.options[:granularity] granularity || raise ArgumentError, "expected :granularity in options, got: #{inspect(granularity)}" fields = @conn.intersperse_map(index.columns, ?,, &index_expr/1) create = case command do :create -> "CREATE INDEX " :create_if_not_exists -> "CREATE INDEX IF NOT EXISTS " end [ [ create, @conn.quote_name(index.name), " ON ", @conn.quote_table(index.prefix, index.table), " (", fields, ") TYPE ", to_string(type), " GRANULARITY " | to_string(granularity) ] ] end def execute_ddl({command, %Index{} = index, _mode}) when command in [:drop, :drop_if_exists] do drop = case command do :drop -> "DROP INDEX " :drop_if_exists -> "DROP INDEX IF EXISTS " end [[drop | @conn.quote_table(index.prefix, index.name)]] end def execute_ddl({command, %Constraint{} = _constraint}) when command in [:create, :create_if_not_exists] do raise "TODO" end def execute_ddl({command, %Constraint{} = _constraint, _mode}) when command in [:drop, :drop_if_exists] do raise "TODO" end def execute_ddl({:rename, %Table{} = current_table, %Table{} = new_table}) do [ [ "RENAME TABLE ", @conn.quote_table(current_table.prefix, current_table.name), " TO ", @conn.quote_table(new_table.prefix, new_table.name) ] ] end def execute_ddl({:rename, %Table{} = table, column, name}) do [ [ "ALTER TABLE ", @conn.quote_table(table.prefix, table.name), " RENAME COLUMN ", @conn.quote_name(column), " TO ", @conn.quote_name(name) ] ] end def execute_ddl(string) when is_binary(string) do [string] end def execute_ddl(list) when is_list(list) do raise ArgumentError, "ClickHouse adapter does not support lists in execute_ddl" end defp pk_definition(columns) do pk_columns = Enum.filter(columns, fn {_, _, _, opts} -> case Keyword.get(opts, :primary_key, false) do true = t -> t false = f -> f end end) case pk_columns do [] = empty -> empty pk_columns -> pk_expr = pk_columns |> Enum.map(fn {_, name, type, _} -> if type in [:serial, :bigserial] do raise ArgumentError, "type #{inspect(type)} is not supported as ClickHouse does not support AUTOINCREMENT" end @conn.quote_name(name) end) |> Enum.intersperse(?,) [",PRIMARY KEY (", pk_expr, ?)] end end defp column_definition({:add, _name, %Reference{}, _opts}) do raise ArgumentError, "ClickHouse does not support FOREIGN KEY" end defp column_definition({:add, name, type, opts}) do [ @conn.quote_name(name), ?\s, column_type(type) | column_options(type, opts) ] end # TODO collate support? defp column_options(type, opts) do default = Keyword.fetch(opts, :default) null = Keyword.get(opts, :null) [ default_expr(default, type), null_expr(null) ] end defp column_change({:add, _name, %Reference{}, _opts}) do raise ArgumentError, "ClickHouse does not support FOREIGN KEY" end defp column_change({:add, name, type, opts}) do [ "ADD COLUMN ", @conn.quote_name(name), ?\s, column_type(type) | column_options(type, opts) ] end defp column_change({:modify, _name, %Reference{}, _opts}) do raise ArgumentError, "ClickHouse does not support FOREIGN KEY" end defp column_change({:modify, name, type, opts}) do [ "MODIFY COLUMN ", @conn.quote_name(name), ?\s, column_type(type), modify_default(name, type, opts) | modify_null(name, opts) ] end defp column_change({:remove, name}) do ["DROP COLUMN " | @conn.quote_name(name)] end # TODO defp column_change({:remove, name, _type, _opts}) do column_change({:remove, name}) end defp modify_null(_name, opts) do case Keyword.get(opts, :null) do nil -> [] val -> null_expr(val) end end defp modify_default(name, type, opts) do case Keyword.fetch(opts, :default) do {:ok, _val} = ok -> [" ADD ", default_expr(ok, type), " FOR ", @conn.quote_name(name)] :error -> [] end end defp null_expr(true), do: " NULL" defp null_expr(false), do: " NOT NULL" defp null_expr(_), do: [] @dialyzer {:no_improper_lists, default_expr: 2} defp default_expr({:ok, nil}, _type) do " DEFAULT NULL" end defp default_expr({:ok, literal}, _type) when is_binary(literal) do [" DEFAULT '", @conn.escape_string(literal), ?'] end defp default_expr({:ok, literal}, _type) when is_number(literal) do [" DEFAULT " | to_string(literal)] end defp default_expr({:ok, {:fragment, expr}}, _type) do [" DEFAULT " | expr] end defp default_expr({:ok, true}, _type) do " DEFAULT 1" end defp default_expr({:ok, false}, _type) do " DEFAULT 0" end defp default_expr({:ok, list}, _type) when is_list(list) do raise ArgumentError, "ClickHouse adapter does not support lists in :default, " <> "use fragments instead" end defp default_expr({:ok, map}, _type) when is_map(map) do raise ArgumentError, "ClickHouse adapter does not support maps in :default, " <> "use fragments instead" end defp default_expr(:error, _), do: [] defp index_expr(literal) when is_binary(literal), do: literal defp index_expr(literal), do: @conn.quote_name(literal) defp options_expr(nil), do: [] defp options_expr(options) when is_list(options) do raise ArgumentError, "ClickHouse adapter does not support lists in :options" end defp options_expr(options), do: [?\s | to_string(options)] defp column_type(type) when type in [:serial, :bigserial] do raise ArgumentError, "type #{inspect(type)} is not supported as ClickHouse does not support AUTOINCREMENT" end defp column_type(:id) do raise ArgumentError, "type :id is ambiguous, use a literal (e.g. :Int64 or :UInt64) instead" end defp column_type(:numeric) do raise ArgumentError, "type :numeric is not supported" end defp column_type(:time) do raise ArgumentError, "type :time is not supported" end defp column_type(:map) do raise ArgumentError, ~s[type :map is ambiguous, use a literal (e.g. :JSON or :"Map(String, UInt8)") instead] end defp column_type(:decimal) do raise ArgumentError, ~s[type :decimal is ambiguous, use a literal (e.g. :"Decimal(p, s)") instead] end defp column_type(:uuid), do: "UUID" defp column_type(:boolean), do: "Bool" defp column_type(:integer), do: "Int32" defp column_type(:bigint), do: "Int64" defp column_type(type) when type in [:string, :binary, :binary_id] do "String" end defp column_type(:float), do: "Float64" defp column_type({:array, type}) do ["Array(", column_type(type), ?)] end # TODO DateTime('UTC') defp column_type(:utc_datetime), do: "DateTime" defp column_type(:utc_datetime_usec), do: "DateTime64(6)" defp column_type(:naive_datetime), do: "DateTime" defp column_type(:naive_datetime_usec), do: "DateTime64(6)" defp column_type(type), do: Atom.to_string(type) end