Skip to content

Commit 1d7f4b7

Browse files
authored
Adds ability to skip the validation of PostgreSQL constraints on create (#271)
Validation of constraints in PostgreSQL can cause large tables to stay locked for a long time (preventing writes, because it has to check all rows in order to consider the constraint as valid. PostgreSQL has a way to get around this by first creating constraints as not valid. And then running a query to validate the constraint. The validation query does not lock the table, i.e. it can be run while allowing writes in parallel. This commit implements the ability to skip validation of constraints. It does not implement the validation query. This was inspired by #244. https://www.postgresql.org/docs/current/sql-altertable.html
1 parent 14ca094 commit 1d7f4b7

File tree

7 files changed

+46
-8
lines changed

7 files changed

+46
-8
lines changed

lib/ecto/adapters/postgres/connection.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,10 +1029,10 @@ if Code.ensure_loaded?(Postgrex) do
10291029
defp null_expr(_), do: []
10301030

10311031
defp new_constraint_expr(%Constraint{check: check} = constraint) when is_binary(check) do
1032-
["CONSTRAINT ", quote_name(constraint.name), " CHECK (", check, ")"]
1032+
["CONSTRAINT ", quote_name(constraint.name), " CHECK (", check, ")", validate(constraint.validate)]
10331033
end
10341034
defp new_constraint_expr(%Constraint{exclude: exclude} = constraint) when is_binary(exclude) do
1035-
["CONSTRAINT ", quote_name(constraint.name), " EXCLUDE USING ", exclude]
1035+
["CONSTRAINT ", quote_name(constraint.name), " EXCLUDE USING ", exclude, validate(constraint.validate)]
10361036
end
10371037

10381038
defp default_expr({:ok, nil}, _type), do: " DEFAULT NULL"

lib/ecto/adapters/tds/connection.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,10 @@ if Code.ensure_loaded?(Tds) do
10611061
error!(nil, msg)
10621062
end
10631063

1064+
def execute_ddl({:create, %Constraint{validate: false}}) do
1065+
error!(nil, "`:validate` is not supported by the Tds adapter")
1066+
end
1067+
10641068
def execute_ddl({:create, %Constraint{} = constraint}) do
10651069
table_name = quote_table(constraint.prefix, constraint.table)
10661070

lib/ecto/migration.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,9 @@ defmodule Ecto.Migration do
390390
391391
To define a constraint in a migration, see `Ecto.Migration.constraint/3`.
392392
"""
393-
defstruct name: nil, table: nil, check: nil, exclude: nil, prefix: nil, comment: nil
393+
defstruct name: nil, table: nil, check: nil, exclude: nil, prefix: nil, comment: nil, validate: true
394394
@type t :: %__MODULE__{name: atom, table: String.t, prefix: atom | nil,
395-
check: String.t | nil, exclude: String.t | nil, comment: String.t | nil}
395+
check: String.t | nil, exclude: String.t | nil, comment: String.t | nil, validate: boolean}
396396
end
397397

398398
defmodule Command do
@@ -1165,6 +1165,9 @@ defmodule Ecto.Migration do
11651165
* `:check` - A check constraint expression. Required when creating a check constraint.
11661166
* `:exclude` - An exclusion constraint expression. Required when creating an exclusion constraint.
11671167
* `:prefix` - The prefix for the table.
1168+
* `:validate` - Whether or not to validate the constraint on creation (true by default). Only
1169+
available in PostgreSQL, and should be followed by a command to validate the foreign key in
1170+
a following migration if false.
11681171
11691172
"""
11701173
def constraint(table, name, opts \\ [])

test/ecto/adapters/myxql_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,10 +1339,20 @@ defmodule Ecto.Adapters.MyXQLTest do
13391339
assert execute_ddl(create)
13401340
end
13411341

1342+
assert_raise ArgumentError, "MySQL adapter does not support check constraints", fn ->
1343+
create = {:create, constraint(:products, "foo", check: "price", validate: false)}
1344+
assert execute_ddl(create)
1345+
end
1346+
13421347
assert_raise ArgumentError, "MySQL adapter does not support exclusion constraints", fn ->
13431348
create = {:create, constraint(:products, "bar", exclude: "price")}
13441349
assert execute_ddl(create)
13451350
end
1351+
1352+
assert_raise ArgumentError, "MySQL adapter does not support exclusion constraints", fn ->
1353+
create = {:create, constraint(:products, "bar", exclude: "price", validate: false)}
1354+
assert execute_ddl(create)
1355+
end
13461356
end
13471357

13481358
test "create an index using a different type" do

test/ecto/adapters/postgres_test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1691,6 +1691,11 @@ defmodule Ecto.Adapters.PostgresTest do
16911691
~s|COMMENT ON CONSTRAINT "price_must_be_positive" ON "foo"."products" IS 'comment'|]
16921692
end
16931693

1694+
test "create invalid constraint" do
1695+
create = {:create, constraint(:products, "price_must_be_positive", check: "price > 0", prefix: "foo", validate: false)}
1696+
assert execute_ddl(create) == [~s|ALTER TABLE "foo"."products" ADD CONSTRAINT "price_must_be_positive" CHECK (price > 0) NOT VALID|]
1697+
end
1698+
16941699
test "drop constraint" do
16951700
drop = {:drop, constraint(:products, "price_must_be_positive")}
16961701
assert execute_ddl(drop) ==

test/ecto/adapters/tds_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,14 @@ defmodule Ecto.Adapters.TdsTest do
12531253
]
12541254
end
12551255

