Current section
Files
Jump to
Current section
Files
lib/connection/ddl.ex
defmodule MssqlEcto.Connection.DDL do
@moduledoc false
alias Ecto.Migration.{Table, Index, Reference, Constraint}
import MssqlEcto.Connection.Helper
@creates [:create, :create_if_not_exists]
@drops [:drop, :drop_if_exists]
def logs(%Mssqlex.Result{} = result) do
messages =
case result do
%{messages: messages} ->
messages
_ ->
[]
end
for message <- messages do
%{message: message, severity: severity} = message
{ddl_log_level(severity), message, []}
end
end
# TODO these are for Postgres, maybe irrelevant for MSSQL
defp ddl_log_level("DEBUG"), do: :debug
defp ddl_log_level("LOG"), do: :info
defp ddl_log_level("INFO"), do: :info
defp ddl_log_level("NOTICE"), do: :info
defp ddl_log_level("WARNING"), do: :warn
defp ddl_log_level("ERROR"), do: :error
defp ddl_log_level("FATAL"), do: :error
defp ddl_log_level("PANIC"), do: :error
defp ddl_log_level(_severity), do: :info
def table_exists_query(table) do
{"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ?", [table]}
end
# execute dll (all MSSQL from here on)
def execute({command, %Table{} = table, columns})
when command in @creates do
query = [
if_do(
command == :create_if_not_exists,
"IF NOT EXISTS (SELECT * from SYSOBJECTS WHERE name='#{table.name}' and xtype='U') "
),
"CREATE TABLE ",
quote_table(table.prefix, table.name),
?\s,
?(,
column_definitions(table, columns),
pk_definition(columns, ", ", table),
?),
options_expr(table.options)
]
[query]
end
def execute({command, %Table{} = table}) when command in @drops do
[
[
if_do(
command == :drop_if_exists,
"IF OBJECT_ID('#{table.prefix}#{table.name}', 'U') IS NOT NULL "
),
"DROP TABLE ",
quote_table(table.prefix, table.name)
]
]
end
def execute({:alter, %Table{} = table, changes}) do
query = [
column_changes(table, changes),
quote_alter(pk_definition(changes, " ADD ", table), table)
]
[query]
end
def execute({:create, %Index{} = index}) do
fields = intersperse_map(index.columns, ", ", &index_expr/1)
queries = [
[
"CREATE ",
if_do(index.unique, "UNIQUE "),
"INDEX ",
quote_name(index.name),
" ON ",
quote_table(index.prefix, index.table),
?\s,
?(,
fields,
?),
if_do(index.where, [" WHERE ", to_string(index.where)])
]
]
queries
end
def execute({:create_if_not_exists, %Index{} = _index}) do
raise("create index if not exists: not supported")
end
def execute({command, %Index{} = index}) when command in @drops do
if_exists = if command == :drop_if_exists, do: "IF EXISTS ", else: []
[
[
"DROP INDEX ",
if_exists,
quote_name(index.name),
" ON ",
quote_table(index.prefix, index.table)
]
]
end
def execute({:rename, %Table{} = current_table, %Table{} = new_table}) do
[
[
"EXEC sp_rename ",
quote_name([current_table.prefix, current_table.name], ?'),
", ",
quote_name(new_table.name, ?'),
", 'OBJECT'"
]
]
end
def execute({:rename, %Table{} = table, current_column, new_column}) do
[
[
"EXEC sp_rename ",
quote_name([table.prefix, table.name, current_column], ?'),
", ",
quote_name(new_column, ?'),
", 'COLUMN'"
]
]
end
def execute({:create, %Constraint{} = constraint}) do
queries = [
[
"ALTER TABLE ",
quote_table(constraint.prefix, constraint.table),
" ADD ",
new_constraint_expr(constraint)
]
]
queries
end
def execute({:drop, %Constraint{} = constraint}) do
[
[
"ALTER TABLE ",
quote_table(constraint.prefix, constraint.table),
" DROP CONSTRAINT ",
quote_name(constraint.name)
]
]
end
def execute(string) when is_binary(string), do: [string]
def execute(keyword) when is_list(keyword),
do: error!(nil, "MSSQL adapter does not support keyword lists in execute")
@doc false
def supports_ddl_transaction? do
false
end
## Helpers
defp quote_alter([], _table), do: []
defp quote_alter(statement, table),
do: ["ALTER TABLE ", quote_table(table.prefix, table.name), statement, "; "]
defp pk_definition(columns, prefix, table) do
pks = for {_, name, _, opts} <- columns, opts[:primary_key], do: name
case pks do
[] ->
[]
_ ->
[
prefix,
"CONSTRAINT ",
constraint_name("pk", table),
" PRIMARY KEY (",
intersperse_map(pks, ", ", "e_name/1),
")"
]
end
end
defp column_definitions(table, columns) do
intersperse_map(columns, ", ", &column_definition(table, &1))
end
defp column_definition(table, {:add, name, %Reference{} = ref, opts}) do
[
quote_name(name),
?\s,
reference_column_type(ref.type, opts),
column_options(ref.type, opts, table, name),
reference_expr(ref, table, name)
]
end
defp column_definition(table, {:add, name, type, opts}) do
[
quote_name(name),
?\s,
column_type(type, opts),
column_options(type, opts, table, name)
]
end
defp column_changes(table, columns) do
{additions, changes} = Enum.split_with(columns, fn val -> elem(val, 0) == :add end)
[
if_do(additions !== [], column_additions(additions, table)),
if_do(changes !== [], Enum.map(changes, &column_change(table, &1)))
]
end
defp column_additions(additions, table) do
quote_alter(
[" ADD ", intersperse_map(additions, ", ", &column_change(table, &1))],
table
)
end
defp column_change(table, {:add, name, %Reference{} = ref, opts}) do
[
quote_name(name),
?\s,
reference_column_type(ref.type, opts),
column_options(ref.type, opts, table, name),
reference_expr(ref, table, name)
]
end
defp column_change(table, {:add, name, type, opts}) do
[
quote_name(name),
?\s,
column_type(type, opts),
column_options(type, opts, table, name)
]
end
defp column_change(table, {:modify, name, %Reference{} = ref, opts}) do
[
quote_alter(constraint_expr(ref, table, name), table),
quote_alter(
[
" ALTER COLUMN ",
quote_name(name),
?\s,
reference_column_type(ref.type, opts),
modify_null(name, opts)
],
table
),
modify_default(name, ref.type, opts, table, name)
]
end
defp column_change(table, {:modify, name, type, opts}) do
[
quote_alter(
[
" ALTER COLUMN ",
quote_name(name),
?\s,
column_type(type, opts),
modify_null(name, opts)
],
table
),
modify_default(name, type, opts, table, name)
]
end
defp column_change(table, {:remove, name}) do
[
if_do(
table.primary_key,
quote_alter([" DROP CONSTRAINT ", constraint_name("pk", table)], table)
),
quote_alter([" DROP COLUMN ", quote_name(name)], table)
]
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, table, name) do
case Keyword.fetch(opts, :default) do
{:ok, val} ->
constraint_tag = constraint_name("default", table, name)
[
"IF OBJECT_ID('",
constraint_tag,
"', 'D') IS NOT NULL ",
quote_alter(
[" DROP CONSTRAINT ", constraint_name("default", table, name)],
table
),
quote_alter(
[
" ADD",
default_expr({:ok, val}, type, table, name),
" FOR ",
quote_name(name)
],
table
)
]
:error ->
[]
end
end
defp column_options(type, opts, table, name) do
default = Keyword.fetch(opts, :default)
null = Keyword.get(opts, :null)
[default_expr(default, type, table, name), null_expr(null)]
end
defp null_expr(false), do: " NOT NULL"
defp null_expr(true), do: " NULL"
defp null_expr(_), do: []
defp new_constraint_expr(%Constraint{check: check} = constraint)
when is_binary(check) do
["CONSTRAINT ", quote_name(constraint.name), " CHECK (", check, ")"]
end
defp constraint_name(constraint_type, table, name \\ []) do
sections =
[
quote_name(table.prefix, nil),
quote_name(table.name, nil),
quote_name(name, nil),
constraint_type
]
|> Enum.reject(&(&1 === []))
[?", Enum.intersperse(sections, ?_), ?"]
end
defp default_expr({:ok, _} = default, type, table, name),
do: [
" CONSTRAINT ",
constraint_name("default", table, name),
default_expr(default, type)
]
defp default_expr(:error, _, _, _), do: []
defp default_expr({:ok, nil}, _type), do: " DEFAULT NULL"
defp default_expr({:ok, []}, _type), do: error!(nil, "arrays not supported")
defp default_expr({:ok, literal}, _type) when is_binary(literal),
do: [" DEFAULT '", escape_string(literal), ?']
defp default_expr({:ok, literal}, _type) when is_number(literal),
do: [" DEFAULT ", to_string(literal)]
defp default_expr({:ok, literal}, _type) when is_boolean(literal),
do: [" DEFAULT ", to_string(if literal, do: 1, else: 0)]
defp default_expr({:ok, {:fragment, expr}}, _type), do: [" DEFAULT ", expr]
defp default_expr({:ok, expr}, type),
do:
raise(
ArgumentError,
"unknown default `#{inspect(expr)}` for type `#{inspect(type)}`. " <>
":default may be a string, number, boolean, empty list or a fragment(...)"
)
defp index_expr(literal) when is_binary(literal), do: literal
defp index_expr(literal), do: quote_name(literal)
defp options_expr(nil), do: []
defp options_expr(keyword) when is_list(keyword),
do:
error!(
nil,
"Microsoft SQL Server adapter does not support keyword lists in :options"
)
defp options_expr(options), do: [?\s, options]
defp column_type({:array, type}, opts), do: [column_type(type, opts), "[]"]
defp column_type(type, opts) do
size = Keyword.get(opts, :size)
precision = Keyword.get(opts, :precision)
scale = Keyword.get(opts, :scale)
type_name = ecto_to_db(type)
cond do
size ->
[type_name, ?(, to_string(size), ?)]
precision ->
[type_name, ?(, to_string(precision), ?,, to_string(scale || 0), ?)]
type == :string ->
[type_name, "(255)"]
true ->
type_name
end
end
defp reference_expr(%Reference{} = ref, table, name),
do: [
" CONSTRAINT ",
reference_name(ref, table, name),
" REFERENCES ",
quote_table(table.prefix, ref.table),
?(,
quote_name(ref.column),
?),
reference_on_delete(ref.on_delete),
reference_on_update(ref.on_update)
]
defp constraint_expr(%Reference{} = ref, table, name),
do: [
" ADD CONSTRAINT ",
reference_name(ref, table, name),
?\s,
"FOREIGN KEY (",
quote_name(name),
") REFERENCES ",
quote_table(table.prefix, ref.table),
?(,
quote_name(ref.column),
?),
reference_on_delete(ref.on_delete),
reference_on_update(ref.on_update)
]
defp reference_name(%Reference{name: nil}, table, column),
do: quote_name("#{table.name}_#{column}_fkey")
defp reference_name(%Reference{name: name}, _table, _column),
do: quote_name(name)
defp reference_column_type(:serial, _opts), do: "int"
defp reference_column_type(:bigserial, _opts), do: "bigint"
defp reference_column_type(type, opts), do: column_type(type, opts)
defp reference_on_delete(:nilify_all), do: " ON DELETE SET NULL"
defp reference_on_delete(:delete_all), do: " ON DELETE CASCADE"
defp reference_on_delete(_), do: []
defp reference_on_update(:nilify_all), do: " ON UPDATE SET NULL"
defp reference_on_update(:update_all), do: " ON UPDATE CASCADE"
defp reference_on_update(_), do: []
end