mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-19 13:03:14 +12:00
feat: support base_filter (#18)
This commit is contained in:
parent
4299cbaa05
commit
51cb9c1a68
8 changed files with 331 additions and 178 deletions
|
@ -1,6 +1,12 @@
|
|||
# THIS FILE IS AUTOGENERATED USING `mix ash.formatter`
|
||||
# DONT MODIFY IT BY HAND
|
||||
locals_without_parens = [migrate?: 1, repo: 1, table: 1]
|
||||
locals_without_parens = [
|
||||
base_filter_sql: 1,
|
||||
migrate?: 1,
|
||||
repo: 1,
|
||||
skip_unique_indexes: 1,
|
||||
table: 1
|
||||
]
|
||||
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
|
|
|
@ -9,12 +9,12 @@ defmodule AshPostgres do
|
|||
|
||||
alias Ash.Dsl.Extension
|
||||
|
||||
@doc "Fetch the configured repo for a resource"
|
||||
@doc "The configured repo for a resource"
|
||||
def repo(resource) do
|
||||
Extension.get_opt(resource, [:postgres], :repo, nil, true)
|
||||
end
|
||||
|
||||
@doc "Fetch the configured table for a resource"
|
||||
@doc "The configured table for a resource"
|
||||
def table(resource) do
|
||||
Extension.get_opt(resource, [:postgres], :table, nil, true)
|
||||
end
|
||||
|
@ -23,4 +23,14 @@ defmodule AshPostgres do
|
|||
def migrate?(resource) do
|
||||
Extension.get_opt(resource, [:postgres], :migrate?, nil, true)
|
||||
end
|
||||
|
||||
@doc "A stringified version of the base_filter, to be used in a where clause when generating unique indexes"
|
||||
def base_filter_sql(resource) do
|
||||
Extension.get_opt(resource, [:postgres], :base_filter_sql, nil)
|
||||
end
|
||||
|
||||
@doc "Skip generating unique indexes when generating migrations"
|
||||
def skip_unique_indexes?(resource) do
|
||||
Extension.get_opt(resource, [:postgres], :skip_unique_indexes?, [])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -47,6 +47,16 @@ defmodule AshPostgres.DataLayer do
|
|||
doc:
|
||||
"Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations`"
|
||||
],
|
||||
base_filter_sql: [
|
||||
type: :string,
|
||||
doc:
|
||||
"A raw sql version of the base_filter, e.g `representative = true`. Required if trying to create a unique constraint on a resource with a base_filter"
|
||||
],
|
||||
skip_unique_indexes: [
|
||||
type: {:custom, __MODULE__, :validate_skip_unique_indexes, []},
|
||||
default: false,
|
||||
doc: "Skip generating unique indexes when generating migrations"
|
||||
],
|
||||
table: [
|
||||
type: :string,
|
||||
required: true,
|
||||
|
@ -75,6 +85,17 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def validate_skip_unique_indexes(indexes) do
|
||||
indexes = List.wrap(indexes)
|
||||
|
||||
if Enum.all?(indexes, &is_atom/1) do
|
||||
{:ok, indexes}
|
||||
else
|
||||
{:error, "All indexes to skip must be atoms"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def custom_filters(resource) do
|
||||
config = repo(resource).config()
|
||||
|
@ -154,6 +175,10 @@ defmodule AshPostgres.DataLayer do
|
|||
@impl true
|
||||
def offset(query, nil, _), do: query
|
||||
|
||||
def offset(%{offset: old_offset} = query, 0, _resource) when old_offset in [0, nil] do
|
||||
{:ok, query}
|
||||
end
|
||||
|
||||
def offset(query, offset, _resource) do
|
||||
{:ok, from(row in query, offset: ^offset)}
|
||||
end
|
||||
|
@ -183,8 +208,10 @@ defmodule AshPostgres.DataLayer do
|
|||
)
|
||||
)
|
||||
|
||||
data_layer_query = Ash.Query.new(source_resource).data_layer_query
|
||||
|
||||
query =
|
||||
from(source in resource_to_query(source_resource),
|
||||
from(source in data_layer_query,
|
||||
as: :source_record,
|
||||
where: field(source, ^source_field) in ^source_values,
|
||||
inner_lateral_join: destination in ^subquery,
|
||||
|
@ -793,15 +820,20 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
|
||||
defp add_filter_expression(query, filter) do
|
||||
filter
|
||||
|> split_and_statements()
|
||||
|> Enum.reduce(query, fn filter, query ->
|
||||
clause = filter_to_dynamic_expr(filter, query.__ash_bindings__.bindings)
|
||||
wheres =
|
||||
filter
|
||||
|> split_and_statements()
|
||||
|> Enum.map(fn filter ->
|
||||
{params, expr} = filter_to_expr(filter, query.__ash_bindings__.bindings, [])
|
||||
|
||||
from(row in query,
|
||||
where: ^clause
|
||||
)
|
||||
end)
|
||||
%Ecto.Query.BooleanExpr{
|
||||
expr: expr,
|
||||
op: :and,
|
||||
params: params
|
||||
}
|
||||
end)
|
||||
|
||||
%{query | wheres: query.wheres ++ wheres}
|
||||
end
|
||||
|
||||
defp split_and_statements(%Filter{expression: expression}) do
|
||||
|
@ -826,113 +858,6 @@ defmodule AshPostgres.DataLayer do
|
|||
|
||||
defp split_and_statements(other), do: [other]
|
||||
|
||||
defp filter_to_dynamic_expr(%Filter{expression: expression}, bindings) do
|
||||
filter_to_dynamic_expr(expression, bindings)
|
||||
end
|
||||
|
||||
defp filter_to_dynamic_expr(nil, _), do: true
|
||||
defp filter_to_dynamic_expr(true, _), do: true
|
||||
defp filter_to_dynamic_expr(false, _), do: false
|
||||
|
||||
defp filter_to_dynamic_expr(%Expression{op: :and, left: left, right: right}, bindings) do
|
||||
left = filter_to_dynamic_expr(left, bindings)
|
||||
right = filter_to_dynamic_expr(right, bindings)
|
||||
Ecto.Query.dynamic([row], ^left and ^right)
|
||||
end
|
||||
|
||||
defp filter_to_dynamic_expr(%Expression{op: :or, left: left, right: right}, bindings) do
|
||||
left = filter_to_dynamic_expr(left, bindings)
|
||||
right = filter_to_dynamic_expr(right, bindings)
|
||||
Ecto.Query.dynamic([row], ^left or ^right)
|
||||
end
|
||||
|
||||
defp filter_to_dynamic_expr(%Not{expression: expression}, bindings) do
|
||||
expression = filter_to_dynamic_expr(expression, bindings)
|
||||
|
||||
Ecto.Query.dynamic([row], not (^expression))
|
||||
end
|
||||
|
||||
defp filter_to_dynamic_expr(%Predicate{} = pred, bindings) do
|
||||
%{predicate: predicate, relationship_path: relationship_path, attribute: attribute} = pred
|
||||
|
||||
current_binding =
|
||||
case attribute do
|
||||
%Ash.Resource.Attribute{} ->
|
||||
Enum.find_value(bindings, fn {binding, data} ->
|
||||
data.path == relationship_path && data.type in [:left, :inner, :root] && binding
|
||||
end)
|
||||
|
||||
%Ash.Query.Aggregate{} = aggregate ->
|
||||
Enum.find_value(bindings, fn {binding, data} ->
|
||||
data.path == aggregate.relationship_path && data.type == :aggregate && binding
|
||||
end)
|
||||
end
|
||||
|
||||
type = Ash.Type.ecto_type(attribute.type)
|
||||
|
||||
filter_value_to_dynamic_expr(attribute, predicate, type, current_binding)
|
||||
end
|
||||
|
||||
defp filter_value_to_dynamic_expr(attribute, %Eq{value: value}, _type, current_binding) do
|
||||
Ecto.Query.dynamic([{row, current_binding}], field(row, ^attribute.name) == ^value)
|
||||
end
|
||||
|
||||
defp filter_value_to_dynamic_expr(attribute, %LessThan{value: value}, _type, current_binding) do
|
||||
Ecto.Query.dynamic([{row, current_binding}], field(row, ^attribute.name) < ^value)
|
||||
end
|
||||
|
||||
defp filter_value_to_dynamic_expr(attribute, %GreaterThan{value: value}, _type, current_binding) do
|
||||
Ecto.Query.dynamic([{row, current_binding}], field(row, ^attribute.name) > ^value)
|
||||
end
|
||||
|
||||
defp filter_value_to_dynamic_expr(attribute, %In{values: values}, _type, current_binding) do
|
||||
Ecto.Query.dynamic([{row, current_binding}], field(row, ^attribute.name) in ^values)
|
||||
end
|
||||
|
||||
defp filter_value_to_dynamic_expr(attribute, %IsNil{nil?: true}, _type, current_binding) do
|
||||
Ecto.Query.dynamic([{row, current_binding}], is_nil(field(row, ^attribute.name)))
|
||||
end
|
||||
|
||||
defp filter_value_to_dynamic_expr(attribute, %IsNil{nil?: false}, _type, current_binding) do
|
||||
Ecto.Query.dynamic([{row, current_binding}], not is_nil(field(row, ^attribute.name)))
|
||||
end
|
||||
|
||||
defp filter_value_to_dynamic_expr(attribute, %Trigram{} = trigram, _type, current_binding) do
|
||||
case trigram do
|
||||
%{equals: nil, greater_than: greater_than, less_than: nil, text: text} ->
|
||||
Ecto.Query.dynamic(
|
||||
[{row, current_binding}],
|
||||
fragment("similarity(?, ?) > ?", field(row, ^attribute.name), ^text, ^greater_than)
|
||||
)
|
||||
|
||||
%{equals: nil, greater_than: nil, less_than: less_than, text: text} ->
|
||||
Ecto.Query.dynamic(
|
||||
[{row, current_binding}],
|
||||
fragment("similarity(?, ?) < ?", field(row, ^attribute.name), ^text, ^less_than)
|
||||
)
|
||||
|
||||
%{equals: nil, greater_than: greater_than, less_than: less_than, text: text} ->
|
||||
Ecto.Query.dynamic(
|
||||
[{row, current_binding}],
|
||||
fragment(
|
||||
"similarity(?, ?) BETWEEN ? AND ?",
|
||||
field(row, ^attribute.name),
|
||||
^text,
|
||||
^less_than,
|
||||
^greater_than
|
||||
)
|
||||
)
|
||||
|
||||
%{equals: equals, text: text} ->
|
||||
Ecto.Query.dynamic(
|
||||
[{row, current_binding}],
|
||||
fragment("similarity(?, ?) = ?", field(row, ^attribute.name), ^text, ^equals)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# IMPORTANT: We need to rework this so we don't need this hacky logic.
|
||||
# Specifically, we can't use dynamic expers in selects, so we need this for aggregates :(
|
||||
defp filter_to_expr(%Filter{expression: expression}, bindings, params) do
|
||||
filter_to_expr(expression, bindings, params)
|
||||
end
|
||||
|
@ -962,7 +887,7 @@ defmodule AshPostgres.DataLayer do
|
|||
case attribute do
|
||||
%Ash.Resource.Attribute{} ->
|
||||
Enum.find_value(bindings, fn {binding, data} ->
|
||||
data.path == relationship_path && data.type in [:left, :root] && binding
|
||||
data.path == relationship_path && data.type in [:inner, :left, :root] && binding
|
||||
end)
|
||||
|
||||
%Ash.Query.Aggregate{} = aggregate ->
|
||||
|
@ -971,60 +896,111 @@ defmodule AshPostgres.DataLayer do
|
|||
end)
|
||||
end
|
||||
|
||||
type = Ash.Type.ecto_type(attribute.type)
|
||||
|
||||
filter_value_to_expr(attribute.name, predicate, type, current_binding, params)
|
||||
filter_value_to_expr(
|
||||
attribute.name,
|
||||
predicate,
|
||||
attribute.type,
|
||||
current_binding,
|
||||
params,
|
||||
pred.embedded
|
||||
)
|
||||
end
|
||||
|
||||
defp filter_value_to_expr(attribute, %Eq{value: value}, type, current_binding, params) do
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%Eq{value: value},
|
||||
type,
|
||||
current_binding,
|
||||
params,
|
||||
embedded?
|
||||
) do
|
||||
simple_operator_expr(
|
||||
:==,
|
||||
params,
|
||||
value,
|
||||
type,
|
||||
current_binding,
|
||||
attribute
|
||||
attribute,
|
||||
embedded?
|
||||
)
|
||||
end
|
||||
|
||||
defp filter_value_to_expr(attribute, %LessThan{value: value}, type, current_binding, params) do
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%LessThan{value: value},
|
||||
type,
|
||||
current_binding,
|
||||
params,
|
||||
embedded?
|
||||
) do
|
||||
simple_operator_expr(
|
||||
:<,
|
||||
params,
|
||||
value,
|
||||
type,
|
||||
current_binding,
|
||||
attribute
|
||||
attribute,
|
||||
embedded?
|
||||
)
|
||||
end
|
||||
|
||||
defp filter_value_to_expr(attribute, %GreaterThan{value: value}, type, current_binding, params) do
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%GreaterThan{value: value},
|
||||
type,
|
||||
current_binding,
|
||||
params,
|
||||
embedded?
|
||||
) do
|
||||
simple_operator_expr(
|
||||
:>,
|
||||
params,
|
||||
value,
|
||||
type,
|
||||
current_binding,
|
||||
attribute
|
||||
attribute,
|
||||
embedded?
|
||||
)
|
||||
end
|
||||
|
||||
defp filter_value_to_expr(attribute, %In{values: values}, type, current_binding, params) do
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%In{values: values},
|
||||
type,
|
||||
current_binding,
|
||||
params,
|
||||
embedded?
|
||||
) do
|
||||
simple_operator_expr(
|
||||
:in,
|
||||
params,
|
||||
values,
|
||||
{:in, type},
|
||||
current_binding,
|
||||
attribute
|
||||
attribute,
|
||||
embedded?
|
||||
)
|
||||
end
|
||||
|
||||
defp filter_value_to_expr(attribute, %IsNil{nil?: true}, _type, current_binding, params) do
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%IsNil{nil?: true},
|
||||
_type,
|
||||
current_binding,
|
||||
params,
|
||||
_embedded?
|
||||
) do
|
||||
{params, {:is_nil, [], [{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []}]}}
|
||||
end
|
||||
|
||||
defp filter_value_to_expr(attribute, %IsNil{nil?: false}, _type, current_binding, params) do
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%IsNil{nil?: false},
|
||||
_type,
|
||||
current_binding,
|
||||
params,
|
||||
_embedded?
|
||||
) do
|
||||
{params,
|
||||
{:not, [], [{:is_nil, [], [{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []}]}]}}
|
||||
end
|
||||
|
@ -1034,13 +1010,14 @@ defmodule AshPostgres.DataLayer do
|
|||
%Trigram{} = trigram,
|
||||
_type,
|
||||
current_binding,
|
||||
params
|
||||
params,
|
||||
false
|
||||
) do
|
||||
param_count = Enum.count(params)
|
||||
|
||||
case trigram do
|
||||
%{equals: equals, greater_than: nil, less_than: nil, text: text} ->
|
||||
{params ++ [{text, {current_binding, attribute}}, {equals, :float}],
|
||||
{params ++ [{text, :string}, {equals, :float}],
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
|
@ -1053,7 +1030,7 @@ defmodule AshPostgres.DataLayer do
|
|||
]}}
|
||||
|
||||
%{equals: nil, greater_than: greater_than, less_than: nil, text: text} ->
|
||||
{params ++ [{text, {current_binding, attribute}}, {greater_than, :float}],
|
||||
{params ++ [{text, :string}, {greater_than, :float}],
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
|
@ -1066,7 +1043,7 @@ defmodule AshPostgres.DataLayer do
|
|||
]}}
|
||||
|
||||
%{equals: nil, greater_than: nil, less_than: less_than, text: text} ->
|
||||
{params ++ [{text, {current_binding, attribute}}, {less_than, :float}],
|
||||
{params ++ [{text, :string}, {less_than, :float}],
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
|
@ -1080,7 +1057,7 @@ defmodule AshPostgres.DataLayer do
|
|||
|
||||
%{equals: nil, greater_than: greater_than, less_than: less_than, text: text} ->
|
||||
{params ++
|
||||
[{text, {current_binding, attribute}}, {less_than, :float}, {greater_than, :float}],
|
||||
[{text, :string}, {less_than, :float}, {greater_than, :float}],
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
|
@ -1096,8 +1073,73 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp simple_operator_expr(op, params, value, type, current_binding, attribute) do
|
||||
{params ++ [{value, type}],
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%Trigram{} = trigram,
|
||||
_type,
|
||||
current_binding,
|
||||
params,
|
||||
true
|
||||
) do
|
||||
case trigram do
|
||||
%{equals: equals, greater_than: nil, less_than: nil, text: text} ->
|
||||
{params,
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
raw: ", ",
|
||||
expr: tagged(text, :string),
|
||||
raw: ") = ",
|
||||
expr: tagged(equals, :float),
|
||||
raw: ""
|
||||
]}}
|
||||
|
||||
%{equals: nil, greater_than: greater_than, less_than: nil, text: text} ->
|
||||
{params,
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
raw: ", ",
|
||||
expr: tagged(text, :string),
|
||||
raw: ") > ",
|
||||
expr: tagged(greater_than, :float),
|
||||
raw: ""
|
||||
]}}
|
||||
|
||||
%{equals: nil, greater_than: nil, less_than: less_than, text: text} ->
|
||||
{params,
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
raw: ", ",
|
||||
expr: tagged(text, :string),
|
||||
raw: ") < ",
|
||||
expr: tagged(less_than, :float),
|
||||
raw: ""
|
||||
]}}
|
||||
|
||||
%{equals: nil, greater_than: greater_than, less_than: less_than, text: text} ->
|
||||
{params,
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
raw: ", ",
|
||||
expr: tagged(text, :string),
|
||||
raw: ") BETWEEN ",
|
||||
expr: tagged(less_than, :float),
|
||||
raw: " AND ",
|
||||
expr: tagged(greater_than, :float),
|
||||
raw: ""
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
defp simple_operator_expr(op, params, value, type, current_binding, attribute, false) do
|
||||
{params ++ [{value, Ash.Type.ecto_type(type)}],
|
||||
{op, [],
|
||||
[
|
||||
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
|
@ -1105,6 +1147,31 @@ defmodule AshPostgres.DataLayer do
|
|||
]}}
|
||||
end
|
||||
|
||||
defp simple_operator_expr(op, params, value, type, current_binding, attribute, true) do
|
||||
{params,
|
||||
{op, [],
|
||||
[
|
||||
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
tagged(value, type)
|
||||
]}}
|
||||
end
|
||||
|
||||
defp tagged(value, type) do
|
||||
%Ecto.Query.Tagged{value: value, type: get_type(type)}
|
||||
end
|
||||
|
||||
defp get_type({:array, type}) do
|
||||
{:array, get_type(type)}
|
||||
end
|
||||
|
||||
defp get_type(type) do
|
||||
if Ash.Type.ash_type?(type) do
|
||||
Ash.Type.storage_type(type)
|
||||
else
|
||||
type
|
||||
end
|
||||
end
|
||||
|
||||
defp add_binding(query, data) do
|
||||
current = query.__ash_bindings__.current
|
||||
bindings = query.__ash_bindings__.bindings
|
||||
|
@ -1129,6 +1196,9 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
|
||||
defp maybe_get_resource_query(resource) do
|
||||
{table(resource), resource}
|
||||
case Ash.Query.data_layer_query(Ash.Query.new(resource), only_validate_filter?: false) do
|
||||
{:ok, query} -> query
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,11 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
|
||||
alias AshPostgres.MigrationGenerator.{Operation, Phase}
|
||||
|
||||
defstruct snapshot_path: @default_snapshot_path, migration_path: nil, quiet: false, format: true
|
||||
defstruct snapshot_path: @default_snapshot_path,
|
||||
migration_path: nil,
|
||||
quiet: false,
|
||||
format: true,
|
||||
dry_run: false
|
||||
|
||||
def generate(apis, opts \\ []) do
|
||||
apis = List.wrap(apis)
|
||||
|
@ -78,8 +82,19 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
new_snapshot.identities
|
||||
|> Kernel.++(identities)
|
||||
|> Enum.sort_by(& &1.name)
|
||||
# We sort the identities by there being an identity with a matching name in the existing snapshot
|
||||
# so that we prefer identities that currently exist over new ones
|
||||
|> Enum.sort_by(fn identity ->
|
||||
existing_snapshot
|
||||
|> Kernel.||(%{})
|
||||
|> Map.get(:identities, [])
|
||||
|> Enum.any?(fn existing_identity ->
|
||||
existing_identity.name == identity.name
|
||||
end)
|
||||
|> Kernel.!()
|
||||
end)
|
||||
|> Enum.uniq_by(fn identity ->
|
||||
Enum.sort(identity.keys)
|
||||
{Enum.sort(identity.keys), identity.base_filter}
|
||||
end)
|
||||
|
||||
new_snapshot = %{new_snapshot | identities: all_identities}
|
||||
|
@ -253,17 +268,19 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
defp write_migration({up, down}, snapshots, repo, opts) do
|
||||
repo_name = repo |> Module.split() |> List.last() |> Macro.underscore()
|
||||
|
||||
Enum.each(snapshots, fn snapshot ->
|
||||
snapshot_binary = snapshot_to_binary(snapshot)
|
||||
unless opts.dry_run do
|
||||
Enum.each(snapshots, fn snapshot ->
|
||||
snapshot_binary = snapshot_to_binary(snapshot)
|
||||
|
||||
snapshot_file =
|
||||
opts.snapshot_path
|
||||
|> Path.join(repo_name)
|
||||
|> Path.join(snapshot.table <> ".json")
|
||||
snapshot_file =
|
||||
opts.snapshot_path
|
||||
|> Path.join(repo_name)
|
||||
|> Path.join(snapshot.table <> ".json")
|
||||
|
||||
File.mkdir_p(Path.dirname(snapshot_file))
|
||||
File.write!(snapshot_file, snapshot_binary, [])
|
||||
end)
|
||||
File.mkdir_p(Path.dirname(snapshot_file))
|
||||
File.write!(snapshot_file, snapshot_binary, [])
|
||||
end)
|
||||
end
|
||||
|
||||
migration_path =
|
||||
if opts.migration_path do
|
||||
|
@ -309,7 +326,11 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
end
|
||||
"""
|
||||
|
||||
create_file(migration_file, format(contents, opts))
|
||||
if opts.dry_run do
|
||||
Mix.shell().info(format(contents, opts))
|
||||
else
|
||||
create_file(migration_file, format(contents, opts))
|
||||
end
|
||||
end
|
||||
|
||||
defp build_up_and_down(phases) do
|
||||
|
@ -419,6 +440,10 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
group_into_phases(rest, %{phase | operations: [op | phase.operations]}, acc)
|
||||
end
|
||||
|
||||
defp group_into_phases([%{no_phase: true} = op | rest], nil, acc) do
|
||||
group_into_phases(rest, nil, [op | acc])
|
||||
end
|
||||
|
||||
defp group_into_phases([operation | rest], nil, acc) do
|
||||
group_into_phases(rest, nil, [
|
||||
%Phase.Alter{operations: [operation], table: operation.table} | acc
|
||||
|
@ -594,7 +619,8 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
old_snapshot.identities
|
||||
|> Enum.reject(fn old_identity ->
|
||||
Enum.find(snapshot.identities, fn identity ->
|
||||
Enum.sort(old_identity.keys) == Enum.sort(identity.keys)
|
||||
Enum.sort(old_identity.keys) == Enum.sort(identity.keys) &&
|
||||
old_identity.base_filter == identity.base_filter
|
||||
end)
|
||||
end)
|
||||
|> Enum.map(fn identity ->
|
||||
|
@ -605,11 +631,15 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
snapshot.identities
|
||||
|> Enum.reject(fn identity ->
|
||||
Enum.find(old_snapshot.identities, fn old_identity ->
|
||||
Enum.sort(old_identity.keys) == Enum.sort(identity.keys)
|
||||
Enum.sort(old_identity.keys) == Enum.sort(identity.keys) &&
|
||||
old_identity.base_filter == identity.base_filter
|
||||
end)
|
||||
end)
|
||||
|> Enum.map(fn identity ->
|
||||
%Operation.AddUniqueIndex{identity: identity, table: snapshot.table}
|
||||
%Operation.AddUniqueIndex{
|
||||
identity: identity,
|
||||
table: snapshot.table
|
||||
}
|
||||
end)
|
||||
|
||||
attribute_operations ++ unique_indexes_to_add ++ unique_indexes_to_remove ++ acc
|
||||
|
@ -772,7 +802,8 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
attributes: attributes(resource),
|
||||
identities: identities(resource),
|
||||
table: AshPostgres.table(resource),
|
||||
repo: AshPostgres.repo(resource)
|
||||
repo: AshPostgres.repo(resource),
|
||||
base_filter: AshPostgres.base_filter_sql(resource)
|
||||
}
|
||||
|
||||
hash =
|
||||
|
@ -834,13 +865,35 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
defp identities(resource) do
|
||||
resource
|
||||
|> Ash.Resource.identities()
|
||||
|> case do
|
||||
[] ->
|
||||
[]
|
||||
|
||||
identities ->
|
||||
base_filter = Ash.Resource.base_filter(resource)
|
||||
|
||||
if base_filter && !AshPostgres.base_filter_sql(resource) do
|
||||
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`"
|
||||
"""
|
||||
end
|
||||
|
||||
identities
|
||||
end
|
||||
|> Enum.reject(fn identity ->
|
||||
identity.name in AshPostgres.skip_unique_indexes?(resource)
|
||||
end)
|
||||
|> Enum.filter(fn identity ->
|
||||
Enum.all?(identity.keys, fn key ->
|
||||
Ash.Resource.attribute(resource, key)
|
||||
end)
|
||||
end)
|
||||
|> Enum.map(fn identity ->
|
||||
%{identity | keys: Enum.sort(identity.keys)}
|
||||
end)
|
||||
|> Enum.sort_by(& &1.name)
|
||||
|> Enum.map(&Map.take(&1, [:name, :keys]))
|
||||
|> Enum.map(&Map.put(&1, :base_filter, AshPostgres.base_filter_sql(resource)))
|
||||
end
|
||||
|
||||
if :erlang.function_exported(Ash, :uuid, 0) do
|
||||
|
@ -904,7 +957,10 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
identity
|
||||
|> Map.update!(:name, &String.to_atom/1)
|
||||
|> Map.update!(:keys, fn keys ->
|
||||
Enum.map(keys, &String.to_atom/1)
|
||||
keys
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
|> Enum.sort()
|
||||
end)
|
||||
|> Map.put_new(:base_filter, nil)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -111,30 +111,35 @@ defmodule AshPostgres.MigrationGenerator.Operation do
|
|||
|
||||
defmodule AddUniqueIndex do
|
||||
@moduledoc false
|
||||
defstruct [:identity, :table]
|
||||
defstruct [:identity, :table, no_phase: true]
|
||||
|
||||
def up(%{identity: %{name: name, keys: keys}, table: table}) do
|
||||
"create unique_index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{table}_#{
|
||||
name
|
||||
}_unique_index\")"
|
||||
def up(%{identity: %{name: name, keys: keys, base_filter: base_filter}, table: table}) do
|
||||
if base_filter do
|
||||
"create unique_index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{
|
||||
table
|
||||
}_#{name}_unique_index\", where: \"#{base_filter}\")"
|
||||
else
|
||||
"create unique_index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{
|
||||
table
|
||||
}_#{name}_unique_index\")"
|
||||
end
|
||||
end
|
||||
|
||||
def down(%{identity: %{name: name, keys: keys}, table: table}) do
|
||||
# {
|
||||
"drop unique_index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{table}_#{
|
||||
name
|
||||
}_unique_index\")"
|
||||
"drop_if_exists unique_index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{
|
||||
table
|
||||
}_#{name}_unique_index\")"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule RemoveUniqueIndex do
|
||||
@moduledoc false
|
||||
defstruct [:identity, :table]
|
||||
defstruct [:identity, :table, no_phase: true]
|
||||
|
||||
def up(%{identity: %{name: name, keys: keys}, table: table}) do
|
||||
"drop unique_index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{table}_#{
|
||||
name
|
||||
}_unique_index\")"
|
||||
"drop_if_exists unique_index(:#{table}, [#{Enum.map_join(keys, ",", &inspect/1)}], name: \"#{
|
||||
table
|
||||
}_#{name}_unique_index\")"
|
||||
end
|
||||
|
||||
def down(%{identity: %{name: name, keys: keys}, table: table}) do
|
||||
|
|
|
@ -12,8 +12,8 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do
|
|||
Flags:
|
||||
|
||||
* `quiet` - messages for file creations will not be printed
|
||||
* `format` - files that are created will be formatted with the code formatter, defaults to true
|
||||
|
||||
* `no_format` - files that are created will not be formatted with the code formatter
|
||||
* `dry_run` - no files are created, instead the new migration is printed
|
||||
|
||||
#### Conflicts/Multiple Resources
|
||||
|
||||
|
@ -52,7 +52,8 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do
|
|||
snapshot_path: :string,
|
||||
migration_path: :string,
|
||||
quiet: :boolean,
|
||||
format: :boolean
|
||||
no_format: :boolean,
|
||||
dry_run: :boolean
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -83,6 +84,11 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do
|
|||
raise "must supply the --apis argument, or set `config :my_app, apis: [...]` in config"
|
||||
end
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.put(:format, !opts[:no_format])
|
||||
|> Keyword.delete(:no_format)
|
||||
|
||||
AshPostgres.MigrationGenerator.generate(apis, opts)
|
||||
end
|
||||
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -66,7 +66,7 @@ defmodule AshPostgres.MixProject do
|
|||
[
|
||||
{:ecto_sql, "~> 3.4"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:ash, ash_version("~> 1.13.2")},
|
||||
{:ash, ash_version("~> 1.13.3")},
|
||||
{:git_ops, "~> 2.0.1", only: :dev},
|
||||
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
|
||||
{:ex_check, "~> 0.11.0", only: :dev},
|
||||
|
|
6
mix.lock
6
mix.lock
|
@ -1,12 +1,12 @@
|
|||
%{
|
||||
"ash": {:hex, :ash, "1.13.2", "e3f0f2d831e69f956f78e69501cff39e3701566e6f00bf21c01ad8c172eeac0c", [:mix], [{:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.4", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "e3d44a9f123d126ced1614ba69a62af092210de78ddb64be6dc8849a850be158"},
|
||||
"ash": {:hex, :ash, "1.13.3", "b44000ff10d057179c92b171ba89f950cc1a743b1e6f10d8a3029ba026bd6770", [:mix], [{:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.4", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "bdc2a4df6bd2adf8dc586d5cead740cfa5cce732b3a98f4a0ca8b1614b947462"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
|
||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
|
||||
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
|
||||
"dataloader": {:hex, :dataloader, "1.0.6", "fb724d6d3fb6acb87d27e3b32dea3a307936ad2d245faf9cf5221d1323d6a4ba", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
|
||||
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
|
||||
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
|
||||
"earmark": {:hex, :earmark, "1.4.5", "62ffd3bd7722fb7a7b1ecd2419ea0b458c356e7168c1f5d65caf09b4fbdd13c8", [:mix], [], "hexpm", "b7d0e6263d83dc27141a523467799a685965bf8b13b6743413f19a7079843f4f"},
|
||||
"ecto": {:hex, :ecto, "3.4.6", "08f7afad3257d6eb8613309af31037e16c36808dfda5a3cd0cb4e9738db030e4", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6f13a9e2a62e75c2dcfc7207bfc65645ab387af8360db4c89fee8b5a4bf3f70b"},
|
||||
|
@ -21,7 +21,7 @@
|
|||
"git_ops": {:hex, :git_ops, "2.0.1", "9d3df6c710a80a8779dbb144c79fb24c777660ae862cc454ab3193afd0c02a37", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cd499a72523ba338c20973eadb707d25a42e4a77c46d2ff5c45e61e7adae6190"},
|
||||
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
|
||||
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
|
||||
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
|
||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||
"machinery": {:hex, :machinery, "1.0.0", "df6968d84c651b9971a33871c78c10157b6e13e4f3390b0bee5b0e8bdea8c781", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "4f6eb4185a48e7245360bedf653af4acc6fa6ae8ff4690619395543fa1a8395f"},
|
||||
"makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
|
||||
|
|
Loading…
Reference in a new issue