improvement: support custom_indexes

This commit is contained in:
Zach Daniel 2021-09-20 16:38:36 -04:00
parent d5dad1b04a
commit be1fbd6137
6 changed files with 314 additions and 2 deletions

View file

@ -5,9 +5,13 @@ locals_without_parens = [
check: 1,
check_constraint: 2,
check_constraint: 3,
concurrently: 1,
create?: 1,
foreign_key_names: 1,
identity_index_names: 1,
include: 1,
index: 1,
index: 2,
message: 1,
migrate?: 1,
name: 1,
@ -17,14 +21,18 @@ locals_without_parens = [
polymorphic_name: 1,
polymorphic_on_delete: 1,
polymorphic_on_update: 1,
prefix: 1,
reference: 1,
reference: 2,
repo: 1,
skip_unique_indexes: 1,
table: 1,
template: 1,
unique: 1,
unique_index_names: 1,
update?: 1
update?: 1,
using: 1,
where: 1
]
[

View file

@ -29,6 +29,11 @@ defmodule AshPostgres do
Extension.get_entities(resource, [:postgres, :check_constraints])
end
@doc "The configured custom_indexes for a resource"
def custom_indexes(resource) do
Extension.get_entities(resource, [:postgres, :custom_indexes])
end
@doc "The configured polymorphic_reference_on_delete for a resource"
def polymorphic_on_delete(resource) do
Extension.get_opt(resource, [:postgres, :references], :polymorphic_on_delete, nil, true)

69
lib/custom_index.ex Normal file
View file

@ -0,0 +1,69 @@
defmodule AshPostgres.CustomIndex do
@moduledoc false
defstruct [
:table,
:fields,
:name,
:unique,
:concurrently,
:using,
:prefix,
:where,
:include
]
@schema [
fields: [
type: {:list, :string},
doc: "The fields to include in the index."
],
name: [
type: :string,
doc: "the name of the index. Defaults to \"\#\{table\}_\#\{column\}_index\"."
],
unique: [
type: :boolean,
doc: "indicates whether the index should be unique.",
default: false
],
concurrently: [
type: :boolean,
doc: "indicates whether the index should be created/dropped concurrently.",
default: false
],
using: [
type: :string,
doc: "configures the index type."
],
prefix: [
type: :string,
doc: "specify an optional prefix for the index."
],
where: [
type: :string,
doc: "specify conditions for a partial index."
],
include: [
type: {:list, :string},
doc:
"specify fields for a covering index. This is not supported by all databases. For more information on PostgreSQL support, please read the official docs."
]
]
def schema, do: @schema
def name(_resource, %{name: name}) when is_binary(name) do
name
end
# sobelow_skip ["DOS.StringToAtom"]
def name(table, %{fields: fields}) do
[table, fields, "index"]
|> List.flatten()
|> Enum.map(&to_string(&1))
|> Enum.map(&String.replace(&1, ~r"[^\w_]", "_"))
|> Enum.map(&String.replace_trailing(&1, "_", ""))
|> Enum.join("_")
|> String.to_atom()
end
end

View file

@ -42,6 +42,39 @@ defmodule AshPostgres.DataLayer do
]
}
@index %Ash.Dsl.Entity{
name: :index,
describe: """
Add an index to be managed by the migration generator.
""",
examples: [
"index [\"column\", \"column2\"], unique: true, where: \"thing = TRUE\""
],
target: AshPostgres.CustomIndex,
schema: AshPostgres.CustomIndex.schema(),
args: [:fields]
}
@custom_indexes %Ash.Dsl.Section{
name: :custom_indexes,
describe: """
A section for configuring indexes to be created by the migration generator.
In general, prefer to use `identities` for simple unique constraints. This is a tool to allow
for declaring more complex indexes.
""",
examples: [
"""
custom_indexes do
index [:column1, :column2], unique: true, where: "thing = TRUE"
end
"""
],
entities: [
@index
]
}
@reference %Ash.Dsl.Entity{
name: :reference,
describe: """
@ -175,6 +208,7 @@ defmodule AshPostgres.DataLayer do
Postgres data layer configuration
""",
sections: [
@custom_indexes,
@manage_tenant,
@references,
@check_constraints

View file

@ -906,6 +906,15 @@ defmodule AshPostgres.MigrationGenerator do
(multitenancy.attribute && multitenancy.attribute in List.wrap(attribute_or_attributes))
end
defp after?(
%Operation.AddCustomIndex{
table: table
},
%Operation.AddAttribute{table: table}
) do
true
end
defp after?(%Operation.AddCheckConstraint{table: table}, %Operation.RemoveCheckConstraint{
table: table
}),
@ -1075,9 +1084,11 @@ defmodule AshPostgres.MigrationGenerator do
empty_snapshot = %{
attributes: [],
identities: [],
custom_indexes: [],
check_constraints: [],
table: snapshot.table,
repo: snapshot.repo,
base_filter: nil,
multitenancy: %{
attribute: nil,
strategy: nil,
@ -1100,6 +1111,37 @@ defmodule AshPostgres.MigrationGenerator do
rewrite_all_identities? = changing_multitenancy_affects_identities?(snapshot, old_snapshot)
custom_indexes_to_add =
Enum.filter(snapshot.custom_indexes, fn index ->
!Enum.find(old_snapshot.custom_indexes, fn old_custom_index ->
old_custom_index == index
end)
end)
|> Enum.map(fn custom_index ->
%Operation.AddCustomIndex{
index: custom_index,
table: old_snapshot.table,
multitenancy: old_snapshot.multitenancy,
base_filter: old_snapshot.base_filter
}
end)
custom_indexes_to_remove =
Enum.filter(old_snapshot.custom_indexes, fn old_custom_index ->
rewrite_all_identities? ||
!Enum.find(snapshot.custom_indexes, fn index ->
old_custom_index == index
end)
end)
|> Enum.map(fn custom_index ->
%Operation.RemoveCustomIndex{
index: custom_index,
table: snapshot.table,
multitenancy: snapshot.multitenancy,
base_filter: snapshot.base_filter
}
end)
unique_indexes_to_remove =
if rewrite_all_identities? do
old_snapshot.identities
@ -1193,6 +1235,8 @@ defmodule AshPostgres.MigrationGenerator do
unique_indexes_to_rename,
constraints_to_add,
constraints_to_remove,
custom_indexes_to_add,
custom_indexes_to_remove,
acc
]
|> Enum.concat()
@ -1305,7 +1349,8 @@ defmodule AshPostgres.MigrationGenerator do
end
def changing_multitenancy_affects_identities?(snapshot, old_snapshot) do
snapshot.multitenancy != old_snapshot.multitenancy
snapshot.multitenancy != old_snapshot.multitenancy ||
snapshot.base_filter != old_snapshot.base_filter
end
def has_reference?(multitenancy, attribute) do
@ -1504,6 +1549,7 @@ defmodule AshPostgres.MigrationGenerator do
identities: identities(resource),
table: table || AshPostgres.table(resource),
check_constraints: check_constraints(resource),
custom_indexes: custom_indexes(resource),
repo: AshPostgres.repo(resource),
multitenancy: multitenancy(resource),
base_filter: AshPostgres.base_filter_sql(resource),
@ -1555,6 +1601,14 @@ defmodule AshPostgres.MigrationGenerator do
end)
end
defp custom_indexes(resource) do
resource
|> AshPostgres.custom_indexes()
|> Enum.map(fn custom_index ->
Map.from_struct(custom_index)
end)
end
defp multitenancy(resource) do
strategy = Ash.Resource.Info.multitenancy_strategy(resource)
attribute = Ash.Resource.Info.multitenancy_attribute(resource)
@ -1750,6 +1804,8 @@ defmodule AshPostgres.MigrationGenerator do
|> Map.update!(:attributes, fn attributes ->
Enum.map(attributes, &load_attribute(&1, snapshot.table))
end)
|> Map.put_new(:custom_indexes, [])
|> Map.update!(:custom_indexes, &load_custom_indexes/1)
|> Map.put_new(:check_constraints, [])
|> Map.update!(:check_constraints, &load_check_constraints/1)
|> Map.update!(:repo, &String.to_atom/1)
@ -1759,6 +1815,7 @@ defmodule AshPostgres.MigrationGenerator do
global: nil
})
|> Map.update!(:multitenancy, &load_multitenancy/1)
|> Map.put_new(:base_filter, nil)
end
defp load_check_constraints(constraints) do
@ -1771,6 +1828,25 @@ defmodule AshPostgres.MigrationGenerator do
end)
end
defp load_custom_indexes(custom_indexes) do
Enum.map(custom_indexes, fn custom_index ->
custom_index
|> Map.update!(:table, &String.to_existing_atom/1)
|> Map.update!(
:fields,
&Enum.map(&1, fn field ->
String.to_existing_atom(field)
end)
)
|> Map.update!(
:include,
&Enum.map(&1, fn field ->
String.to_existing_atom(field)
end)
)
end)
end
defp load_multitenancy(multitenancy) do
multitenancy
|> Map.update!(:strategy, fn strategy -> strategy && String.to_atom(strategy) end)

