mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-20 05:23:18 +12:00
feat: add check_constraints, both for validation and migrations
This commit is contained in:
parent
e8cc9eb489
commit
1f6621b852
15 changed files with 788 additions and 9 deletions
|
@ -2,8 +2,12 @@
|
||||||
# DONT MODIFY IT BY HAND
|
# DONT MODIFY IT BY HAND
|
||||||
locals_without_parens = [
|
locals_without_parens = [
|
||||||
base_filter_sql: 1,
|
base_filter_sql: 1,
|
||||||
|
check: 1,
|
||||||
|
check_constraint: 2,
|
||||||
|
check_constraint: 3,
|
||||||
create?: 1,
|
create?: 1,
|
||||||
foreign_key_names: 1,
|
foreign_key_names: 1,
|
||||||
|
message: 1,
|
||||||
migrate?: 1,
|
migrate?: 1,
|
||||||
name: 1,
|
name: 1,
|
||||||
on_delete: 1,
|
on_delete: 1,
|
||||||
|
|
|
@ -24,6 +24,11 @@ defmodule AshPostgres do
|
||||||
Extension.get_entities(resource, [:postgres, :references])
|
Extension.get_entities(resource, [:postgres, :references])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "The configured check_constraints for a resource"
|
||||||
|
def check_constraints(resource) do
|
||||||
|
Extension.get_entities(resource, [:postgres, :check_constraints])
|
||||||
|
end
|
||||||
|
|
||||||
@doc "The configured polymorphic_reference_on_delete for a resource"
|
@doc "The configured polymorphic_reference_on_delete for a resource"
|
||||||
def polymorphic_on_delete(resource) do
|
def polymorphic_on_delete(resource) do
|
||||||
Extension.get_opt(resource, [:postgres, :references], :polymorphic_on_delete, nil, true)
|
Extension.get_opt(resource, [:postgres, :references], :polymorphic_on_delete, nil, true)
|
||||||
|
|
31
lib/check_constraint.ex
Normal file
31
lib/check_constraint.ex
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule AshPostgres.CheckConstraint do
|
||||||
|
@moduledoc """
|
||||||
|
Contains configuration for database check constraints
|
||||||
|
"""
|
||||||
|
defstruct [:attribute, :name, :message, :check]
|
||||||
|
|
||||||
|
def schema do
|
||||||
|
[
|
||||||
|
attribute: [
|
||||||
|
type: :any,
|
||||||
|
doc:
|
||||||
|
"The attribute or list of attributes to which an error will be added if the check constraint fails",
|
||||||
|
required: true
|
||||||
|
],
|
||||||
|
name: [
|
||||||
|
type: :string,
|
||||||
|
required: true,
|
||||||
|
doc: "The name of the constraint"
|
||||||
|
],
|
||||||
|
message: [
|
||||||
|
type: :string,
|
||||||
|
doc: "The message to be added if the check constraint fails"
|
||||||
|
],
|
||||||
|
check: [
|
||||||
|
type: :string,
|
||||||
|
doc:
|
||||||
|
"The contents of the check. If this is set, the migration generator will include it when generating migrations"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -96,6 +96,79 @@ defmodule AshPostgres.DataLayer do
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@check_constraint %Ash.Dsl.Entity{
|
||||||
|
name: :check_constraint,
|
||||||
|
describe: """
|
||||||
|
Add a check constraint to be validated.
|
||||||
|
|
||||||
|
If a check constraint exists on the table but not in this section, and it produces an error, a runtime error will be raised.
|
||||||
|
|
||||||
|
Provide a list of attributes instead of a single attribute to add the message to multiple attributes.
|
||||||
|
|
||||||
|
By adding the `check` option, the migration generator will include it when generating migrations.
|
||||||
|
""",
|
||||||
|
examples: [
|
||||||
|
"""
|
||||||
|
check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive"
|
||||||
|
"""
|
||||||
|
],
|
||||||
|
args: [:attribute, :name],
|
||||||
|
target: AshPostgres.CheckConstraint,
|
||||||
|
schema: AshPostgres.CheckConstraint.schema()
|
||||||
|
}
|
||||||
|
|
||||||
|
@check_constraints %Ash.Dsl.Section{
|
||||||
|
name: :check_constraints,
|
||||||
|
describe: """
|
||||||
|
A section for configuring the check constraints for a given table.
|
||||||
|
|
||||||
|
This can be used to automatically create those check constraints, or just to provide message when they are raised
|
||||||
|
""",
|
||||||
|
examples: [
|
||||||
|
"""
|
||||||
|
check_constraints do
|
||||||
|
check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive"
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
],
|
||||||
|
entities: [@check_constraint]
|
||||||
|
}
|
||||||
|
|
||||||
|
@references %Ash.Dsl.Section{
|
||||||
|
name: :references,
|
||||||
|
describe: """
|
||||||
|
A section for configuring the references (foreign keys) in resource migrations.
|
||||||
|
|
||||||
|
This section is only relevant if you are using the migration generator with this resource.
|
||||||
|
Otherwise, it has no effect.
|
||||||
|
""",
|
||||||
|
examples: [
|
||||||
|
"""
|
||||||
|
references do
|
||||||
|
reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey"
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
],
|
||||||
|
entities: [@reference],
|
||||||
|
schema: [
|
||||||
|
polymorphic_on_delete: [
|
||||||
|
type: {:one_of, [:delete, :nilify, :nothing, :restrict]},
|
||||||
|
doc:
|
||||||
|
"For polymorphic resources, configures the on_delete behavior of the automatically generated foreign keys to source tables."
|
||||||
|
],
|
||||||
|
polymorphic_on_update: [
|
||||||
|
type: {:one_of, [:update, :nilify, :nothing, :restrict]},
|
||||||
|
doc:
|
||||||
|
"For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables."
|
||||||
|
],
|
||||||
|
polymorphic_name: [
|
||||||
|
type: {:one_of, [:update, :nilify, :nothing, :restrict]},
|
||||||
|
doc:
|
||||||
|
"For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables."
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@postgres %Ash.Dsl.Section{
|
@postgres %Ash.Dsl.Section{
|
||||||
name: :postgres,
|
name: :postgres,
|
||||||
describe: """
|
describe: """
|
||||||
|
@ -103,7 +176,8 @@ defmodule AshPostgres.DataLayer do
|
||||||
""",
|
""",
|
||||||
sections: [
|
sections: [
|
||||||
@manage_tenant,
|
@manage_tenant,
|
||||||
@references
|
@references,
|
||||||
|
@check_constraints
|
||||||
],
|
],
|
||||||
modules: [
|
modules: [
|
||||||
:repo
|
:repo
|
||||||
|
@ -548,26 +622,23 @@ defmodule AshPostgres.DataLayer do
|
||||||
record
|
record
|
||||||
|> set_table(changeset, type)
|
|> set_table(changeset, type)
|
||||||
|> Ecto.Changeset.change(changeset.attributes)
|
|> Ecto.Changeset.change(changeset.attributes)
|
||||||
|
|> add_configured_foreign_key_constraints(record.__struct__)
|
||||||
|
|> add_unique_indexes(record.__struct__, changeset.tenant, changeset)
|
||||||
|
|> add_check_constraints(record.__struct__)
|
||||||
|
|
||||||
case type do
|
case type do
|
||||||
:create ->
|
:create ->
|
||||||
ecto_changeset
|
ecto_changeset
|
||||||
|> add_unique_indexes(record.__struct__, changeset.tenant, changeset)
|
|
||||||
|> add_my_foreign_key_constraints(record.__struct__)
|
|> add_my_foreign_key_constraints(record.__struct__)
|
||||||
|> add_configured_foreign_key_constraints(record.__struct__)
|
|
||||||
|
|
||||||
type when type in [:upsert, :update] ->
|
type when type in [:upsert, :update] ->
|
||||||
ecto_changeset
|
ecto_changeset
|
||||||
|> add_unique_indexes(record.__struct__, changeset.tenant, changeset)
|
|
||||||
|> add_my_foreign_key_constraints(record.__struct__)
|
|> add_my_foreign_key_constraints(record.__struct__)
|
||||||
|> add_related_foreign_key_constraints(record.__struct__)
|
|> add_related_foreign_key_constraints(record.__struct__)
|
||||||
|> add_configured_foreign_key_constraints(record.__struct__)
|
|
||||||
|
|
||||||
:delete ->
|
:delete ->
|
||||||
ecto_changeset
|
ecto_changeset
|
||||||
|> add_unique_indexes(record.__struct__, changeset.tenant, changeset)
|
|
||||||
|> add_related_foreign_key_constraints(record.__struct__)
|
|> add_related_foreign_key_constraints(record.__struct__)
|
||||||
|> add_configured_foreign_key_constraints(record.__struct__)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -585,6 +656,21 @@ defmodule AshPostgres.DataLayer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp add_check_constraints(changeset, resource) do
|
||||||
|
resource
|
||||||
|
|> AshPostgres.check_constraints()
|
||||||
|
|> Enum.reduce(changeset, fn constraint, changeset ->
|
||||||
|
constraint.attribute
|
||||||
|
|> List.wrap()
|
||||||
|
|> Enum.reduce(changeset, fn attribute, changeset ->
|
||||||
|
Ecto.Changeset.check_constraint(changeset, attribute,
|
||||||
|
name: constraint.name,
|
||||||
|
message: constraint.message || "is invalid"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp add_related_foreign_key_constraints(changeset, resource) do
|
defp add_related_foreign_key_constraints(changeset, resource) do
|
||||||
# TODO: this doesn't guarantee us to get all of them, because if something is related to this
|
# TODO: this doesn't guarantee us to get all of them, because if something is related to this
|
||||||
# schema and there is no back-relation, then this won't catch it's foreign key constraints
|
# schema and there is no back-relation, then this won't catch it's foreign key constraints
|
||||||
|
|
|
@ -841,9 +841,26 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
name in keys || (multitenancy.attribute && name == multitenancy.attribute)
|
name in keys || (multitenancy.attribute && name == multitenancy.attribute)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp after?(
|
||||||
|
%Operation.AddCheckConstraint{
|
||||||
|
constraint: %{attribute: attribute_or_attributes},
|
||||||
|
table: table,
|
||||||
|
multitenancy: multitenancy
|
||||||
|
},
|
||||||
|
%Operation.AddAttribute{table: table, attribute: %{name: name}}
|
||||||
|
) do
|
||||||
|
name in List.wrap(attribute_or_attributes) ||
|
||||||
|
(multitenancy.attribute && multitenancy.attribute in List.wrap(attribute_or_attributes))
|
||||||
|
end
|
||||||
|
|
||||||
defp after?(%Operation.AddUniqueIndex{table: table}, %Operation.RemoveUniqueIndex{table: table}),
|
defp after?(%Operation.AddUniqueIndex{table: table}, %Operation.RemoveUniqueIndex{table: table}),
|
||||||
do: true
|
do: true
|
||||||
|
|
||||||
|
defp after?(%Operation.AddCheckConstraint{table: table}, %Operation.RemoveCheckConstraint{
|
||||||
|
table: table
|
||||||
|
}),
|
||||||
|
do: true
|
||||||
|
|
||||||
defp after?(
|
defp after?(
|
||||||
%Operation.AddUniqueIndex{identity: %{keys: keys}, table: table},
|
%Operation.AddUniqueIndex{identity: %{keys: keys}, table: table},
|
||||||
%Operation.AlterAttribute{table: table, new_attribute: %{name: name}}
|
%Operation.AlterAttribute{table: table, new_attribute: %{name: name}}
|
||||||
|
@ -858,6 +875,26 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
name in keys
|
name in keys
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp after?(
|
||||||
|
%Operation.AddCheckConstraint{
|
||||||
|
constraint: %{attribute: attribute_or_attributes},
|
||||||
|
table: table
|
||||||
|
},
|
||||||
|
%Operation.AlterAttribute{table: table, new_attribute: %{name: name}}
|
||||||
|
) do
|
||||||
|
name in List.wrap(attribute_or_attributes)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp after?(
|
||||||
|
%Operation.AddCheckConstraint{
|
||||||
|
constraint: %{attribute: attribute_or_attributes},
|
||||||
|
table: table
|
||||||
|
},
|
||||||
|
%Operation.RenameAttribute{table: table, new_attribute: %{name: name}}
|
||||||
|
) do
|
||||||
|
name in List.wrap(attribute_or_attributes)
|
||||||
|
end
|
||||||
|
|
||||||
defp after?(
|
defp after?(
|
||||||
%Operation.RemoveUniqueIndex{identity: %{keys: keys}, table: table},
|
%Operation.RemoveUniqueIndex{identity: %{keys: keys}, table: table},
|
||||||
%Operation.RemoveAttribute{table: table, attribute: %{name: name}}
|
%Operation.RemoveAttribute{table: table, attribute: %{name: name}}
|
||||||
|
@ -872,6 +909,20 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
name in keys
|
name in keys
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp after?(
|
||||||
|
%Operation.RemoveCheckConstraint{constraint: %{attribute: attributes}, table: table},
|
||||||
|
%Operation.RemoveAttribute{table: table, attribute: %{name: name}}
|
||||||
|
) do
|
||||||
|
name in List.wrap(attributes)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp after?(
|
||||||
|
%Operation.RemoveCheckConstraint{constraint: %{attribute: attributes}, table: table},
|
||||||
|
%Operation.RenameAttribute{table: table, old_attribute: %{name: name}}
|
||||||
|
) do
|
||||||
|
name in List.wrap(attributes)
|
||||||
|
end
|
||||||
|
|
||||||
defp after?(%Operation.AlterAttribute{table: table}, %Operation.DropForeignKey{
|
defp after?(%Operation.AlterAttribute{table: table}, %Operation.DropForeignKey{
|
||||||
table: table,
|
table: table,
|
||||||
direction: :up
|
direction: :up
|
||||||
|
@ -960,6 +1011,10 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp after?(%Operation.AddCheckConstraint{table: table}, %Operation.CreateTable{table: table}) do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
defp after?(%Operation.AlterAttribute{new_attribute: %{references: references}}, _)
|
defp after?(%Operation.AlterAttribute{new_attribute: %{references: references}}, _)
|
||||||
when not is_nil(references),
|
when not is_nil(references),
|
||||||
do: true
|
do: true
|
||||||
|
@ -982,6 +1037,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
empty_snapshot = %{
|
empty_snapshot = %{
|
||||||
attributes: [],
|
attributes: [],
|
||||||
identities: [],
|
identities: [],
|
||||||
|
check_constraints: [],
|
||||||
table: snapshot.table,
|
table: snapshot.table,
|
||||||
repo: snapshot.repo,
|
repo: snapshot.repo,
|
||||||
multitenancy: %{
|
multitenancy: %{
|
||||||
|
@ -1039,7 +1095,42 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
[unique_indexes_to_remove, attribute_operations, unique_indexes_to_add, acc]
|
constraints_to_add =
|
||||||
|
snapshot.check_constraints
|
||||||
|
|> Enum.reject(fn constraint ->
|
||||||
|
Enum.find(old_snapshot.check_constraints, fn old_constraint ->
|
||||||
|
old_constraint.check == constraint.check && old_constraint.name == constraint.name
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|> Enum.map(fn constraint ->
|
||||||
|
%Operation.AddCheckConstraint{
|
||||||
|
constraint: constraint,
|
||||||
|
table: snapshot.table
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
constraints_to_remove =
|
||||||
|
old_snapshot.check_constraints
|
||||||
|
|> Enum.reject(fn old_constraint ->
|
||||||
|
Enum.find(snapshot.check_constraints, fn constraint ->
|
||||||
|
old_constraint.check == constraint.check && old_constraint.name == constraint.name
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|> Enum.map(fn old_constraint ->
|
||||||
|
%Operation.RemoveCheckConstraint{
|
||||||
|
constraint: old_constraint,
|
||||||
|
table: old_snapshot.table
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
[
|
||||||
|
unique_indexes_to_remove,
|
||||||
|
attribute_operations,
|
||||||
|
unique_indexes_to_add,
|
||||||
|
constraints_to_add,
|
||||||
|
constraints_to_remove,
|
||||||
|
acc
|
||||||
|
]
|
||||||
|> Enum.concat()
|
|> Enum.concat()
|
||||||
|> Enum.map(&Map.put(&1, :multitenancy, snapshot.multitenancy))
|
|> Enum.map(&Map.put(&1, :multitenancy, snapshot.multitenancy))
|
||||||
|> Enum.map(&Map.put(&1, :old_multitenancy, old_snapshot.multitenancy))
|
|> Enum.map(&Map.put(&1, :old_multitenancy, old_snapshot.multitenancy))
|
||||||
|
@ -1338,6 +1429,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
attributes: attributes(resource),
|
attributes: attributes(resource),
|
||||||
identities: identities(resource),
|
identities: identities(resource),
|
||||||
table: table || AshPostgres.table(resource),
|
table: table || AshPostgres.table(resource),
|
||||||
|
check_constraints: check_constraints(resource),
|
||||||
repo: AshPostgres.repo(resource),
|
repo: AshPostgres.repo(resource),
|
||||||
multitenancy: multitenancy(resource),
|
multitenancy: multitenancy(resource),
|
||||||
base_filter: AshPostgres.base_filter_sql(resource),
|
base_filter: AshPostgres.base_filter_sql(resource),
|
||||||
|
@ -1358,6 +1450,37 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
|> Enum.any?(&(&1.type == :create))
|
|> Enum.any?(&(&1.type == :create))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp check_constraints(resource) do
|
||||||
|
resource
|
||||||
|
|> AshPostgres.check_constraints()
|
||||||
|
|> Enum.filter(& &1.check)
|
||||||
|
|> case do
|
||||||
|
[] ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
constraints ->
|
||||||
|
base_filter = Ash.Resource.Info.base_filter(resource)
|
||||||
|
|
||||||
|
if base_filter && !AshPostgres.base_filter_sql(resource) do
|
||||||
|
raise """
|
||||||
|
Cannot create a check constraint for a resource with a base filter without also configuring `base_filter_sql`.
|
||||||
|
|
||||||
|
You must provide the `base_filter_sql` option, or manually create add the check constraint to your migrations.
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
constraints
|
||||||
|
end
|
||||||
|
|> Enum.map(fn constraint ->
|
||||||
|
%{
|
||||||
|
name: constraint.name,
|
||||||
|
attribute: List.wrap(constraint.attribute),
|
||||||
|
check: constraint.check,
|
||||||
|
base_filter: AshPostgres.base_filter_sql(resource)
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp multitenancy(resource) do
|
defp multitenancy(resource) do
|
||||||
strategy = Ash.Resource.Info.multitenancy_strategy(resource)
|
strategy = Ash.Resource.Info.multitenancy_strategy(resource)
|
||||||
attribute = Ash.Resource.Info.multitenancy_attribute(resource)
|
attribute = Ash.Resource.Info.multitenancy_attribute(resource)
|
||||||
|
@ -1452,7 +1575,9 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
|
|
||||||
if base_filter && !AshPostgres.base_filter_sql(resource) do
|
if base_filter && !AshPostgres.base_filter_sql(resource) do
|
||||||
raise """
|
raise """
|
||||||
Currently, ash_postgres cannot translate your base_filter #{inspect(base_filter)} into sql. You must provide the `base_filter_sql` option, or skip unique indexes with `skip_unique_indexes`"
|
Cannot create a unique index for a resource with a base filter without also configuring `base_filter_sql`.
|
||||||
|
|
||||||
|
You must provide the `base_filter_sql` option, or skip unique indexes with `skip_unique_indexes`"
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1533,6 +1658,8 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
|> Map.update!(:attributes, fn attributes ->
|
|> Map.update!(:attributes, fn attributes ->
|
||||||
Enum.map(attributes, &load_attribute(&1, snapshot.table))
|
Enum.map(attributes, &load_attribute(&1, snapshot.table))
|
||||||
end)
|
end)
|
||||||
|
|> Map.put_new(:check_constraints, [])
|
||||||
|
|> Map.update!(:check_constraints, &load_check_constraints/1)
|
||||||
|> Map.update!(:repo, &String.to_atom/1)
|
|> Map.update!(:repo, &String.to_atom/1)
|
||||||
|> Map.put_new(:multitenancy, %{
|
|> Map.put_new(:multitenancy, %{
|
||||||
attribute: nil,
|
attribute: nil,
|
||||||
|
@ -1542,6 +1669,16 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
|> Map.update!(:multitenancy, &load_multitenancy/1)
|
|> Map.update!(:multitenancy, &load_multitenancy/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp load_check_constraints(constraints) do
|
||||||
|
Enum.map(constraints, fn constraint ->
|
||||||
|
Map.update!(constraint, :attribute, fn attribute ->
|
||||||
|
attribute
|
||||||
|
|> List.wrap()
|
||||||
|
|> Enum.map(&String.to_atom/1)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp load_multitenancy(multitenancy) do
|
defp load_multitenancy(multitenancy) do
|
||||||
multitenancy
|
multitenancy
|
||||||
|> Map.update!(:strategy, fn strategy -> strategy && String.to_atom(strategy) end)
|
|> Map.update!(:strategy, fn strategy -> strategy && String.to_atom(strategy) end)
|
||||||
|
|
|
@ -546,4 +546,55 @@ defmodule AshPostgres.MigrationGenerator.Operation do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defmodule AddCheckConstraint do
|
||||||
|
@moduledoc false
|
||||||
|
defstruct [:table, :constraint, :multitenancy, :old_multitenancy, no_phase: true]
|
||||||
|
|
||||||
|
def up(%{
|
||||||
|
constraint: %{
|
||||||
|
name: name,
|
||||||
|
check: check,
|
||||||
|
base_filter: base_filter
|
||||||
|
},
|
||||||
|
table: table
|
||||||
|
}) do
|
||||||
|
if base_filter do
|
||||||
|
"create constraint(:#{table}, :#{name}, check: \"#{base_filter} AND #{check}\")"
|
||||||
|
else
|
||||||
|
"create constraint(:#{table}, :#{name}, check: \"#{check}\")"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down(%{
|
||||||
|
constraint: %{name: name},
|
||||||
|
table: table
|
||||||
|
}) do
|
||||||
|
"drop_if_exists constraint(:#{table}, :#{name})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule RemoveCheckConstraint do
|
||||||
|
@moduledoc false
|
||||||
|
defstruct [:table, :constraint, :multitenancy, :old_multitenancy, no_phase: true]
|
||||||
|
|
||||||
|
def up(%{constraint: %{name: name}, table: table}) do
|
||||||
|
"drop_if_exists constraint(:#{table}, :#{name})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down(%{
|
||||||
|
constraint: %{
|
||||||
|
name: name,
|
||||||
|
check: check,
|
||||||
|
base_filter: base_filter
|
||||||
|
},
|
||||||
|
table: table
|
||||||
|
}) do
|
||||||
|
if base_filter do
|
||||||
|
"create constraint(:#{table}, :#{name}, check: \"#{base_filter} AND #{check}\")"
|
||||||
|
else
|
||||||
|
"create constraint(:#{table}, :#{name}, check: \"#{check}\")"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"uuid_generate_v4()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "id",
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "resource_id",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"destination_field": "id",
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "comment_ratings_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"table": "comments"
|
||||||
|
},
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "score",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "bigint"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "38D98E7F5B0891597BF0D1A1632638954CB7721E01310F989A41D1379E470DF8",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.AshPostgres.TestRepo",
|
||||||
|
"table": "comment_ratings"
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"uuid_generate_v4()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "id",
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "likes",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "post_id",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"destination_field": "id",
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "special_name_fkey",
|
||||||
|
"on_delete": "delete",
|
||||||
|
"on_update": "update",
|
||||||
|
"table": "posts"
|
||||||
|
},
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "title",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "AC27C8DBB84D6687136A96A5E1393042E31235B847365E5DB26DE57F5FC55604",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.AshPostgres.TestRepo",
|
||||||
|
"table": "comments"
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": true,
|
||||||
|
"name": "id",
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "title",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "38D842CAAC012D0973D6A661E8EF0C429BDB535E3AB78EC4F24C951ED323FEFE",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.AshPostgres.TestRepo",
|
||||||
|
"table": "integer_posts"
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"uuid_generate_v4()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "id",
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "resource_id",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"destination_field": "id",
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "post_ratings_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"table": "posts"
|
||||||
|
},
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "score",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "bigint"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "04586179DB8456574D279F2B6BE7AAEAC78D41E556572970B373607084782402",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.AshPostgres.TestRepo",
|
||||||
|
"table": "post_ratings"
|
||||||
|
}
|
88
priv/resource_snapshots/test_repo/posts/20210419181749.json
Normal file
88
priv/resource_snapshots/test_repo/posts/20210419181749.json
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "category",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "citext"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"uuid_generate_v4()\")",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "id",
|
||||||
|
"primary_key?": true,
|
||||||
|
"references": null,
|
||||||
|
"type": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "price",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "public",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "score",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "title",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "\"sponsored\"",
|
||||||
|
"generated?": false,
|
||||||
|
"name": "type",
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": "type = 'sponsored'",
|
||||||
|
"check_constraints": [
|
||||||
|
{
|
||||||
|
"attribute": [
|
||||||
|
"price"
|
||||||
|
],
|
||||||
|
"base_filter": "type = 'sponsored'",
|
||||||
|
"check": "price > 0",
|
||||||
|
"name": "price_must_be_positive"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "AF0084C380DBCD40FCCB82B49E7B36299087EBC0F46E2664524F47C0757D8EB6",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.AshPostgres.TestRepo",
|
||||||
|
"table": "posts"
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
defmodule AshPostgres.TestRepo.Migrations.MigrateResources7 do
|
||||||
|
@moduledoc """
|
||||||
|
Updates resources based on their most recent snapshots.
|
||||||
|
|
||||||
|
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:posts) do
|
||||||
|
modify :score, :bigint
|
||||||
|
add :price, :bigint
|
||||||
|
end
|
||||||
|
|
||||||
|
create constraint(:posts, :price_must_be_positive, check: "type = 'sponsored' AND price > 0")
|
||||||
|
|
||||||
|
alter table(:post_ratings) do
|
||||||
|
modify :score, :bigint
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:integer_posts) do
|
||||||
|
modify :id, :bigint
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:comments) do
|
||||||
|
modify :likes, :bigint
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:comment_ratings) do
|
||||||
|
modify :score, :bigint
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:comment_ratings) do
|
||||||
|
modify :score, :integer
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:comments) do
|
||||||
|
modify :likes, :integer
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:integer_posts) do
|
||||||
|
modify :id, :integer
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:post_ratings) do
|
||||||
|
modify :score, :integer
|
||||||
|
end
|
||||||
|
|
||||||
|
drop_if_exists constraint(:posts, :price_must_be_positive)
|
||||||
|
|
||||||
|
alter table(:posts) do
|
||||||
|
remove :price
|
||||||
|
modify :score, :integer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
test/constraint_test.exs
Normal file
15
test/constraint_test.exs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
defmodule AshPostgres.ConstraintTest do
|
||||||
|
@moduledoc false
|
||||||
|
use AshPostgres.RepoCase, async: false
|
||||||
|
alias AshPostgres.Test.{Api, Post}
|
||||||
|
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
|
test "constraint messages are properly raised" do
|
||||||
|
assert_raise Ash.Error.Invalid, ~r/yo, bad price/, fn ->
|
||||||
|
Post
|
||||||
|
|> Ash.Changeset.new(%{title: "title", price: -1})
|
||||||
|
|> Api.create!()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -559,6 +559,95 @@ defmodule AshPostgres.MigrationGeneratorTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "check constraints" do
|
||||||
|
setup do
|
||||||
|
on_exit(fn ->
|
||||||
|
File.rm_rf!("test_snapshots_path")
|
||||||
|
File.rm_rf!("test_migration_path")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when added, the constraint is created" do
|
||||||
|
defposts do
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key(:id)
|
||||||
|
attribute(:price, :integer)
|
||||||
|
end
|
||||||
|
|
||||||
|
postgres do
|
||||||
|
check_constraints do
|
||||||
|
check_constraint(:price, "price_must_be_positive", check: "price > 0")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defapi([Post])
|
||||||
|
|
||||||
|
AshPostgres.MigrationGenerator.generate(Api,
|
||||||
|
snapshot_path: "test_snapshots_path",
|
||||||
|
migration_path: "test_migration_path",
|
||||||
|
quiet: true,
|
||||||
|
format: false
|
||||||
|
)
|
||||||
|
|
||||||
|
assert file =
|
||||||
|
"test_migration_path/**/*_migrate_resources*.exs"
|
||||||
|
|> Path.wildcard()
|
||||||
|
|> Enum.sort()
|
||||||
|
|> Enum.at(0)
|
||||||
|
|
||||||
|
assert File.read!(file) =~
|
||||||
|
~S[create constraint(:posts, :price_must_be_positive, check: "price > 0")]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when removed, the constraint is dropped before modification" do
|
||||||
|
defposts do
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key(:id)
|
||||||
|
attribute(:price, :integer)
|
||||||
|
end
|
||||||
|
|
||||||
|
postgres do
|
||||||
|
check_constraints do
|
||||||
|
check_constraint(:price, "price_must_be_positive", check: "price > 0")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defapi([Post])
|
||||||
|
|
||||||
|
AshPostgres.MigrationGenerator.generate(Api,
|
||||||
|
snapshot_path: "test_snapshots_path",
|
||||||
|
migration_path: "test_migration_path",
|
||||||
|
quiet: true,
|
||||||
|
format: false
|
||||||
|
)
|
||||||
|
|
||||||
|
defposts do
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key(:id)
|
||||||
|
attribute(:price, :integer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
AshPostgres.MigrationGenerator.generate(Api,
|
||||||
|
snapshot_path: "test_snapshots_path",
|
||||||
|
migration_path: "test_migration_path",
|
||||||
|
quiet: true,
|
||||||
|
format: false
|
||||||
|
)
|
||||||
|
|
||||||
|
assert file =
|
||||||
|
"test_migration_path/**/*_migrate_resources*.exs"
|
||||||
|
|> Path.wildcard()
|
||||||
|
|> Enum.sort()
|
||||||
|
|> Enum.at(1)
|
||||||
|
|
||||||
|
assert File.read!(file) =~
|
||||||
|
~S[drop_if_exists constraint(:posts, :price_must_be_positive)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "polymorphic resources" do
|
describe "polymorphic resources" do
|
||||||
setup do
|
setup do
|
||||||
on_exit(fn ->
|
on_exit(fn ->
|
||||||
|
|
|
@ -6,6 +6,14 @@ defmodule AshPostgres.Test.Post do
|
||||||
postgres do
|
postgres do
|
||||||
table "posts"
|
table "posts"
|
||||||
repo AshPostgres.TestRepo
|
repo AshPostgres.TestRepo
|
||||||
|
base_filter_sql "type = 'sponsored'"
|
||||||
|
|
||||||
|
check_constraints do
|
||||||
|
check_constraint(:price, "price_must_be_positive",
|
||||||
|
message: "yo, bad price",
|
||||||
|
check: "price > 0"
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resource do
|
resource do
|
||||||
|
@ -35,6 +43,7 @@ defmodule AshPostgres.Test.Post do
|
||||||
attribute(:public, :boolean)
|
attribute(:public, :boolean)
|
||||||
attribute(:category, :ci_string)
|
attribute(:category, :ci_string)
|
||||||
attribute(:type, :atom, default: :sponsored, private?: true, writable?: false)
|
attribute(:type, :atom, default: :sponsored, private?: true, writable?: false)
|
||||||
|
attribute(:price, :integer)
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
|
|
Loading…
Reference in a new issue