mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-19 21:13:19 +12:00
improvement: support custom_indexes
This commit is contained in:
parent
d5dad1b04a
commit
be1fbd6137
6 changed files with 314 additions and 2 deletions
|
@ -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
|
||||
]
|
||||
|
||||
[
|
||||
|
|
|
@ -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
69
lib/custom_index.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 [
|
||||
|
|
Loading…
Reference in a new issue