feat: support configuring references

feat: support configuring polymorphic references
feat: support `distinct` Ash queries
This commit is contained in:
Zach Daniel 2021-04-01 02:19:30 -04:00
parent e7ea1f9f5f
commit 4d2d29d976
20 changed files with 872 additions and 82 deletions

View file

@ -5,7 +5,15 @@ locals_without_parens = [
create?: 1,
foreign_key_names: 1,
migrate?: 1,
name: 1,
on_delete: 1,
on_update: 1,
polymorphic?: 1,
polymorphic_name: 1,
polymorphic_on_delete: 1,
polymorphic_on_update: 1,
reference: 1,
reference: 2,
repo: 1,
skip_unique_indexes: 1,
table: 1,

View file

@ -19,6 +19,26 @@ defmodule AshPostgres do
Extension.get_opt(resource, [:postgres], :table, nil, true)
end
@doc "The configured references for a resource"
def references(resource) do
Extension.get_entities(resource, [:postgres, :references])
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)
end
@doc "The configured polymorphic_reference_on_update for a resource"
def polymorphic_on_update(resource) do
Extension.get_opt(resource, [:postgres, :references], :polymorphic_on_update, nil, true)
end
@doc "The configured polymorphic_reference_name for a resource"
def polymorphic_name(resource) do
Extension.get_opt(resource, [:postgres, :references], :polymorphic_on_delete, nil, true)
end
@doc "The configured polymorphic? for a resource"
def polymorphic?(resource) do
Extension.get_opt(resource, [:postgres], :polymorphic?, nil, true)

View file

