improvement: add custom_statements to migration generator

This commit is contained in:
Zach Daniel 2022-07-21 13:34:38 -04:00
parent 16c0497916
commit e20e68e73a
6 changed files with 212 additions and 6 deletions

View file

@ -5,8 +5,10 @@ locals_without_parens = [
check: 1,
check_constraint: 2,
check_constraint: 3,
code?: 1,
concurrently: 1,
create?: 1,
down: 1,
exclusion_constraint_names: 1,
foreign_key_names: 1,
identity_index_names: 1,
@ -30,10 +32,13 @@ locals_without_parens = [
repo: 1,
schema: 1,
skip_unique_indexes: 1,
statement: 1,
statement: 2,
table: 1,
template: 1,
unique: 1,
unique_index_names: 1,
up: 1,
update?: 1,
using: 1,
where: 1

View file

@ -44,6 +44,11 @@ defmodule AshPostgres do
Extension.get_entities(resource, [:postgres, :custom_indexes])
end
@doc "The configured custom_statements for a resource"
def custom_statements(resource) do
Extension.get_entities(resource, [:postgres, :custom_statements])
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)

View file

@ -75,6 +75,55 @@ defmodule AshPostgres.DataLayer do
]
}
@statement %Ash.Dsl.Entity{
name: :statement,
describe: """
Add a custom statement for migrations.
""",
examples: [
"""
statement :pgweb_idx do
up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));"
down "DROP INDEX pgweb_idx;"
end
"""
],
target: AshPostgres.Statement,
schema: AshPostgres.Statement.schema(),
args: [:name]
}
@custom_statements %Ash.Dsl.Section{
name: :custom_statements,
describe: """
A section for configuring custom statements to be added to migrations.
Changing custom statements may require manual intervention, because Ash can't determine what order they should run
in (i.e if they depend on table structure that you've added, or vice versa). As such, any `down` statements we run
for custom statements happen first, and any `up` statements happen last.
Additionally, when changing a custom statement, we must make some assumptions, i.e that we should migrate
the old structure down using the previously configured `down` and recreate it.
This may not be desired, and so what you may end up doing is simply modifying the old migration and deleting whatever was
generated by the migration generator. As always: read your migrations after generating them!
""",
examples: [
"""
custom_statements do
# the name is used to detect if you remove or modify the statement
custom_statement :pgweb_idx do
up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));"
down "DROP INDEX pgweb_idx;"
end
end
"""
],
entities: [
@statement
]
}
@reference %Ash.Dsl.Entity{
name: :reference,
describe: """
@ -209,6 +258,7 @@ defmodule AshPostgres.DataLayer do
""",
sections: [
@custom_indexes,
@custom_statements,
@manage_tenant,
@references,
@check_constraints

View file

@ -342,15 +342,11 @@ defmodule AshPostgres.MigrationGenerator do
|> Enum.filter(& &1.has_create_action)
|> Enum.count()
snapshot_identities =
snapshots
|> Enum.map(& &1.identities)
|> Enum.concat()
new_snapshot = %{
snapshot
| attributes: merge_attributes(attributes, snapshot.table, count_with_create),
identities: snapshot_identities
custom_indexes: snapshots |> Enum.flat_map(& &1.custom_indexes) |> Enum.uniq(),
custom_statements: snapshots |> Enum.flat_map(& &1.custom_statements) |> Enum.uniq()
}
all_identities =
@ -944,6 +940,18 @@ defmodule AshPostgres.MigrationGenerator do
sort_operations(rest, new_acc)
end
defp after?(
%Operation.AddCustomStatement{},
_
),
do: true
defp after?(
_,
%Operation.RemoveCustomStatement{}
),
do: true
defp after?(
%Operation.AddAttribute{attribute: %{order: l}, table: table, schema: schema},
%Operation.AddAttribute{attribute: %{order: r}, table: table, schema: schema}
@ -1214,6 +1222,7 @@ defmodule AshPostgres.MigrationGenerator do
identities: [],
schema: nil,
custom_indexes: [],
custom_statements: [],
check_constraints: [],
table: snapshot.table,
repo: snapshot.repo,
@ -1241,6 +1250,37 @@ defmodule AshPostgres.MigrationGenerator do
rewrite_all_identities? = changing_multitenancy_affects_identities?(snapshot, old_snapshot)
custom_statements_to_add =
snapshot.custom_statements
|> Enum.reject(fn statement ->
Enum.any?(old_snapshot.custom_statements, &(&1.name == statement.name))
end)
|> Enum.map(&%Operation.AddCustomStatement{statement: &1, table: snapshot.table})
custom_statements_to_remove =
old_snapshot.custom_statements
|> Enum.reject(fn old_statement ->
Enum.any?(snapshot.custom_statements, &(&1.name == old_statement.name))
end)
|> Enum.map(&%Operation.RemoveCustomStatement{statement: &1, table: snapshot.table})
custom_statements_to_alter =
snapshot.custom_statements
|> Enum.flat_map(fn statement ->
old_statement = Enum.find(old_snapshot.custom_statements, &(&1.name == statement.name))
if old_statement &&
(old_statement.code? != statement.code? ||
old_statement.up != statement.up || old_statement.down != statement.down) do
[
%Operation.RemoveCustomStatement{statement: old_statement, table: snapshot.table},
%Operation.AddCustomStatement{statement: statement, table: snapshot.table}
]
else
[]
end
end)
custom_indexes_to_add =
Enum.filter(snapshot.custom_indexes, fn index ->
!Enum.find(old_snapshot.custom_indexes, fn old_custom_index ->
@ -1377,6 +1417,9 @@ defmodule AshPostgres.MigrationGenerator do
constraints_to_remove,
custom_indexes_to_add,
custom_indexes_to_remove,
custom_statements_to_add,
custom_statements_to_remove,
custom_statements_to_alter,
acc
]
|> Enum.concat()
@ -1791,6 +1834,7 @@ defmodule AshPostgres.MigrationGenerator do
schema: schema || AshPostgres.schema(resource),
check_constraints: check_constraints(resource),
custom_indexes: custom_indexes(resource),
custom_statements: custom_statements(resource),
repo: AshPostgres.repo(resource),
multitenancy: multitenancy(resource),
base_filter: AshPostgres.base_filter_sql(resource),
@ -1859,6 +1903,14 @@ defmodule AshPostgres.MigrationGenerator do
end)
end
defp custom_statements(resource) do
resource
|> AshPostgres.custom_statements()
|> Enum.map(fn custom_statement ->
Map.from_struct(custom_statement)
end)
end
defp multitenancy(resource) do
strategy = Ash.Resource.Info.multitenancy_strategy(resource)
attribute = Ash.Resource.Info.multitenancy_attribute(resource)
@ -2111,6 +2163,8 @@ defmodule AshPostgres.MigrationGenerator do
end)
|> Map.put_new(:custom_indexes, [])
|> Map.update!(:custom_indexes, &load_custom_indexes/1)
|> Map.put_new(:custom_statements, [])
|> Map.update!(:custom_statements, &load_custom_statements/1)
|> Map.put_new(:check_constraints, [])
|> Map.update!(:check_constraints, &load_check_constraints/1)
|> Map.update!(:repo, &String.to_atom/1)
@ -2141,6 +2195,12 @@ defmodule AshPostgres.MigrationGenerator do
end)
end
defp load_custom_statements(statements) do
Enum.map(statements || [], fn statement ->
Map.update!(statement, :name, &String.to_atom/1)
end)
end
defp load_multitenancy(multitenancy) do
multitenancy
|> Map.update!(:strategy, fn strategy -> strategy && String.to_atom(strategy) end)

View file

@ -713,6 +713,48 @@ defmodule AshPostgres.MigrationGenerator.Operation do
end
end
defmodule AddCustomStatement do
@moduledoc false
defstruct [:statement, :table, no_phase: true]
def up(%{statement: %{up: up, code?: false}}) do
"""
execute(\"\"\"
#{String.trim(up)}
\"\"\")
"""
end
def up(%{statement: %{up: up, code?: true}}) do
up
end
def down(%{statement: %{down: down, code?: false}}) do
"""
execute(\"\"\"
#{String.trim(down)}
\"\"\")
"""
end
def down(%{statement: %{down: down, code?: true}}) do
down
end
end
defmodule RemoveCustomStatement do
@moduledoc false
defstruct [:statement, :table, no_phase: true]
def up(%{statement: statement, table: table}) do
AddCustomStatement.down(%AddCustomStatement{statement: statement, table: table})
end
def down(%{statement: statement, table: table}) do
AddCustomStatement.up(%AddCustomStatement{statement: statement, table: table})
end
end
defmodule AddCustomIndex do
@moduledoc false
defstruct [:table, :schema, :index, :base_filter, :multitenancy, no_phase: true]

44
lib/statement.ex Normal file
View file

@ -0,0 +1,44 @@
defmodule AshPostgres.Statement do
@moduledoc false
defstruct [
:name,
:up,
:down,
:code?
]
@schema [
name: [
type: :atom,
required: true,
doc: """
The name of the statement, must be unique within the resource
"""
],
code?: [
type: :boolean,
default: false,
doc: """
Whether the provided up/down should be treated as code or sql strings.
By default, we place the strings inside of ecto migration's `execute/1`
function and assume they are sql. Use this option if you want to provide custom
elixir code to be placed directly in the migrations
"""
],
up: [
type: :string,
doc: """
How to create the structure of the statement
""",
required: true
],
down: [
type: :string,
doc: "How to tear down the structure of the statement",
required: true
]
]
def schema, do: @schema
end