1256+
test "create check constraint with invalid validate opts" do
1257+
create = {:create, constraint(:products, "price_must_be_positive", check: "price > 0", validate: false)}
1258+
1259+
assert_raise ArgumentError, "`:validate` is not supported by the Tds adapter", fn ->
1260+
execute_ddl(create)
1261+
end
1262+
end
1263+
12561264
test "drop constraint" do
12571265
drop = {:drop, constraint(:products, "price_must_be_positive")}
12581266

test/ecto/migration_test.exs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,21 @@ defmodule Ecto.MigrationTest do
114114

115115
test "creates a constraint" do
116116
assert constraint(:posts, :price_is_positive, check: "price > 0") ==
117-
%Constraint{table: "posts", name: :price_is_positive, check: "price > 0"}
117+
%Constraint{table: "posts", name: :price_is_positive, check: "price > 0", validate: true}
118118
assert constraint("posts", :price_is_positive, check: "price > 0") ==
119-
%Constraint{table: "posts", name: :price_is_positive, check: "price > 0"}
119+
%Constraint{table: "posts", name: :price_is_positive, check: "price > 0", validate: true}
120120
assert constraint(:posts, :exclude_price, exclude: "price") ==
121-
%Constraint{table: "posts", name: :exclude_price, exclude: "price"}
121+
%Constraint{table: "posts", name: :exclude_price, exclude: "price", validate: true}
122122
assert constraint("posts", :exclude_price, exclude: "price") ==
123-
%Constraint{table: "posts", name: :exclude_price, exclude: "price"}
123+
%Constraint{table: "posts", name: :exclude_price, exclude: "price", validate: true}
124+
assert constraint(:posts, :price_is_positive, check: "price > 0", validate: false) ==
125+
%Constraint{table: "posts", name: :price_is_positive, check: "price > 0", validate: false}
126+
assert constraint("posts", :price_is_positive, check: "price > 0", validate: false) ==
127+
%Constraint{table: "posts", name: :price_is_positive, check: "price > 0", validate: false}
128+
assert constraint(:posts, :exclude_price, exclude: "price", validate: false) ==
129+
%Constraint{table: "posts", name: :exclude_price, exclude: "price", validate: false}
130+
assert constraint("posts", :exclude_price, exclude: "price", validate: false) ==
131+
%Constraint{table: "posts", name: :exclude_price, exclude: "price", validate: false}
124132
end
125133

126134
test "runs a reversible command" do

0 commit comments

Comments
 (0)