@ -42,13 +42,68 @@ defmodule AshPostgres.DataLayer do
]
}
@reference %Ash.Dsl.Entity{
name: :reference,
describe: """
Configures the reference for a relationship in resource migrations.
Keep in mind that multiple relationships can theoretically involve the same destination and foreign keys.
In those cases, you only need to configure the `reference` behavior for one of them. Any conflicts will result
in an error, across this resource and any other resources that share a table with this one. For this reason,
instead of adding a reference configuration for `:nothing`, its best to just leave the configuration out, as that
is the default behavior if *no* relationship anywhere has configured the behavior of that reference.
""",
examples: [
"reference :post, on_delete: :delete, on_update: :update, name: \"comments_to_posts_fkey\""
],
args: [:relationship],
target: AshPostgres.Reference,
schema: AshPostgres.Reference.schema()
}
@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{
name: :postgres,
describe: """
Postgres data layer configuration
""",
sections: [
@manage_tenant
@manage_tenant,
@references
],
modules: [
:repo
@ -217,6 +272,7 @@ defmodule AshPostgres.DataLayer do
def can?(_, :nested_expressions), do: true
def can?(_, {:query_aggregate, :count}), do: true
def can?(_, :sort), do: true
def can?(_, :distinct), do: true
def can?(_, {:sort, _}), do: true
def can?(_, _), do: false
@ -703,6 +759,40 @@ defmodule AshPostgres.DataLayer do
end)
end
@impl true
def distinct(query, distinct_on, resource) do
query = default_bindings(query, resource)
query =
query
|> default_bindings(resource)
|> Map.update!(:distinct, fn distinct ->
distinct =
distinct ||
%Ecto.Query.QueryExpr{
expr: []
}
expr =
Enum.map(distinct_on, fn distinct_on_field ->
binding =
case Map.fetch(query.__ash_bindings__.aggregates, distinct_on_field) do
{:ok, binding} ->
binding
:error ->
0
end
{:asc, {{:., [], [{:&, [], [binding]}, distinct_on_field]}, [], []}}
end)
%{distinct | expr: distinct.expr ++ expr}
end)
{:ok, query}
end
defp sanitize_sort(sort) do
sort
|> List.wrap()

View file

@ -257,20 +257,13 @@ defmodule AshPostgres.MigrationGenerator do
defp merge_attributes(attributes, table, count) do
attributes
|> Enum.group_by(& &1.name)
|> Enum.map(fn
{_name, [attribute]} ->
if count > 1 do
%{attribute | allow_nil?: true}
else
attribute
end
{name, attributes} ->
|> Enum.map(fn {name, attributes} ->
%{
name: name,
type: merge_types(Enum.map(attributes, & &1.type), name, table),
default: merge_defaults(Enum.map(attributes, & &1.default)),
allow_nil?: Enum.any?(attributes, & &1.allow_nil?) || Enum.count(attributes) < count,
generated?: Enum.any?(attributes, & &1.generated?),
references: merge_references(Enum.map(attributes, & &1.references), name, table),
primary_key?: false
}
@ -285,16 +278,40 @@ defmodule AshPostgres.MigrationGenerator do
[] ->
nil
[reference] ->
reference
references ->
conflicting_table_field_names =
Enum.map_join(references, "\n", fn reference ->
"* #{reference.table}.#{reference.destination_field}"
end)
%{
destination_field: merge_uniq!(references, table, :destination_field, name),
multitenancy: merge_uniq!(references, table, :multitenancy, name),
on_delete: merge_uniq!(references, table, :on_delete, name),
on_update: merge_uniq!(references, table, :on_update, name),
name: merge_uniq!(references, table, :name, name),
table: merge_uniq!(references, table, :table, name)
}
end
end
raise "Conflicting references for `#{table}.#{name}`:\n#{conflicting_table_field_names}"
defp merge_uniq!(references, table, field, attribute) do
references
|> Enum.map(&Map.get(&1, field))
|> Enum.filter(& &1)
|> Enum.uniq()
|> case do
[] ->
nil
[value] ->
value
values ->
values = Enum.map_join(values, "\n", &" * #{inspect(&1)}")
raise """
Conflicting configurations for references for #{table}.#{attribute}:
Values:
#{values}
"""
end
end
@ -1113,7 +1130,7 @@ defmodule AshPostgres.MigrationGenerator do
snapshot_file
|> File.read!()
|> load_snapshot()
|> load_snapshot(snapshot.table)
end
else
get_old_snapshot(folder, snapshot)
@ -1128,7 +1145,7 @@ defmodule AshPostgres.MigrationGenerator do
if File.exists?(old_snapshot_file) do
old_snapshot_file
|> File.read!()
|> load_snapshot()
|> load_snapshot(snapshot.table)
end
end
@ -1212,7 +1229,12 @@ defmodule AshPostgres.MigrationGenerator do
Map.put(attribute, :references, %{
destination_field: relationship.source_field,
multitenancy: multitenancy(relationship.source),
table: AshPostgres.table(relationship.source)
table: AshPostgres.table(relationship.source),
on_delete: AshPostgres.polymorphic_on_delete(relationship.source),
on_update: AshPostgres.polymorphic_on_update(relationship.source),
name:
AshPostgres.polymorphic_name(relationship.source) ||
"#{relationship.context[:data_layer][:table]}_#{relationship.source_field}_fkey"
})
else
attribute
@ -1289,9 +1311,14 @@ defmodule AshPostgres.MigrationGenerator do
Enum.find_value(Ash.Resource.Info.relationships(resource), fn relationship ->
if attribute.name == relationship.source_field && relationship.type == :belongs_to &&
foreign_key?(relationship) do
configured_reference = configured_reference(resource, attribute.name, relationship.name)
%{
destination_field: relationship.destination_field,
multitenancy: multitenancy(relationship.destination),
on_delete: configured_reference.on_delete,
on_update: configured_reference.on_update,
name: configured_reference.name,
table:
relationship.context[:data_layer][:table] ||
AshPostgres.table(relationship.destination)
@ -1300,6 +1327,17 @@ defmodule AshPostgres.MigrationGenerator do
end)
end
defp configured_reference(resource, attribute, relationship) do
resource
|> AshPostgres.references()
|> Enum.find(&(&1.relationship == relationship))
|> Kernel.||(%{
on_delete: nil,
on_update: nil,
name: "#{AshPostgres.table(resource)}_#{attribute}_fkey"
})
end
defp migration_type({:array, type}), do: {:array, migration_type(type)}
defp migration_type(Ash.Type.CiString), do: :citext
defp migration_type(Ash.Type.UUID), do: :uuid
@ -1390,7 +1428,7 @@ defmodule AshPostgres.MigrationGenerator do
type
end
defp load_snapshot(json) do
defp load_snapshot(json, table) do
json
|> Jason.decode!(keys: :atoms!)
|> Map.put_new(:has_create_action, true)
@ -1398,7 +1436,7 @@ defmodule AshPostgres.MigrationGenerator do
Enum.map(identities, &load_identity/1)
end)
|> Map.update!(:attributes, fn attributes ->
Enum.map(attributes, &load_attribute/1)
Enum.map(attributes, &load_attribute(&1, table))
end)
|> Map.update!(:repo, &String.to_atom/1)
|> Map.put_new(:multitenancy, %{
@ -1415,7 +1453,7 @@ defmodule AshPostgres.MigrationGenerator do
|> Map.update!(:attribute, fn attribute -> attribute && String.to_atom(attribute) end)
end
defp load_attribute(attribute) do
defp load_attribute(attribute, table) do
attribute
|> Map.update!(:type, &load_type/1)
|> Map.update!(:name, &String.to_atom/1)
@ -1428,6 +1466,9 @@ defmodule AshPostgres.MigrationGenerator do
references ->
references
|> Map.update!(:destination_field, &String.to_atom/1)
|> Map.put_new(:on_delete, nil)
|> Map.put_new(:on_update, nil)
|> Map.put_new(:name, "#{table}_#{attribute.name}")
|> Map.put_new(:multitenancy, %{
attribute: nil,
strategy: nil,

View file

@ -19,6 +19,26 @@ defmodule AshPostgres.MigrationGenerator.Operation do
def maybe_add_null(false), do: "null: false"
def maybe_add_null(_), do: nil
def on_delete(%{on_delete: on_delete}) when on_delete in [:delete, :nilify] do
"on_delete: :#{on_delete}_all"
end
def on_delete(%{on_delete: on_delete}) when is_atom(on_delete) and not is_nil(on_delete) do
"on_delete: :#{on_delete}"
end
def on_delete(_), do: nil
def on_update(%{on_update: on_update}) when on_update in [:update, :nilify] do
"on_update: :#{on_update}_all"
end
def on_update(%{on_update: on_update}) when is_atom(on_update) and not is_nil(on_update) do
"on_update: :#{on_update}"
end
def on_update(_), do: nil
end
defmodule CreateTable do
@ -36,11 +56,12 @@ defmodule AshPostgres.MigrationGenerator.Operation do
multitenancy: %{strategy: :attribute, attribute: source_attribute},
attribute:
%{
references: %{
references:
%{
table: table,
destination_field: destination_field,
multitenancy: %{strategy: :attribute, attribute: destination_attribute}
}
} = reference
} = attribute
}) do
[
@ -49,7 +70,10 @@ defmodule AshPostgres.MigrationGenerator.Operation do
[
"type: #{inspect(attribute.type)}",
"column: #{inspect(destination_field)}",
"with: [#{source_attribute}: :#{destination_attribute}]"
"with: [#{source_attribute}: :#{destination_attribute}]",
"name: #{inspect(reference.name)}",
on_delete(reference),
on_update(reference)
],
")",
maybe_add_default(attribute.default),
@ -62,11 +86,12 @@ defmodule AshPostgres.MigrationGenerator.Operation do
multitenancy: %{strategy: :context},
attribute:
%{
references: %{
references:
%{
table: table,
destination_field: destination_field,
multitenancy: %{strategy: :attribute}
}
} = reference
} = attribute
}) do
[
@ -75,7 +100,10 @@ defmodule AshPostgres.MigrationGenerator.Operation do
[
"type: #{inspect(attribute.type)}",
"column: #{inspect(destination_field)}",
"prefix: \"public\""
"prefix: \"public\"",
"name: #{inspect(reference.name)}",
on_delete(reference),
on_update(reference)
],
")",
maybe_add_default(attribute.default),
@ -113,14 +141,18 @@ defmodule AshPostgres.MigrationGenerator.Operation do
def up(%{
multitenancy: %{strategy: :context},
attribute:
%{references: %{table: table, destination_field: destination_field}} = attribute
%{references: %{table: table, destination_field: destination_field} = reference} =
attribute
}) do
[
"add #{inspect(attribute.name)}",
"references(:#{table}",
[
"type: #{inspect(attribute.type)}",
"column: #{inspect(destination_field)}"
"column: #{inspect(destination_field)}",
"name: #{inspect(reference.name)}",
on_delete(reference),
on_update(reference)
],
")",
maybe_add_default(attribute.default),
@ -131,14 +163,18 @@ defmodule AshPostgres.MigrationGenerator.Operation do
def up(%{
attribute:
%{references: %{table: table, destination_field: destination_field}} = attribute
%{references: %{table: table, destination_field: destination_field} = reference} =
attribute
}) do
[
"add #{inspect(attribute.name)}",
"references(:#{table}",
[
"type: #{inspect(attribute.type)}",
"column: #{inspect(destination_field)}"
"column: #{inspect(destination_field)}",
"name: #{inspect(reference.name)}",
on_delete(reference),
on_update(reference)
],
")",
maybe_add_default(attribute.default),
@ -188,6 +224,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
@moduledoc false
defstruct [:old_attribute, :new_attribute, :table, :multitenancy, :old_multitenancy]
import Helper
defp alter_opts(attribute, old_attribute) do
primary_key =
if attribute.primary_key? and !old_attribute.primary_key? do
@ -231,52 +269,80 @@ defmodule AshPostgres.MigrationGenerator.Operation do
defp reference(%{strategy: :context}, %{
type: type,
references: %{
references:
%{
multitenancy: %{strategy: :context},
table: table,
destination_field: destination_field
}
} = reference
}) do
"references(:#{table}, type: #{inspect(type)}, column: #{inspect(destination_field)})"
join([
"references(:#{table}, type: #{inspect(type)}, column: #{inspect(destination_field)}",
"name: #{inspect(reference.name)}",
on_delete(reference),
on_update(reference),
")"
])
end
defp reference(%{strategy: :attribute, attribute: source_attribute}, %{
type: type,
references: %{
references:
%{
multitenancy: %{strategy: :attribute, attribute: destination_attribute},
table: table,
destination_field: destination_field
}
} = reference
}) do
join([
"references(:#{table}, type: #{inspect(type)}, column: #{inspect(destination_field)}, with: [#{
source_attribute
}: :#{destination_attribute}])"
}: :#{destination_attribute}]",
"name: #{inspect(reference.name)}",
on_delete(reference),
on_update(reference),
")"
])
end
defp reference(
%{strategy: :context},
%{
type: type,
references: %{
references:
%{
table: table,
destination_field: destination_field
}
} = reference
}
) do
"references(:#{table}, type: #{inspect(type)}, column: #{inspect(destination_field)}, prefix: \"public\")"
join([
"references(:#{table}, type: #{inspect(type)}, column: #{inspect(destination_field)}, prefix: \"public\"",
"name: #{inspect(reference.name)}",
on_delete(reference),
on_update(reference),
")"
])
end
defp reference(
_,
%{
type: type,
references: %{
references:
%{
table: table,
destination_field: destination_field
}
} = reference
}
) do
"references(:#{table}, type: #{inspect(type)}, column: #{inspect(destination_field)})"
join([
"references(:#{table}, type: #{inspect(type)}, column: #{inspect(destination_field)}",
"name: #{inspect(reference.name)}",
on_delete(reference),
on_update(reference),
")"
])
end
def down(op) do
@ -297,14 +363,14 @@ defmodule AshPostgres.MigrationGenerator.Operation do
# We only need to drop it before altering an attribute with `references/3`
defstruct [:attribute, :table, :multitenancy, :direction, no_phase: true]
def up(%{attribute: attribute, table: table, direction: :up}) do
"drop constraint(:#{table}, \"#{table}_#{attribute.name}_fkey\")"
def up(%{table: table, references: reference, direction: :up}) do
"drop constraint(:#{table}, #{inspect(reference.name)})"
end
def up(_), do: ""
def down(%{attribute: attribute, table: table, direction: :down}) do
"drop constraint(:#{table}, \"#{table}_#{attribute.name}_fkey\")"
def down(%{table: table, references: reference, direction: :down}) do
"drop constraint(:#{table}, #{inspect(reference.name)})"
end
def down(_), do: ""

56
lib/reference.ex Normal file
View file

@ -0,0 +1,56 @@
defmodule AshPostgres.Reference do
@moduledoc """
Contains configuration for a database reference
"""
defstruct [:relationship, :on_delete, :on_update, :name]
def schema do
[
relationship: [
type: :atom,
required: true,
doc: "The relationship to be configured"
],
on_delete: [
type: {:one_of, [:delete, :nilify, :nothing, :restrict]},
doc: """
What should happen to records of this resource when the referenced record of the *destination* resource is deleted.
The difference between `:nothing` and `:restrict` is subtle and, if you are unsure, choose `:nothing` (the default behavior).
`:restrict` will prevent the deletion from happening *before* the end of the database transaction, whereas `:nothing` allows the
transaction to complete before doing so. This allows for things like deleting the destination row and *then* deleting the source
row.
## Important!
No resource logic is applied with this operation! No authorization rules or validations take place, and no notifications are issued.
This operation happens *directly* in the database.
This option is called `on_delete`, instead of `on_destroy`, because it is hooking into the database level deletion, *not*
a `destroy` action in your resource.
"""
],
on_update: [
type: {:one_of, [:update, :nilify, :nothing, :restrict]},
doc: """
What should happen to records of this resource when the referenced destination_field of the *destination* record is update.
The difference between `:nothing` and `:restrict` is subtle and, if you are unsure, choose `:nothing` (the default behavior).
`:restrict` will prevent the deletion from happening *before* the end of the database transaction, whereas `:nothing` allows the
transaction to complete before doing so. This allows for things like updating the destination row and *then* updating the reference
as long as you are in a transaction.
## Important!
No resource logic is applied with this operation! No authorization rules or validations take place, and no notifications are issued.
This operation happens *directly* in the database.
"""
],
name: [
type: :string,
doc:
"The name of the foreign key to generate in the database. Defaults to <table>_<source_field>_fkey"
]
]
end
end

View file

@ -95,7 +95,7 @@ defmodule AshPostgres.MixProject do
{:ecto_sql, "~> 3.5"},
{:jason, "~> 1.0"},
{:postgrex, ">= 0.0.0"},
{:ash, ash_version("~> 1.34 and >= 1.34.6")},
{:ash, ash_version("~> 1.38")},
{:git_ops, "~> 2.0.1", only: :dev},
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:ex_check, "~> 0.11.0", only: :dev},

View file

@ -1,7 +1,7 @@
%{
"ash": {:hex, :ash, "1.34.6", "def814f707b5f325a26eeb399743b1432b09fe8905c7347229f222f2c450dd34", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:timex, ">= 3.0.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "1865d60a53039d66a3257bc7c0e75b62450e12a2102dcf50366c1e9ee4fb4363"},
"ash": {:hex, :ash, "1.38.0", "da043ce59c91872d83bbb53697139251abd75383818f7d2a26467dfd2689090c", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:timex, ">= 3.0.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "fe0bcab014299be369fc78b7f6cb690b284eeca5370fa3f514c7181e368dc810"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"},
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
@ -21,7 +21,7 @@
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"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.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"},
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
@ -36,8 +36,8 @@
"sobelow": {:hex, :sobelow, "0.10.2", "00e91208046d3b434f9f08779fe0ca7c6d6595b7fa33b289e792dffa6dde8081", [:mix], [], "hexpm", "e30fc994330cf6f485c1c4f2fb7c4b2d403557d0e101c6e5329fd17a58e55a7e"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"timex": {:hex, :timex, "3.6.3", "58ce6c9eda8ed47fc80c24dde09d481465838d3bcfc230949287fc1b0b0041c1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "6d69f4f95fcf5684102a9cb3cf92c5ba6545bd60ed8d8a6a93cd2a4a4fb0d9ec"},
"timex": {:hex, :timex, "3.7.3", "df8a2ea814749d700d6878ab9eacac9fdb498ecee2f507cb0002ec172bc24d0f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8691c1d86ca3a7bc14a156e2199dc8927be95d1a8f0e3b69e4bb2d6262c53ac6"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"},
"tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}

View file

@ -0,0 +1,4 @@
[
"uuid-ossp",
"pg_trgm"
]

View file

@ -0,0 +1,53 @@
{
"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": "integer"
}
],
"base_filter": null,
"has_create_action": true,
"hash": "C5E5F3D67674E95A1AE3F53029243C4A8B5D39F327B851F2F28F950BF232121B",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "comment_ratings"
}

View file

@ -0,0 +1,53 @@
{
"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": "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,
"has_create_action": true,
"hash": "8946E82C6BEEC8DD5645BB4592C8418F778411905A8654B888BF1917510F922A",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "comments"
}

View file

@ -0,0 +1,41 @@
{
"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": "name",
"primary_key?": false,
"references": null,
"type": "text"
}
],
"base_filter": null,
"has_create_action": true,
"hash": "B6460C9462B201C00284E48633BDB965A05C993F5F87A7171C3A4BC086189B6C",
"identities": [
{
"base_filter": null,
"keys": [
"name"
],
"name": "unique_by_name"
}
],
"multitenancy": {
"attribute": "id",
"global": true,
"strategy": "attribute"
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "multitenant_orgs"
}

View file

@ -0,0 +1,53 @@
{
"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": "integer"
}
],
"base_filter": null,
"has_create_action": true,
"hash": "D1937780750E122FC88936942D96EF9F298A3ED33607B4D9CB8B5F2F10BF7BAA",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "post_ratings"
}

View file

@ -0,0 +1,69 @@
{
"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": "public",
"primary_key?": false,
"references": null,
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "score",
"primary_key?": false,
"references": null,
"type": "integer"
},
{
"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": null,
"has_create_action": true,
"hash": "7C159A0E409D2645F19FB399E62073F345F2B83A11A2E22AE611323E76E7E006",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "posts"
}

View file

@ -0,0 +1,53 @@
{
"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": "name",
"primary_key?": false,
"references": null,
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "org_id",
"primary_key?": false,
"references": {
"destination_field": "id",
"multitenancy": {
"attribute": "id",
"global": true,
"strategy": "attribute"
},
"name": "multitenant_posts_org_id_fkey",
"on_delete": null,
"on_update": null,
"table": "multitenant_orgs"
},
"type": "uuid"
}
],
"base_filter": null,
"has_create_action": true,
"hash": "AABCDE07879AC4580DCA46668F3F83EB1ABD36B41121687B1032560B0382F5EB",
"identities": [],
"multitenancy": {
"attribute": null,
"global": false,
"strategy": "context"
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "multitenant_posts"
}

View file

@ -0,0 +1,21 @@
defmodule AshPostgres.TestRepo.Migrations.Install2Extensions do
@moduledoc """
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
execute("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
execute("CREATE EXTENSION IF NOT EXISTS \"pg_trgm\"")
end
def down do
# Uncomment this if you actually want to uninstall the extensions
# when this migration is rolled back.
execute("DROP EXTENSION IF EXISTS \"uuid-ossp\"")
execute("DROP EXTENSION IF EXISTS \"pg_trgm\"")
end
end

View file

@ -0,0 +1,88 @@
defmodule AshPostgres.TestRepo.Migrations.MigrateResources5 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 :id, :uuid, default: fragment("uuid_generate_v4()")
end
alter table(:post_ratings) do
modify :id, :uuid, default: fragment("uuid_generate_v4()")
end
alter table(:multitenant_orgs) do
modify :id, :uuid, default: fragment("uuid_generate_v4()")
end
alter table(:comments) do
modify :id, :uuid, default: fragment("uuid_generate_v4()")
end
alter table(:comment_ratings) do
modify :id, :uuid, default: fragment("uuid_generate_v4()")
modify :resource_id,
references(:comments, type: :uuid, column: :id, name: "comment_ratings_id_fkey")
end
alter table(:comments) do
modify :post_id,
references(:posts,
type: :uuid,
column: :id,
name: "special_name_fkey",
on_delete: :delete_all,
on_update: :update_all
)
end
alter table(:post_ratings) do
modify :resource_id,
references(:posts, type: :uuid, column: :id, name: "post_ratings_id_fkey")
end
end
def down do
alter table(:post_ratings) do
modify :resource_id,
references(:posts, type: :binary_id, column: :id, name: "post_ratings_resource_id")
end
alter table(:comments) do
modify :post_id, references(:posts, type: :binary_id, column: :id, name: "comments_post_id")
end
alter table(:comment_ratings) do
modify :resource_id,
references(:comments,
type: :binary_id,
column: :id,
name: "comment_ratings_resource_id"
)
modify :id, :binary_id, default: nil
end
alter table(:comments) do
modify :id, :binary_id, default: nil
end
alter table(:multitenant_orgs) do
modify :id, :binary_id, default: nil
end
alter table(:post_ratings) do
modify :id, :binary_id, default: nil
end
alter table(:posts) do
modify :id, :binary_id, default: nil
end
end
end

View file

@ -0,0 +1,37 @@
defmodule AshPostgres.TestRepo.TenantMigrations.MigrateResources3 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(:multitenant_posts, prefix: prefix()) do
modify :id, :uuid, default: fragment("uuid_generate_v4()")
modify :org_id,
references(:multitenant_orgs,
type: :uuid,
column: :id,
prefix: "public",
name: "multitenant_posts_org_id_fkey"
)
end
end
def down do
alter table(:multitenant_posts, prefix: prefix()) do
modify :org_id,
references(:multitenant_orgs,
type: :binary_id,
column: :id,
prefix: "public",
name: "multitenant_posts_org_id"
)
modify :id, :binary_id, default: nil
end
end
end

33
test/distinct_test.exs Normal file
View file

@ -0,0 +1,33 @@
defmodule AshPostgres.DistinctTest do
@moduledoc false
use AshPostgres.RepoCase, async: false
alias AshPostgres.Test.{Api, Post}
require Ash.Query
test "records returned are distinct on the provided field" do
Post
|> Ash.Changeset.new(%{title: "title"})
|> Api.create!()
Post
|> Ash.Changeset.new(%{title: "title"})
|> Api.create!()
Post
|> Ash.Changeset.new(%{title: "foo"})
|> Api.create!()
Post
|> Ash.Changeset.new(%{title: "foo"})
|> Api.create!()
results =
Post
|> Ash.Query.distinct(:title)
|> Ash.Query.sort(:title)
|> Api.read!()
assert [%{title: "foo"}, %{title: "title"}] = results
end
end

View file

@ -6,6 +6,10 @@ defmodule AshPostgres.Test.Comment do
postgres do
table "comments"
repo AshPostgres.TestRepo
references do
reference(:post, on_delete: :delete, on_update: :update, name: "special_name_fkey")
end
end
actions do