View file

@ -20,6 +20,12 @@ defmodule AshPostgres.MigrationGenerator.Operation do
def maybe_add_null(false), do: "null: false"
def maybe_add_null(_), do: nil
def option(key, value) do
if value do
"#{key}: #{inspect(value)}"
end
end
def on_delete(%{on_delete: on_delete}) when on_delete in [:delete, :nilify] do
"on_delete: :#{on_delete}_all"
end
@ -563,6 +569,120 @@ defmodule AshPostgres.MigrationGenerator.Operation do
end
end
defmodule AddCustomIndex do
@moduledoc false
defstruct [:table, :index, :base_filter, :multitenancy, no_phase: true]
import Helper
def up(%{
index: index,
table: table,
base_filter: base_filter,
multitenancy: multitenancy
}) do
keys =
case multitenancy.strategy do
:attribute ->
[to_string(multitenancy.attribute) | index.fields]
_ ->
index.fields
end
index =
if index.where && base_filter do
%{index | where: base_filter <> " AND " <> index.where}
else
index
end
opts =
join([
option(:name, index.name),
option(:unique, index.unique),
option(:concurrently, index.concurrently),
option(:using, index.using),
option(:prefix, index.prefix),
option(:where, index.where),
option(:include, index.include)
])
"create index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], #{opts})"
end
def down(%{index: index, table: table, multitenancy: multitenancy}) do
index_name = AshPostgres.CustomIndex.name(table, index)
keys =
case multitenancy.strategy do
:attribute ->
[to_string(multitenancy.attribute) | index.fields]
_ ->
index.fields
end
"drop_if_exists index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{index_name}\")"
end
end
defmodule RemoveCustomIndex do
@moduledoc false
defstruct [:table, :index, :base_filter, :multitenancy, no_phase: true]
import Helper
def up(%{index: index, table: table, multitenancy: multitenancy}) do
index_name = AshPostgres.CustomIndex.name(table, index)
keys =
case multitenancy.strategy do
:attribute ->
[to_string(multitenancy.attribute) | index.fields]
_ ->
index.fields
end
"drop_if_exists index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{index_name}\")"
end
def down(%{
index: index,
table: table,
base_filter: base_filter,
multitenancy: multitenancy
}) do
keys =
case multitenancy.strategy do
:attribute ->
[to_string(multitenancy.attribute) | index.fields]
_ ->
index.fields
end
index =
if index.where && base_filter do
%{index | where: base_filter <> " AND " <> index.where}
else
index
end
opts =
join([
option(:name, index.name),
option(:unique, index.unique),
option(:concurrently, index.concurrently),
option(:using, index.using),
option(:prefix, index.prefix),
option(:where, index.where),
option(:include, index.include)
])
"create index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], #{opts})"
end
end
defmodule RenameUniqueIndex do
@moduledoc false
defstruct [