From 33e693ab5241a2e451e770c30f34197548c0e927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 24 Oct 2020 11:51:14 +0200 Subject: [PATCH] Support composite foreign keys with match --- integration_test/sql/migration.exs | 63 ++++++++++++++++++------ integration_test/sql/transaction.exs | 2 +- integration_test/tds/test_helper.exs | 17 ++----- lib/ecto/adapters/myxql/connection.ex | 44 +++++++++-------- lib/ecto/adapters/postgres/connection.ex | 62 +++++++++++++---------- lib/ecto/adapters/tds/connection.ex | 21 +++++--- lib/ecto/migration.ex | 28 ++++++++--- test/ecto/adapters/myxql_test.exs | 7 ++- test/ecto/adapters/postgres_test.exs | 53 +++++++++++--------- test/ecto/adapters/tds_test.exs | 19 ++++--- 10 files changed, 195 insertions(+), 121 deletions(-) diff --git a/integration_test/sql/migration.exs b/integration_test/sql/migration.exs index 6ccc08cd..fc379a0b 100644 --- a/integration_test/sql/migration.exs +++ b/integration_test/sql/migration.exs @@ -133,10 +133,6 @@ defmodule Ecto.Integration.MigrationTest do alter table(:alter_fk_posts) do modify :alter_fk_user_id, references(:alter_fk_users, on_delete: :nilify_all) end - - execute "INSERT INTO alter_fk_users (id) VALUES ('1')" - execute "INSERT INTO alter_fk_posts (id, alter_fk_user_id) VALUES ('1', '1')" - execute "DELETE FROM alter_fk_users" end def down do @@ -158,10 +154,6 @@ defmodule Ecto.Integration.MigrationTest do alter table(:alter_fk_posts) do modify :alter_fk_user_id, references(:alter_fk_users, on_update: :update_all) end - - execute "INSERT INTO alter_fk_users (id) VALUES ('1')" - execute "INSERT INTO alter_fk_posts (id, alter_fk_user_id) VALUES ('1', '1')" - execute "UPDATE alter_fk_users SET id = '2'" end def down do @@ -232,6 +224,23 @@ defmodule Ecto.Integration.MigrationTest do end end + defmodule CompositeForeignKeyMigration do + use Ecto.Migration + + def change do + create table(:composite_parent) do + add :key_id, :integer + end + + create unique_index(:composite_parent, [:id, :key_id]) + + create table(:composite_child) do + add :parent_key_id, :integer + add :parent_id, references(:composite_parent, with: [parent_key_id: :key_id]) + end + end + end + defmodule ReferencesRollbackMigration do use Ecto.Migration @@ -431,7 +440,7 @@ defmodule Ecto.Integration.MigrationTest do assert :ok == down(PoolRepo, num, InferredDropIndexMigration, log: false) end - test "supports references", %{migration_number: num} do + test "supports on delete", %{migration_number: num} do assert :ok == up(PoolRepo, num, OnDeleteMigration, log: false) parent1 = PoolRepo.insert! Ecto.put_meta(%Parent{}, source: "parent1") @@ -452,6 +461,18 @@ defmodule Ecto.Integration.MigrationTest do assert :ok == down(PoolRepo, num, OnDeleteMigration, log: false) end + test "composite foreign keys", %{migration_number: num} do + assert :ok == up(PoolRepo, num, CompositeForeignKeyMigration, log: false) + + PoolRepo.insert_all("composite_parent", [[key_id: 2]]) + assert [id] = PoolRepo.all(from p in "composite_parent", select: p.id) + + catch_error(PoolRepo.insert_all("composite_child", [[parent_id: id, parent_key_id: 1]])) + assert {1, nil} = PoolRepo.insert_all("composite_child", [[parent_id: id, parent_key_id: 2]]) + + assert :ok == down(PoolRepo, num, CompositeForeignKeyMigration, log: false) + end + test "rolls back references in change/1", %{migration_number: num} do assert :ok == up(PoolRepo, num, ReferencesRollbackMigration, log: false) assert :ok == down(PoolRepo, num, ReferencesRollbackMigration, log: false) @@ -498,7 +519,6 @@ defmodule Ecto.Integration.MigrationTest do :ok = down(PoolRepo, num, AlterColumnMigration, log: false) end - @tag :modify_column_with_from test "modify column with from", %{migration_number: num} do assert :ok == up(PoolRepo, num, AlterColumnFromMigration, log: false) @@ -508,9 +528,8 @@ defmodule Ecto.Integration.MigrationTest do :ok = down(PoolRepo, num, AlterColumnFromMigration, log: false) end - @tag :modify_column_with_from @tag :alter_primary_key - test "modify column with from andd pkey", %{migration_number: num} do + test "modify column with from and pkey", %{migration_number: num} do assert :ok == up(PoolRepo, num, AlterColumnFromPkeyMigration, log: false) assert [1] == @@ -519,17 +538,31 @@ defmodule Ecto.Integration.MigrationTest do :ok = down(PoolRepo, num, AlterColumnFromPkeyMigration, log: false) end - @tag :modify_foreign_key_on_delete test "modify foreign key's on_delete constraint", %{migration_number: num} do assert :ok == up(PoolRepo, num, AlterForeignKeyOnDeleteMigration, log: false) + + PoolRepo.insert_all("alter_fk_users", [[]]) + assert [id] = PoolRepo.all from p in "alter_fk_users", select: p.id + + PoolRepo.insert_all("alter_fk_posts", [[alter_fk_user_id: id]]) + PoolRepo.delete_all("alter_fk_users") assert [nil] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id + :ok = down(PoolRepo, num, AlterForeignKeyOnDeleteMigration, log: false) end - @tag :modify_foreign_key_on_update + @tag :assigns_id_type test "modify foreign key's on_update constraint", %{migration_number: num} do assert :ok == up(PoolRepo, num, AlterForeignKeyOnUpdateMigration, log: false) - assert [2] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id + + PoolRepo.insert_all("alter_fk_users", [[]]) + assert [id] = PoolRepo.all from p in "alter_fk_users", select: p.id + + PoolRepo.insert_all("alter_fk_posts", [[alter_fk_user_id: id]]) + PoolRepo.update_all("alter_fk_users", set: [id: 12345]) + assert [12345] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id + + PoolRepo.delete_all("alter_fk_posts") :ok = down(PoolRepo, num, AlterForeignKeyOnUpdateMigration, log: false) end diff --git a/integration_test/sql/transaction.exs b/integration_test/sql/transaction.exs index 61a880b9..6ac93345 100644 --- a/integration_test/sql/transaction.exs +++ b/integration_test/sql/transaction.exs @@ -100,7 +100,7 @@ defmodule Ecto.Integration.TransactionTest do end end - @tag :pk_insert + @tag :assigns_id_type test "transaction rolls back with reason on aborted transaction" do e1 = PoolRepo.insert!(%Trans{num: 13}) diff --git a/integration_test/tds/test_helper.exs b/integration_test/tds/test_helper.exs index f60effad..6d2e9209 100644 --- a/integration_test/tds/test_helper.exs +++ b/integration_test/tds/test_helper.exs @@ -8,17 +8,12 @@ ExUnit.start( :subquery_aggregates, # sql don't have array type :array_type, - # I'm not sure if this is even possible if we SET IDENTITY_INSERT ON - :modify_foreign_key_on_update, - :modify_foreign_key_on_delete, - # NEXT 4 exclusions: can only be supported with MERGE statement and it is tricky to make it fast - :with_conflict_target, - :without_conflict_target, - :upsert_all, + # upserts can only be supported with MERGE statement and it is tricky to make it fast :upsert, - # I'm not sure why, but if decimal is passed as parameter mssql server will round differently ecto/integration_test/cases/interval.exs:186 + :upsert_all, + # mssql rounds differently than ecto/integration_test/cases/interval.exs :uses_msec, - # Unique index compares even NULL values for post_id, so below fails inserting permalinks without setting valid post_id + # unique index compares even NULL values for post_id, so below fails inserting permalinks without setting valid post_id :insert_cell_wise_defaults, # MSSQL does not support strings on text fields :text_type_as_string, @@ -45,10 +40,6 @@ ExUnit.start( # But I couldn't make it work :( :on_replace_nilify, :on_replace_update, - # This can't be executed since it requires - # `SET IDENTITY_INSERT [ [ database_name . ] schema_name . ] table_name ON` - # and after insert we need to turn it on, must be run manually in transaction - :pk_insert, # Tds allows nested transactions so this will never raise and SQL query should be "BEGIN TRAN" :transaction_checkout_raises, # JSON_VALUE always returns strings (even for e.g. integers) and returns null for diff --git a/lib/ecto/adapters/myxql/connection.ex b/lib/ecto/adapters/myxql/connection.ex index 97b54711..a1c54aca 100644 --- a/lib/ecto/adapters/myxql/connection.ex +++ b/lib/ecto/adapters/myxql/connection.ex @@ -141,7 +141,7 @@ if Code.ensure_loaded?(MyXQL) do @impl true def insert(prefix, table, header, rows, on_conflict, []) do - fields = intersperse_map(header, ?,, "e_name/1) + fields = quote_names(header) ["INSERT INTO ", quote_table(prefix, table), " (", fields, ") VALUES ", insert_all(rows) | on_conflict(on_conflict, header)] end @@ -794,7 +794,7 @@ if Code.ensure_loaded?(MyXQL) do case pks do [] -> [] - _ -> [[prefix, "PRIMARY KEY (", intersperse_map(pks, ", ", "e_name/1), ?)]] + _ -> [[prefix, "PRIMARY KEY (", quote_names(pks), ?)]] end end @@ -923,26 +923,28 @@ if Code.ensure_loaded?(MyXQL) do end end - defp constraint_expr(%Reference{} = ref, table, name), - do: [", ADD CONSTRAINT ", reference_name(ref, table, name), - " FOREIGN KEY (", quote_name(name), ?), - " REFERENCES ", quote_table(ref.prefix || table.prefix, ref.table), - ?(, quote_name(ref.column), ?), - reference_on_delete(ref.on_delete), reference_on_update(ref.on_update)] + defp reference_expr(type, ref, table, name) do + {current_columns, reference_columns} = Enum.unzip([{name, ref.column} | ref.with]) - defp constraint_if_not_exists_expr(%Reference{} = ref, table, name), - do: [", ADD CONSTRAINT ", reference_name(ref, table, name), - " FOREIGN KEY IF NOT EXISTS (", quote_name(name), ?), - " REFERENCES ", quote_table(ref.prefix || table.prefix, ref.table), - ?(, quote_name(ref.column), ?), - reference_on_delete(ref.on_delete), reference_on_update(ref.on_update)] + if ref.match do + error!(nil, ":match is not supported in references for tds") + end + + ["CONSTRAINT ", reference_name(ref, table, name), + " ", type, " (", quote_names(current_columns), ?), + " REFERENCES ", quote_table(ref.prefix || table.prefix, ref.table), + ?(, quote_names(reference_columns), ?), + reference_on_delete(ref.on_delete), reference_on_update(ref.on_update)] + end defp reference_expr(%Reference{} = ref, table, name), - do: [", CONSTRAINT ", reference_name(ref, table, name), - " FOREIGN KEY (", quote_name(name), ?), - " REFERENCES ", quote_table(ref.prefix || table.prefix, ref.table), - ?(, quote_name(ref.column), ?), - reference_on_delete(ref.on_delete), reference_on_update(ref.on_update)] + do: [", " | reference_expr("FOREIGN KEY", ref, table, name)] + + defp constraint_expr(%Reference{} = ref, table, name), + do: [", ADD " | reference_expr("FOREIGN KEY", ref, table, name)] + + defp constraint_if_not_exists_expr(%Reference{} = ref, table, name), + do: [", ADD " | reference_expr("FOREIGN KEY IF NOT EXISTS", ref, table, name)] defp drop_constraint_expr(%Reference{} = ref, table, name), do: ["DROP FOREIGN KEY ", reference_name(ref, table, name), ", "] @@ -980,9 +982,9 @@ if Code.ensure_loaded?(MyXQL) do {expr || expr(source, sources, query), name} end - defp quote_name(name) defp quote_name(name) when is_atom(name), do: quote_name(Atom.to_string(name)) + defp quote_name(name) do if String.contains?(name, "`") do error!(nil, "bad field name #{inspect name}") @@ -991,6 +993,8 @@ if Code.ensure_loaded?(MyXQL) do [?`, name, ?`] end + defp quote_names(names), do: intersperse_map(names, ?,, "e_name/1) + defp quote_table(nil, name), do: quote_table(name) defp quote_table(prefix, name), do: [quote_table(prefix), ?., quote_table(name)] diff --git a/lib/ecto/adapters/postgres/connection.ex b/lib/ecto/adapters/postgres/connection.ex index d4153401..9802b222 100644 --- a/lib/ecto/adapters/postgres/connection.ex +++ b/lib/ecto/adapters/postgres/connection.ex @@ -157,7 +157,7 @@ if Code.ensure_loaded?(Postgrex) do if header == [] do [" VALUES " | intersperse_map(rows, ?,, fn _ -> "(DEFAULT)" end)] else - [?\s, ?(, intersperse_map(header, ?,, "e_name/1), ") VALUES " | insert_all(rows, 1)] + [?\s, ?(, quote_names(header), ") VALUES " | insert_all(rows, 1)] end ["INSERT INTO ", quote_table(prefix, table), insert_as(on_conflict), @@ -188,7 +188,7 @@ if Code.ensure_loaded?(Postgrex) do defp conflict_target([]), do: [] defp conflict_target(targets), - do: [?(, intersperse_map(targets, ?,, "e_name/1), ?), ?\s] + do: [?(, quote_names(targets), ?), ?\s] defp replace(fields) do ["UPDATE SET " | @@ -754,7 +754,7 @@ if Code.ensure_loaded?(Postgrex) do defp returning([]), do: [] defp returning(returning), - do: [" RETURNING " | intersperse_map(returning, ", ", "e_name/1)] + do: [" RETURNING " | quote_names(returning)] defp create_names(%{sources: sources}, as_prefix) do create_names(sources, 0, tuple_size(sources), as_prefix) |> List.to_tuple() @@ -905,7 +905,7 @@ if Code.ensure_loaded?(Postgrex) do {"SELECT true FROM information_schema.tables WHERE table_name = $1 AND table_schema = current_schema() LIMIT 1", [table]} end - # From https://www.postgresql.org/docs/9.3/static/protocol-error-fields.html. + # From https://www.postgresql.org/docs/current/protocol-error-fields.html. defp ddl_log_level("DEBUG"), do: :debug defp ddl_log_level("LOG"), do: :info defp ddl_log_level("INFO"), do: :info @@ -924,7 +924,7 @@ if Code.ensure_loaded?(Postgrex) do case pks do [] -> [] - _ -> [prefix, "PRIMARY KEY (", intersperse_map(pks, ", ", "e_name/1), ")"] + _ -> [prefix, "PRIMARY KEY (", quote_names(pks), ")"] end end @@ -954,7 +954,7 @@ if Code.ensure_loaded?(Postgrex) do 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), reference_expr(ref, table, name)] + column_options(ref.type, opts), ", ", reference_expr(ref, table, name)] end defp column_definition(_table, {:add, name, type, opts}) do @@ -967,7 +967,7 @@ if Code.ensure_loaded?(Postgrex) do defp column_change(table, {:add, name, %Reference{} = ref, opts}) do ["ADD COLUMN ", quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(ref.type, opts), reference_expr(ref, table, name)] + column_options(ref.type, opts), ", ADD ", reference_expr(ref, table, name)] end defp column_change(_table, {:add, name, type, opts}) do @@ -977,7 +977,7 @@ if Code.ensure_loaded?(Postgrex) do defp column_change(table, {:add_if_not_exists, name, %Reference{} = ref, opts}) do ["ADD COLUMN IF NOT EXISTS ", quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(ref.type, opts), reference_expr(ref, table, name)] + column_options(ref.type, opts), ", ADD ", reference_expr(ref, table, name)] end defp column_change(_table, {:add_if_not_exists, name, type, opts}) do @@ -986,23 +986,25 @@ if Code.ensure_loaded?(Postgrex) do end defp column_change(table, {:modify, name, %Reference{} = ref, opts}) do - [drop_constraint_expr(opts[:from], table, name), "ALTER COLUMN ", quote_name(name), " TYPE ", reference_column_type(ref.type, opts), - constraint_expr(ref, table, name), modify_null(name, opts), modify_default(name, ref.type, opts)] + [drop_reference_expr(opts[:from], table, name), "ALTER COLUMN ", quote_name(name), + " TYPE ", reference_column_type(ref.type, opts), + ", ADD ", reference_expr(ref, table, name), + modify_null(name, opts), modify_default(name, ref.type, opts)] end defp column_change(table, {:modify, name, type, opts}) do - [drop_constraint_expr(opts[:from], table, name), "ALTER COLUMN ", quote_name(name), " TYPE ", + [drop_reference_expr(opts[:from], table, name), "ALTER COLUMN ", quote_name(name), " TYPE ", column_type(type, opts), modify_null(name, opts), modify_default(name, type, opts)] end defp column_change(_table, {:remove, name}), do: ["DROP COLUMN ", quote_name(name)] defp column_change(table, {:remove, name, %Reference{} = ref, _opts}) do - [drop_constraint_expr(ref, table, name), "DROP COLUMN ", quote_name(name)] + [drop_reference_expr(ref, table, name), "DROP COLUMN ", quote_name(name)] end defp column_change(_table, {:remove, name, _type, _opts}), do: ["DROP COLUMN ", quote_name(name)] defp column_change(table, {:remove_if_exists, name, %Reference{} = ref}) do - [drop_constraint_if_exists_expr(ref, table, name), "DROP COLUMN IF EXISTS ", quote_name(name)] + [drop_reference_if_exists_expr(ref, table, name), "DROP COLUMN IF EXISTS ", quote_name(name)] end defp column_change(_table, {:remove_if_exists, name, _type}), do: ["DROP COLUMN IF EXISTS ", quote_name(name)] @@ -1135,25 +1137,24 @@ if Code.ensure_loaded?(Postgrex) do end end - defp reference_expr(%Reference{} = ref, table, name), - do: [" CONSTRAINT ", reference_name(ref, table, name), " REFERENCES ", - quote_table(ref.prefix || table.prefix, ref.table), ?(, quote_name(ref.column), ?), - reference_on_delete(ref.on_delete), reference_on_update(ref.on_update), validate(ref.validate)] + defp reference_expr(%Reference{} = ref, table, name) do + {current_columns, reference_columns} = Enum.unzip([{name, ref.column} | ref.with]) - defp constraint_expr(%Reference{} = ref, table, name), - do: [", ADD CONSTRAINT ", reference_name(ref, table, name), ?\s, - "FOREIGN KEY (", quote_name(name), ") REFERENCES ", - quote_table(ref.prefix || table.prefix, ref.table), ?(, quote_name(ref.column), ?), - reference_on_delete(ref.on_delete), reference_on_update(ref.on_update), validate(ref.validate)] + ["CONSTRAINT ", reference_name(ref, table, name), ?\s, + "FOREIGN KEY (", quote_names(current_columns), ") REFERENCES ", + quote_table(ref.prefix || table.prefix, ref.table), ?(, quote_names(reference_columns), ?), + reference_match(ref.match), reference_on_delete(ref.on_delete), + reference_on_update(ref.on_update), validate(ref.validate)] + end - defp drop_constraint_expr(%Reference{} = ref, table, name), + defp drop_reference_expr(%Reference{} = ref, table, name), do: ["DROP CONSTRAINT ", reference_name(ref, table, name), ", "] - defp drop_constraint_expr(_, _, _), + defp drop_reference_expr(_, _, _), do: [] - defp drop_constraint_if_exists_expr(%Reference{} = ref, table, name), + defp drop_reference_if_exists_expr(%Reference{} = ref, table, name), do: ["DROP CONSTRAINT IF EXISTS ", reference_name(ref, table, name), ", "] - defp drop_constraint_if_exists_expr(_, _, _), + defp drop_reference_if_exists_expr(_, _, _), do: [] defp reference_name(%Reference{name: nil}, table, column), @@ -1176,6 +1177,11 @@ if Code.ensure_loaded?(Postgrex) do defp reference_on_update(:restrict), do: " ON UPDATE RESTRICT" defp reference_on_update(_), do: [] + defp reference_match(nil), do: [] + defp reference_match(:full), do: " MATCH FULL" + defp reference_match(:simple), do: " MATCH SIMPLE" + defp reference_match(:partial), do: " MATCH PARTIAL" + defp validate(false), do: " NOT VALID" defp validate(_), do: [] @@ -1191,6 +1197,10 @@ if Code.ensure_loaded?(Postgrex) do [source, ?. | quote_name(name)] end + defp quote_names(names) do + intersperse_map(names, ?,, "e_name/1) + end + defp quote_name(name) when is_atom(name) do quote_name(Atom.to_string(name)) end diff --git a/lib/ecto/adapters/tds/connection.ex b/lib/ecto/adapters/tds/connection.ex index a2d61722..a077d521 100644 --- a/lib/ecto/adapters/tds/connection.ex +++ b/lib/ecto/adapters/tds/connection.ex @@ -209,7 +209,7 @@ if Code.ensure_loaded?(Tds) do [ ?\s, ?(, - intersperse_map(header, ", ", "e_name/1), + quote_names(header), ?), returning, " VALUES " | insert_all(rows, 1) @@ -432,9 +432,9 @@ if Code.ensure_loaded?(Tds) do defp cte_header(%Ecto.Query{select: %{fields: fields}} = query, _) do [ " (", - intersperse_map(fields, ", ", fn + intersperse_map(fields, ",", fn {key, _} -> - [quote_name(key)] + quote_name(key) other -> error!( @@ -1194,7 +1194,7 @@ if Code.ensure_loaded?(Tds) do [] _ -> - [prefix, "PRIMARY KEY CLUSTERED (#{intersperse_map(pks, ", ", "e_name/1)})"] + [prefix, "PRIMARY KEY CLUSTERED (", quote_names(pks), ?)] end end @@ -1419,13 +1419,19 @@ if Code.ensure_loaded?(Tds) do end defp constraint_expr(%Reference{} = ref, table, name) do + {current_columns, reference_columns} = Enum.unzip([{name, ref.column} | ref.with]) + + if ref.match do + error!(nil, ":match is not supported in references for tds") + end + [ " CONSTRAINT ", reference_name(ref, table, name), - " FOREIGN KEY (#{quote_name(name)})", + " FOREIGN KEY (#{quote_names(current_columns)})", " REFERENCES ", quote_table(ref.prefix || table.prefix, ref.table), - "(#{quote_name(ref.column)})", + "(#{quote_names(reference_columns)})", reference_on_delete(ref.on_delete), reference_on_update(ref.on_update) ] @@ -1433,7 +1439,6 @@ if Code.ensure_loaded?(Tds) do defp reference_expr(%Reference{} = ref, table, name) do [",", constraint_expr(ref, table, name)] - |> Enum.map_join("", &"#{&1}") end defp reference_name(%Reference{name: nil}, table, column), @@ -1475,6 +1480,8 @@ if Code.ensure_loaded?(Tds) do "[#{name}]" end + defp quote_names(names), do: intersperse_map(names, ?,, "e_name/1) + defp quote_table(nil, name), do: quote_table(name) defp quote_table({server, db, schema}, name), diff --git a/lib/ecto/migration.ex b/lib/ecto/migration.ex index 6aceecb2..7ddfa64d 100644 --- a/lib/ecto/migration.ex +++ b/lib/ecto/migration.ex @@ -380,8 +380,12 @@ defmodule Ecto.Migration do To define a reference in a migration, see `Ecto.Migration.references/2`. """ - defstruct name: nil, prefix: nil, table: nil, column: :id, type: :bigserial, on_delete: :nothing, on_update: :nothing, validate: true - @type t :: %__MODULE__{table: String.t, prefix: atom | nil, column: atom, type: atom, on_delete: atom, on_update: atom, validate: boolean} + defstruct name: nil, prefix: nil, table: nil, column: :id, type: :bigserial, + on_delete: :nothing, on_update: :nothing, validate: true, + with: [], match: nil + @type t :: %__MODULE__{table: String.t, prefix: atom | nil, column: atom, type: atom, + on_delete: atom, on_update: atom, validate: boolean, + with: list, match: atom | nil} end defmodule Constraint do @@ -657,12 +661,12 @@ defmodule Ecto.Migration do * `:where` - specify conditions for a partial index. * `:include` - specify fields for a covering index. This is not supported by all databases. For more information on PostgreSQL support, please - [read the official docs](https://www.postgresql.org/docs/11/indexes-index-only-scans.html). + [read the official docs](https://www.postgresql.org/docs/current/indexes-index-only-scans.html). ## Adding/dropping indexes concurrently PostgreSQL supports adding/dropping indexes concurrently (see the - [docs](http://www.postgresql.org/docs/9.4/static/sql-createindex.html)). + [docs](http://www.postgresql.org/docs/current/static/sql-createindex.html)). However, this feature does not work well with the transactions used by Ecto to guarantee integrity during migrations. @@ -697,7 +701,7 @@ defmodule Ecto.Migration do For example, PostgreSQL supports several index types like B-tree (the default), Hash, GIN, and GiST. More information on index types can be found - in the [PostgreSQL docs](http://www.postgresql.org/docs/9.4/static/indexes-types.html). + in the [PostgreSQL docs](http://www.postgresql.org/docs/current/indexes-types.html). ## Partial indexes @@ -709,7 +713,7 @@ defmodule Ecto.Migration do to the generated `WHERE` clause as-is. More information on partial indexes can be found in the [PostgreSQL - docs](http://www.postgresql.org/docs/9.4/static/indexes-partial.html). + docs](http://www.postgresql.org/docs/current/indexes-partial.html). ## Examples @@ -1102,6 +1106,13 @@ defmodule Ecto.Migration do add :group_id, references("groups") end + create table("categories") do + add :group_id, :integer + # A composite foreign that points from categories (product_id, group_id) + # to products (id, group_id) + add :product_id, references("products", with: [group_id: :group_id]) + end + ## Options * `:name` - The name of the underlying reference, which defaults to @@ -1118,6 +1129,11 @@ defmodule Ecto.Migration do * `:validate` - Whether or not to validate the foreign key constraint on creation or not. Only available in PostgreSQL, and should be followed by a command to validate the foreign key in a following migration if false. + * `:with` - defines additional keys to the foreign key in order to build + a composite primary key + * `:match` - select if the match is `:simple`, `:partial`, or `:full`. This is + [supported only by PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html) + at the moment. """ def references(table, opts \\ []) diff --git a/test/ecto/adapters/myxql_test.exs b/test/ecto/adapters/myxql_test.exs index 96c49c07..d29f4d14 100644 --- a/test/ecto/adapters/myxql_test.exs +++ b/test/ecto/adapters/myxql_test.exs @@ -1059,7 +1059,8 @@ defmodule Ecto.Adapters.MyXQLTest do {:add, :category_2, %Reference{table: :categories, on_delete: :nothing}, []}, {:add, :category_3, %Reference{table: :categories, on_delete: :delete_all}, [null: false]}, {:add, :category_4, %Reference{table: :categories, on_delete: :nilify_all}, []}, - {:add, :category_5, %Reference{table: :categories, prefix: :foo, on_delete: :nilify_all}, []}]} + {:add, :category_5, %Reference{table: :categories, prefix: :foo, on_delete: :nilify_all}, []}, + {:add, :category_6, %Reference{table: :categories, with: [here: :there], on_delete: :nilify_all}, []}]} assert execute_ddl(create) == [""" CREATE TABLE `posts` (`id` bigint unsigned not null auto_increment, @@ -1075,6 +1076,8 @@ defmodule Ecto.Adapters.MyXQLTest do CONSTRAINT `posts_category_4_fkey` FOREIGN KEY (`category_4`) REFERENCES `categories`(`id`) ON DELETE SET NULL, `category_5` BIGINT UNSIGNED, CONSTRAINT `posts_category_5_fkey` FOREIGN KEY (`category_5`) REFERENCES `foo`.`categories`(`id`) ON DELETE SET NULL, + `category_6` BIGINT UNSIGNED, + CONSTRAINT `posts_category_6_fkey` FOREIGN KEY (`category_6`,`here`) REFERENCES `categories`(`id`,`there`) ON DELETE SET NULL, PRIMARY KEY (`id`)) ENGINE = INNODB """ |> remove_newlines] end @@ -1102,7 +1105,7 @@ defmodule Ecto.Adapters.MyXQLTest do {:add, :name, :string, []}]} assert execute_ddl(create) == [""" - CREATE TABLE `posts` (`a` integer, `b` integer, `name` varchar(255), PRIMARY KEY (`a`, `b`)) ENGINE = INNODB + CREATE TABLE `posts` (`a` integer, `b` integer, `name` varchar(255), PRIMARY KEY (`a`,`b`)) ENGINE = INNODB """ |> remove_newlines] end diff --git a/test/ecto/adapters/postgres_test.exs b/test/ecto/adapters/postgres_test.exs index b30033e5..9adcf20c 100644 --- a/test/ecto/adapters/postgres_test.exs +++ b/test/ecto/adapters/postgres_test.exs @@ -1206,7 +1206,7 @@ defmodule Ecto.Adapters.PostgresTest do assert execute_ddl(create) == [""" CREATE TABLE "foo"."posts" - ("category_0" bigint CONSTRAINT "posts_category_0_fkey" REFERENCES "foo"."categories"("id")) + ("category_0" bigint, CONSTRAINT "posts_category_0_fkey" FOREIGN KEY ("category_0") REFERENCES "foo"."categories"("id")) """ |> remove_newlines] end @@ -1219,7 +1219,7 @@ defmodule Ecto.Adapters.PostgresTest do ]} assert execute_ddl(create) == [remove_newlines(""" CREATE TABLE "posts" - ("category_0" bigint CONSTRAINT "posts_category_0_fkey" REFERENCES "categories"("id"), "created_at" timestamp, "updated_at" timestamp) + ("category_0" bigint, CONSTRAINT "posts_category_0_fkey" FOREIGN KEY ("category_0") REFERENCES "categories"("id"), "created_at" timestamp, "updated_at" timestamp) """), ~s|COMMENT ON TABLE "posts" IS 'comment'|, ~s|COMMENT ON COLUMN "posts"."category_0" IS 'column comment'|, @@ -1231,7 +1231,7 @@ defmodule Ecto.Adapters.PostgresTest do [{:add, :category_0, %Reference{table: :categories}, []}]} assert execute_ddl(create) == [remove_newlines(""" CREATE TABLE "foo"."posts" - ("category_0" bigint CONSTRAINT "posts_category_0_fkey" REFERENCES "foo"."categories"("id")) + ("category_0" bigint, CONSTRAINT "posts_category_0_fkey" FOREIGN KEY ("category_0") REFERENCES "foo"."categories"("id")) """), ~s|COMMENT ON TABLE "foo"."posts" IS 'table comment'|] end @@ -1245,7 +1245,7 @@ defmodule Ecto.Adapters.PostgresTest do ]} assert execute_ddl(create) == [remove_newlines(""" CREATE TABLE "foo"."posts" - ("category_0" bigint CONSTRAINT "posts_category_0_fkey" REFERENCES "foo"."categories"("id"), "created_at" timestamp, "updated_at" timestamp) + ("category_0" bigint, CONSTRAINT "posts_category_0_fkey" FOREIGN KEY ("category_0") REFERENCES "foo"."categories"("id"), "created_at" timestamp, "updated_at" timestamp) """), ~s|COMMENT ON COLUMN "foo"."posts"."category_0" IS 'column comment'|, ~s|COMMENT ON COLUMN "foo"."posts"."updated_at" IS 'column comment 2'|] @@ -1265,22 +1265,26 @@ defmodule Ecto.Adapters.PostgresTest do {:add, :category_8, %Reference{table: :categories, on_delete: :nilify_all, on_update: :update_all}, [null: false]}, {:add, :category_9, %Reference{table: :categories, on_delete: :restrict}, []}, {:add, :category_10, %Reference{table: :categories, on_update: :restrict}, []}, - {:add, :category_11, %Reference{table: :categories, prefix: "foo", on_update: :restrict}, []}]} + {:add, :category_11, %Reference{table: :categories, prefix: "foo", on_update: :restrict}, []}, + {:add, :category_12, %Reference{table: :categories, with: [here: :there]}, []}, + {:add, :category_13, %Reference{table: :categories, on_update: :restrict, with: [here: :there], match: :full}, []},]} assert execute_ddl(create) == [""" CREATE TABLE "posts" ("id" serial, - "category_0" bigint CONSTRAINT "posts_category_0_fkey" REFERENCES "categories"("id"), - "category_1" bigint CONSTRAINT "foo_bar" REFERENCES "categories"("id"), - "category_2" bigint CONSTRAINT "posts_category_2_fkey" REFERENCES "categories"("id"), - "category_3" bigint NOT NULL CONSTRAINT "posts_category_3_fkey" REFERENCES "categories"("id") ON DELETE CASCADE, - "category_4" bigint CONSTRAINT "posts_category_4_fkey" REFERENCES "categories"("id") ON DELETE SET NULL, - "category_5" bigint CONSTRAINT "posts_category_5_fkey" REFERENCES "categories"("id"), - "category_6" bigint NOT NULL CONSTRAINT "posts_category_6_fkey" REFERENCES "categories"("id") ON UPDATE CASCADE, - "category_7" bigint CONSTRAINT "posts_category_7_fkey" REFERENCES "categories"("id") ON UPDATE SET NULL, - "category_8" bigint NOT NULL CONSTRAINT "posts_category_8_fkey" REFERENCES "categories"("id") ON DELETE SET NULL ON UPDATE CASCADE, - "category_9" bigint CONSTRAINT "posts_category_9_fkey" REFERENCES "categories"("id") ON DELETE RESTRICT, - "category_10" bigint CONSTRAINT "posts_category_10_fkey" REFERENCES "categories"("id") ON UPDATE RESTRICT, - "category_11" bigint CONSTRAINT "posts_category_11_fkey" REFERENCES "foo"."categories"("id") ON UPDATE RESTRICT, + "category_0" bigint, CONSTRAINT "posts_category_0_fkey" FOREIGN KEY ("category_0") REFERENCES "categories"("id"), + "category_1" bigint, CONSTRAINT "foo_bar" FOREIGN KEY ("category_1") REFERENCES "categories"("id"), + "category_2" bigint, CONSTRAINT "posts_category_2_fkey" FOREIGN KEY ("category_2") REFERENCES "categories"("id"), + "category_3" bigint NOT NULL, CONSTRAINT "posts_category_3_fkey" FOREIGN KEY ("category_3") REFERENCES "categories"("id") ON DELETE CASCADE, + "category_4" bigint, CONSTRAINT "posts_category_4_fkey" FOREIGN KEY ("category_4") REFERENCES "categories"("id") ON DELETE SET NULL, + "category_5" bigint, CONSTRAINT "posts_category_5_fkey" FOREIGN KEY ("category_5") REFERENCES "categories"("id"), + "category_6" bigint NOT NULL, CONSTRAINT "posts_category_6_fkey" FOREIGN KEY ("category_6") REFERENCES "categories"("id") ON UPDATE CASCADE, + "category_7" bigint, CONSTRAINT "posts_category_7_fkey" FOREIGN KEY ("category_7") REFERENCES "categories"("id") ON UPDATE SET NULL, + "category_8" bigint NOT NULL, CONSTRAINT "posts_category_8_fkey" FOREIGN KEY ("category_8") REFERENCES "categories"("id") ON DELETE SET NULL ON UPDATE CASCADE, + "category_9" bigint, CONSTRAINT "posts_category_9_fkey" FOREIGN KEY ("category_9") REFERENCES "categories"("id") ON DELETE RESTRICT, + "category_10" bigint, CONSTRAINT "posts_category_10_fkey" FOREIGN KEY ("category_10") REFERENCES "categories"("id") ON UPDATE RESTRICT, + "category_11" bigint, CONSTRAINT "posts_category_11_fkey" FOREIGN KEY ("category_11") REFERENCES "foo"."categories"("id") ON UPDATE RESTRICT, + "category_12" bigint, CONSTRAINT "posts_category_12_fkey" FOREIGN KEY ("category_12","here") REFERENCES "categories"("id","there"), + "category_13" bigint, CONSTRAINT "posts_category_13_fkey" FOREIGN KEY ("category_13","here") REFERENCES "categories"("id","there") MATCH FULL ON UPDATE RESTRICT, PRIMARY KEY ("id")) """ |> remove_newlines] end @@ -1300,7 +1304,7 @@ defmodule Ecto.Adapters.PostgresTest do {:add, :name, :string, []}]} assert execute_ddl(create) == [""" - CREATE TABLE "posts" ("a" integer, "b" integer, "name" varchar(255), PRIMARY KEY ("a", "b")) + CREATE TABLE "posts" ("a" integer, "b" integer, "name" varchar(255), PRIMARY KEY ("a","b")) """ |> remove_newlines] end @@ -1312,7 +1316,7 @@ defmodule Ecto.Adapters.PostgresTest do assert execute_ddl(create) == [""" CREATE TABLE "posts" ("id" bigint GENERATED BY DEFAULT AS IDENTITY, - "category_0" bigint CONSTRAINT "posts_category_0_fkey" REFERENCES "categories"("id"), + "category_0" bigint, CONSTRAINT "posts_category_0_fkey" FOREIGN KEY ("category_0") REFERENCES "categories"("id"), "name" varchar(255), PRIMARY KEY ("id")) """ |> remove_newlines] @@ -1493,10 +1497,13 @@ defmodule Ecto.Adapters.PostgresTest do assert execute_ddl(alter) == [""" ALTER TABLE "posts" ADD COLUMN "title" varchar(100) DEFAULT 'Untitled' NOT NULL, - ADD COLUMN "author_id" bigint CONSTRAINT "posts_author_id_fkey" REFERENCES "author"("id"), - ADD COLUMN "category_id" bigint CONSTRAINT "posts_category_id_fkey" REFERENCES "categories"("id") NOT VALID, + ADD COLUMN "author_id" bigint, + ADD CONSTRAINT "posts_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "author"("id"), + ADD COLUMN "category_id" bigint, + ADD CONSTRAINT "posts_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories"("id") NOT VALID, ADD COLUMN IF NOT EXISTS "subtitle" varchar(100) NOT NULL, - ADD COLUMN IF NOT EXISTS "editor_id" bigint CONSTRAINT "posts_editor_id_fkey" REFERENCES "editor"("id"), + ADD COLUMN IF NOT EXISTS "editor_id" bigint, + ADD CONSTRAINT "posts_editor_id_fkey" FOREIGN KEY ("editor_id") REFERENCES "editor"("id"), ALTER COLUMN "price" TYPE numeric(8,2), ALTER COLUMN "price" DROP NOT NULL, ALTER COLUMN "cost" TYPE integer, @@ -1551,7 +1558,7 @@ defmodule Ecto.Adapters.PostgresTest do assert execute_ddl(alter) == [""" ALTER TABLE "foo"."posts" - ADD COLUMN "author_id" bigint CONSTRAINT "posts_author_id_fkey" REFERENCES "foo"."author"("id"), + ADD COLUMN "author_id" bigint, ADD CONSTRAINT "posts_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "foo"."author"("id"), ALTER COLUMN \"permalink_id\" TYPE bigint, ADD CONSTRAINT "posts_permalink_id_fkey" FOREIGN KEY ("permalink_id") REFERENCES "foo"."permalinks"("id"), ALTER COLUMN "permalink_id" SET NOT NULL diff --git a/test/ecto/adapters/tds_test.exs b/test/ecto/adapters/tds_test.exs index c84674d3..5402c8b8 100644 --- a/test/ecto/adapters/tds_test.exs +++ b/test/ecto/adapters/tds_test.exs @@ -202,7 +202,7 @@ defmodule Ecto.Adapters.TdsTest do |> plan() assert all(query) == - ~s{WITH [tree] ([id], [depth]) AS (} <> + ~s{WITH [tree] ([id],[depth]) AS (} <> ~s{SELECT c0.[id] AS [id], 1 AS [depth] FROM [categories] AS c0 WHERE (c0.[parent_id] IS NULL) } <> ~s{UNION ALL } <> ~s{(SELECT c0.[id], t1.[depth] + 1 FROM [categories] AS c0 } <> @@ -241,7 +241,7 @@ defmodule Ecto.Adapters.TdsTest do |> plan() assert all(query) == - ~s{WITH [comments_scope] ([entity_id], [text]) AS (} <> + ~s{WITH [comments_scope] ([entity_id],[text]) AS (} <> ~s{SELECT c0.[entity_id] AS [entity_id], c0.[text] AS [text] } <> ~s{FROM [comments] AS c0 WHERE (c0.[deleted_at] IS NULL)) } <> ~s{SELECT p0.[title], c1.[text] } <> @@ -734,7 +734,7 @@ defmodule Ecto.Adapters.TdsTest do |> plan() result = - ~s{WITH [cte1] ([id], [smth]) AS } <> + ~s{WITH [cte1] ([id],[smth]) AS } <> ~s{(SELECT s0.[id] AS [id], @1 AS [smth] FROM [schema1] AS s0 WHERE (@2)) } <> ~s{SELECT s0.[id], @3 FROM [schema] AS s0 INNER JOIN [schema2] AS s1 ON @4 } <> ~s{INNER JOIN [schema2] AS s2 ON @5 } <> @@ -988,12 +988,12 @@ defmodule Ecto.Adapters.TdsTest do test "insert" do # prefx, table, header, rows, on_conflict, returning query = insert(nil, "schema", [:x, :y], [[:x, :y]], {:raise, [], []}, []) - assert query == ~s{INSERT INTO [schema] ([x], [y]) VALUES (@1, @2)} + assert query == ~s{INSERT INTO [schema] ([x],[y]) VALUES (@1, @2)} query = insert(nil, "schema", [:x, :y], [[:x, :y], [nil, :y]], {:raise, [], []}, [:id]) assert query == - ~s{INSERT INTO [schema] ([x], [y]) OUTPUT INSERTED.[id] VALUES (@1, @2),(DEFAULT, @3)} + ~s{INSERT INTO [schema] ([x],[y]) OUTPUT INSERTED.[id] VALUES (@1, @2),(DEFAULT, @3)} query = insert(nil, "schema", [], [[]], {:raise, [], []}, [:id]) assert query == ~s{INSERT INTO [schema] OUTPUT INSERTED.[id] DEFAULT VALUES} @@ -1016,7 +1016,7 @@ defmodule Ecto.Adapters.TdsTest do ) assert query == - ~s{INSERT INTO [schema] ([x], [y], [z]) VALUES (@1, (SELECT s0.[id] FROM [schema] AS s0), @4),(DEFAULT, DEFAULT, (SELECT s0.[id] FROM [schema] AS s0))} + ~s{INSERT INTO [schema] ([x],[y],[z]) VALUES (@1, (SELECT s0.[id] FROM [schema] AS s0), @4),(DEFAULT, DEFAULT, (SELECT s0.[id] FROM [schema] AS s0))} end test "update" do @@ -1109,7 +1109,8 @@ defmodule Ecto.Adapters.TdsTest do [null: false]}, {:add, :category_4, %Reference{table: :categories, on_delete: :nilify_all}, []}, {:add, :category_5, %Reference{table: :categories, prefix: :foo, on_delete: :nilify_all}, - []} + []}, + {:add, :category_6, %Reference{table: :categories, with: [here: :there], on_delete: :nilify_all}, []} ]} assert execute_ddl(create) == [ @@ -1127,6 +1128,8 @@ defmodule Ecto.Adapters.TdsTest do CONSTRAINT [posts_category_4_fkey] FOREIGN KEY ([category_4]) REFERENCES [categories]([id]) ON DELETE SET NULL ON UPDATE NO ACTION, [category_5] BIGINT, CONSTRAINT [posts_category_5_fkey] FOREIGN KEY ([category_5]) REFERENCES [foo].[categories]([id]) ON DELETE SET NULL ON UPDATE NO ACTION, + [category_6] BIGINT, + CONSTRAINT [posts_category_6_fkey] FOREIGN KEY ([category_6],[here]) REFERENCES [categories]([id],[there]) ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT [posts_pkey] PRIMARY KEY CLUSTERED ([id])); """ |> remove_newlines @@ -1162,7 +1165,7 @@ defmodule Ecto.Adapters.TdsTest do assert execute_ddl(create) == [ """ CREATE TABLE [posts] ([a] integer, [b] integer, [name] nvarchar(255), - CONSTRAINT [posts_pkey] PRIMARY KEY CLUSTERED ([a], [b])); + CONSTRAINT [posts_pkey] PRIMARY KEY CLUSTERED ([a],[b])); """ |> remove_newlines |> Kernel.<>(" ")