2020-06-14 19:04:18 +12:00
|
|
|
defmodule AshPostgres.DataLayer do
|
2020-10-29 15:26:45 +13:00
|
|
|
@manage_tenant %Ash.Dsl.Section{
|
|
|
|
name: :manage_tenant,
|
|
|
|
describe: """
|
|
|
|
Configuration for the behavior of a resource that manages a tenant
|
|
|
|
""",
|
2020-12-27 19:20:12 +13:00
|
|
|
examples: [
|
|
|
|
"""
|
|
|
|
manage_tenant do
|
|
|
|
template ["organization_", :id]
|
|
|
|
create? true
|
|
|
|
update? false
|
|
|
|
end
|
|
|
|
"""
|
|
|
|
],
|
2020-10-29 15:26:45 +13:00
|
|
|
schema: [
|
|
|
|
template: [
|
|
|
|
type: {:custom, __MODULE__, :tenant_template, []},
|
|
|
|
required: true,
|
|
|
|
doc: """
|
|
|
|
A template that will cause the resource to create/manage the specified schema.
|
|
|
|
|
|
|
|
Use this if you have a resource that, when created, it should create a new tenant
|
|
|
|
for you. For example, if you have a `customer` resource, and you want to create
|
|
|
|
a schema for each customer based on their id, e.g `customer_10` set this option
|
|
|
|
to `["customer_", :id]`. Then, when this is created, it will create a schema called
|
|
|
|
`["customer_", :id]`, and run your tenant migrations on it. Then, if you were to change
|
|
|
|
that customer's id to `20`, it would rename the schema to `customer_20`. Generally speaking
|
|
|
|
you should avoid changing the tenant id.
|
|
|
|
"""
|
|
|
|
],
|
|
|
|
create?: [
|
|
|
|
type: :boolean,
|
|
|
|
default: true,
|
|
|
|
doc: "Whether or not to automatically create a tenant when a record is created"
|
|
|
|
],
|
|
|
|
update?: [
|
|
|
|
type: :boolean,
|
|
|
|
default: true,
|
|
|
|
doc: "Whether or not to automatically update the tenant name if the record is udpated"
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
2020-10-29 16:53:28 +13:00
|
|
|
|
2021-04-01 19:19:30 +13:00
|
|
|
@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."
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-04-20 06:26:41 +12:00
|
|
|
@check_constraint %Ash.Dsl.Entity{
|
|
|
|
name: :check_constraint,
|
|
|
|
describe: """
|
|
|
|
Add a check constraint to be validated.
|
|
|
|
|
|
|
|
If a check constraint exists on the table but not in this section, and it produces an error, a runtime error will be raised.
|
|
|
|
|
|
|
|
Provide a list of attributes instead of a single attribute to add the message to multiple attributes.
|
|
|
|
|
|
|
|
By adding the `check` option, the migration generator will include it when generating migrations.
|
|
|
|
""",
|
|
|
|
examples: [
|
|
|
|
"""
|
|
|
|
check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive"
|
|
|
|
"""
|
|
|
|
],
|
|
|
|
args: [:attribute, :name],
|
|
|
|
target: AshPostgres.CheckConstraint,
|
|
|
|
schema: AshPostgres.CheckConstraint.schema()
|
|
|
|
}
|
|
|
|
|
|
|
|
@check_constraints %Ash.Dsl.Section{
|
|
|
|
name: :check_constraints,
|
|
|
|
describe: """
|
|
|
|
A section for configuring the check constraints for a given table.
|
|
|
|
|
|
|
|
This can be used to automatically create those check constraints, or just to provide message when they are raised
|
|
|
|
""",
|
|
|
|
examples: [
|
|
|
|
"""
|
|
|
|
check_constraints do
|
|
|
|
check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive"
|
|
|
|
end
|
|
|
|
"""
|
|
|
|
],
|
|
|
|
entities: [@check_constraint]
|
|
|
|
}
|
|
|
|
|
|
|
|
@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."
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2020-06-14 19:04:18 +12:00
|
|
|
@postgres %Ash.Dsl.Section{
|
|
|
|
name: :postgres,
|
|
|
|
describe: """
|
|
|
|
Postgres data layer configuration
|
|
|
|
""",
|
2020-10-29 15:26:45 +13:00
|
|
|
sections: [
|
2021-04-01 19:19:30 +13:00
|
|
|
@manage_tenant,
|
2021-04-20 06:26:41 +12:00
|
|
|
@references,
|
|
|
|
@check_constraints
|
2020-10-29 15:26:45 +13:00
|
|
|
],
|
2020-10-29 17:17:48 +13:00
|
|
|
modules: [
|
|
|
|
:repo
|
|
|
|
],
|
2020-12-27 19:20:12 +13:00
|
|
|
examples: [
|
|
|
|
"""
|
|
|
|
postgres do
|
|
|
|
repo MyApp.Repo
|
|
|
|
table "organizations"
|
|
|
|
end
|
|
|
|
"""
|
|
|
|
],
|
2020-06-14 19:04:18 +12:00
|
|
|
schema: [
|
|
|
|
repo: [
|
2020-10-29 16:53:28 +13:00
|
|
|
type: :atom,
|
2020-06-14 19:04:18 +12:00
|
|
|
required: true,
|
|
|
|
doc:
|
2020-09-03 20:18:11 +12:00
|
|
|
"The repo that will be used to fetch your data. See the `AshPostgres.Repo` documentation for more"
|
2020-06-14 19:04:18 +12:00
|
|
|
],
|
2020-09-11 12:26:47 +12:00
|
|
|
migrate?: [
|
|
|
|
type: :boolean,
|
|
|
|
default: true,
|
|
|
|
doc:
|
|
|
|
"Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations`"
|
|
|
|
],
|
2020-09-20 10:08:09 +12:00
|
|
|
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"
|
|
|
|
],
|
2021-01-22 09:32:26 +13:00
|
|
|
unique_index_names: [
|
|
|
|
type: :any,
|
|
|
|
default: [],
|
|
|
|
doc: """
|
|
|
|
A list of unique index names that could raise errors, or an mfa to a function that takes a changeset
|
2021-03-20 11:41:16 +13:00
|
|
|
and returns the list. Must be in the format `{[:affected, :keys], "name_of_constraint"}` or `{[:affected, :keys], "name_of_constraint", "custom error message"}`
|
2021-04-28 09:16:56 +12:00
|
|
|
|
|
|
|
Note that this is *not* used to rename the unique indexes created from `identities`.
|
|
|
|
Use `identity_index_names` for that. This is used to tell ash_postgres about unique indexes that
|
|
|
|
exist in the database that it didn't create.
|
|
|
|
"""
|
|
|
|
],
|
|
|
|
identity_index_names: [
|
|
|
|
type: :any,
|
|
|
|
default: [],
|
|
|
|
doc: """
|
|
|
|
A keyword list of identity names to the unique index name that they should use when being managed by the migration
|
|
|
|
generator.
|
2021-03-20 11:41:16 +13:00
|
|
|
"""
|
|
|
|
],
|
|
|
|
foreign_key_names: [
|
|
|
|
type: :any,
|
|
|
|
default: [],
|
|
|
|
doc: """
|
|
|
|
A list of foreign keys that could raise errors, or an mfa to a function that takes a changeset and returns the list.
|
|
|
|
Must be in the format `{:key, "name_of_constraint"}` or `{:key, "name_of_constraint", "custom error message"}`
|
2021-01-22 09:32:26 +13:00
|
|
|
"""
|
|
|
|
],
|
2020-06-14 19:04:18 +12:00
|
|
|
table: [
|
|
|
|
type: :string,
|
2021-01-29 13:42:55 +13:00
|
|
|
doc:
|
|
|
|
"The table to store and read the resource from. Required unless `polymorphic?` is true."
|
|
|
|
],
|
|
|
|
polymorphic?: [
|
|
|
|
type: :boolean,
|
|
|
|
default: false,
|
|
|
|
doc: """
|
|
|
|
Declares this resource as polymorphic.
|
|
|
|
|
|
|
|
Polymorphic resources cannot be read or updated unless the table is provided in the query/changeset context.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
PolymorphicResource
|
|
|
|
|> Ash.Query.set_context(%{data_layer: %{table: "table"}})
|
|
|
|
|> MyApi.read!()
|
|
|
|
|
|
|
|
When relating to polymorphic resources, you'll need to use the `context` option on relationships,
|
|
|
|
e.g
|
|
|
|
|
|
|
|
belongs_to :polymorphic_association, PolymorphicResource,
|
|
|
|
context: %{data_layer: %{table: "table"}}
|
|
|
|
"""
|
2020-06-14 19:04:18 +12:00
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2020-06-19 15:04:41 +12:00
|
|
|
alias Ash.Filter
|
2021-01-22 09:32:26 +13:00
|
|
|
alias Ash.Query.{BooleanExpression, Not, Ref}
|
2020-10-06 18:39:47 +13:00
|
|
|
|
2021-01-24 16:45:15 +13:00
|
|
|
alias Ash.Query.Function.{Ago, Contains}
|
2021-01-22 09:32:26 +13:00
|
|
|
alias Ash.Query.Operator.IsNil
|
|
|
|
|
|
|
|
alias AshPostgres.Functions.{Fragment, TrigramSimilarity, Type}
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2021-02-06 12:59:33 +13:00
|
|
|
import AshPostgres, only: [repo: 1]
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
@behaviour Ash.DataLayer
|
|
|
|
|
2020-12-27 19:20:12 +13:00
|
|
|
@sections [@postgres]
|
|
|
|
|
|
|
|
@moduledoc """
|
|
|
|
A postgres data layer that levereges Ecto's postgres capabilities.
|
|
|
|
|
|
|
|
# Table of Contents
|
|
|
|
#{Ash.Dsl.Extension.doc_index(@sections)}
|
|
|
|
|
|
|
|
#{Ash.Dsl.Extension.doc(@sections)}
|
|
|
|
"""
|
|
|
|
|
2020-10-29 16:53:28 +13:00
|
|
|
use Ash.Dsl.Extension,
|
2020-12-27 19:20:12 +13:00
|
|
|
sections: @sections,
|
2021-01-29 13:42:55 +13:00
|
|
|
transformers: [
|
|
|
|
AshPostgres.Transformers.VerifyRepo,
|
|
|
|
AshPostgres.Transformers.EnsureTableOrPolymorphic
|
|
|
|
]
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
@doc false
|
|
|
|
def tenant_template(value) do
|
|
|
|
value = List.wrap(value)
|
|
|
|
|
|
|
|
if Enum.all?(value, &(is_binary(&1) || is_atom(&1))) do
|
|
|
|
{:ok, value}
|
|
|
|
else
|
|
|
|
{:error, "Expected all values for `manages_tenant` to be strings or atoms"}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-09-20 10:08:09 +12:00
|
|
|
@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
|
|
|
|
|
2020-07-23 17:13:47 +12:00
|
|
|
import Ecto.Query, only: [from: 2, subquery: 1]
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
@impl true
|
2020-06-19 15:04:41 +12:00
|
|
|
def can?(_, :async_engine), do: true
|
|
|
|
def can?(_, :transact), do: true
|
|
|
|
def can?(_, :composite_primary_key), do: true
|
|
|
|
def can?(_, :upsert), do: true
|
2020-08-26 16:28:55 +12:00
|
|
|
|
|
|
|
def can?(resource, {:join, other_resource}) do
|
2021-02-23 17:53:18 +13:00
|
|
|
data_layer = Ash.DataLayer.data_layer(resource)
|
|
|
|
other_data_layer = Ash.DataLayer.data_layer(other_resource)
|
2020-08-26 16:28:55 +12:00
|
|
|
data_layer == other_data_layer and repo(data_layer) == repo(other_data_layer)
|
|
|
|
end
|
|
|
|
|
2021-04-30 09:31:19 +12:00
|
|
|
def can?(resource, {:lateral_join, resources}) do
|
|
|
|
repo = repo(resource)
|
2021-02-23 17:53:18 +13:00
|
|
|
data_layer = Ash.DataLayer.data_layer(resource)
|
2021-04-30 09:31:19 +12:00
|
|
|
|
|
|
|
data_layer == __MODULE__ &&
|
|
|
|
Enum.all?(resources, fn resource ->
|
|
|
|
Ash.DataLayer.data_layer(resource) == data_layer && repo(resource) == repo
|
|
|
|
end)
|
2020-08-26 16:28:55 +12:00
|
|
|
end
|
|
|
|
|
2020-06-29 14:29:38 +12:00
|
|
|
def can?(_, :boolean_filter), do: true
|
2020-07-25 11:27:34 +12:00
|
|
|
def can?(_, {:aggregate, :count}), do: true
|
2021-04-05 08:05:41 +12:00
|
|
|
def can?(_, {:aggregate, :sum}), do: true
|
2020-07-23 17:13:47 +12:00
|
|
|
def can?(_, :aggregate_filter), do: true
|
|
|
|
def can?(_, :aggregate_sort), do: true
|
2020-08-19 16:52:23 +12:00
|
|
|
def can?(_, :create), do: true
|
2021-04-09 16:53:50 +12:00
|
|
|
def can?(_, :select), do: true
|
2020-08-19 16:52:23 +12:00
|
|
|
def can?(_, :read), do: true
|
|
|
|
def can?(_, :update), do: true
|
|
|
|
def can?(_, :destroy), do: true
|
|
|
|
def can?(_, :filter), do: true
|
|
|
|
def can?(_, :limit), do: true
|
|
|
|
def can?(_, :offset), do: true
|
2020-10-29 15:26:45 +13:00
|
|
|
def can?(_, :multitenancy), do: true
|
2021-01-22 09:32:26 +13:00
|
|
|
def can?(_, {:filter_expr, _}), do: true
|
|
|
|
def can?(_, :nested_expressions), do: true
|
2020-10-18 12:13:51 +13:00
|
|
|
def can?(_, {:query_aggregate, :count}), do: true
|
2020-08-19 17:18:52 +12:00
|
|
|
def can?(_, :sort), do: true
|
2021-04-01 19:19:30 +13:00
|
|
|
def can?(_, :distinct), do: true
|
2020-07-23 17:13:47 +12:00
|
|
|
def can?(_, {:sort, _}), do: true
|
2020-08-17 18:46:59 +12:00
|
|
|
def can?(_, _), do: false
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-06-30 16:16:17 +12:00
|
|
|
@impl true
|
|
|
|
def in_transaction?(resource) do
|
|
|
|
repo(resource).in_transaction?()
|
|
|
|
end
|
|
|
|
|
2020-06-14 19:04:18 +12:00
|
|
|
@impl true
|
|
|
|
def limit(query, nil, _), do: {:ok, query}
|
|
|
|
|
|
|
|
def limit(query, limit, _resource) do
|
|
|
|
{:ok, from(row in query, limit: ^limit)}
|
|
|
|
end
|
|
|
|
|
2020-07-08 12:01:01 +12:00
|
|
|
@impl true
|
|
|
|
def source(resource) do
|
2021-02-06 12:59:33 +13:00
|
|
|
AshPostgres.table(resource) || ""
|
2021-01-29 13:42:55 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def set_context(resource, data_layer_query, context) do
|
2021-02-06 12:59:33 +13:00
|
|
|
if context[:data_layer][:table] do
|
2021-01-29 13:42:55 +13:00
|
|
|
{:ok,
|
|
|
|
%{
|
|
|
|
data_layer_query
|
|
|
|
| from: %{data_layer_query.from | source: {context[:data_layer][:table], resource}}
|
|
|
|
}}
|
|
|
|
else
|
|
|
|
{:ok, data_layer_query}
|
|
|
|
end
|
2020-07-08 12:01:01 +12:00
|
|
|
end
|
|
|
|
|
2020-06-14 19:04:18 +12:00
|
|
|
@impl true
|
|
|
|
def offset(query, nil, _), do: query
|
|
|
|
|
2020-09-20 10:08:09 +12:00
|
|
|
def offset(%{offset: old_offset} = query, 0, _resource) when old_offset in [0, nil] do
|
|
|
|
{:ok, query}
|
|
|
|
end
|
|
|
|
|
2020-06-14 19:04:18 +12:00
|
|
|
def offset(query, offset, _resource) do
|
|
|
|
{:ok, from(row in query, offset: ^offset)}
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def run_query(query, resource) do
|
2021-03-22 10:58:47 +13:00
|
|
|
if AshPostgres.polymorphic?(resource) && no_table?(query) do
|
|
|
|
raise_table_error!(resource, :read)
|
|
|
|
else
|
|
|
|
{:ok, repo(resource).all(query, repo_opts(query))}
|
|
|
|
end
|
2020-10-29 15:26:45 +13:00
|
|
|
end
|
|
|
|
|
2021-03-22 10:58:47 +13:00
|
|
|
defp no_table?(%{from: %{source: {"", _}}}), do: true
|
|
|
|
defp no_table?(_), do: false
|
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
defp repo_opts(%Ash.Changeset{tenant: tenant, resource: resource}) do
|
|
|
|
repo_opts(%{tenant: tenant, resource: resource})
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
defp repo_opts(%{tenant: tenant, resource: resource}) when not is_nil(tenant) do
|
2021-02-23 17:53:18 +13:00
|
|
|
if Ash.Resource.Info.multitenancy_strategy(resource) == :context do
|
2020-10-29 15:26:45 +13:00
|
|
|
[prefix: tenant]
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp repo_opts(_), do: []
|
|
|
|
|
2020-10-06 18:39:47 +13:00
|
|
|
@impl true
|
|
|
|
def functions(resource) do
|
|
|
|
config = repo(resource).config()
|
|
|
|
|
2021-01-22 09:32:26 +13:00
|
|
|
functions = [AshPostgres.Functions.Type, AshPostgres.Functions.Fragment]
|
|
|
|
|
2020-10-06 18:39:47 +13:00
|
|
|
if "pg_trgm" in (config[:installed_extensions] || []) do
|
2021-01-22 09:32:26 +13:00
|
|
|
functions ++
|
|
|
|
[
|
|
|
|
AshPostgres.Functions.TrigramSimilarity
|
|
|
|
]
|
2020-10-06 18:39:47 +13:00
|
|
|
else
|
2021-01-22 09:32:26 +13:00
|
|
|
functions
|
2020-10-06 18:39:47 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-10-18 12:13:51 +13:00
|
|
|
@impl true
|
|
|
|
def run_aggregate_query(query, aggregates, resource) do
|
|
|
|
subquery = from(row in subquery(query), select: %{})
|
|
|
|
|
|
|
|
query =
|
|
|
|
Enum.reduce(
|
|
|
|
aggregates,
|
|
|
|
subquery,
|
|
|
|
&add_subquery_aggregate_select(&2, &1, resource)
|
|
|
|
)
|
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
{:ok, repo(resource).one(query, repo_opts(query))}
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def set_tenant(_resource, query, tenant) do
|
|
|
|
{:ok, Ecto.Query.put_query_prefix(query, to_string(tenant))}
|
2020-10-18 12:13:51 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def run_aggregate_query_with_lateral_join(
|
|
|
|
query,
|
|
|
|
aggregates,
|
|
|
|
root_data,
|
|
|
|
destination_resource,
|
2021-04-30 09:31:19 +12:00
|
|
|
path
|
2020-10-18 12:13:51 +13:00
|
|
|
) do
|
|
|
|
lateral_join_query =
|
|
|
|
lateral_join_query(
|
|
|
|
query,
|
|
|
|
root_data,
|
2021-04-30 09:31:19 +12:00
|
|
|
path
|
2020-10-18 12:13:51 +13:00
|
|
|
)
|
|
|
|
|
2021-04-30 09:31:19 +12:00
|
|
|
source_resource =
|
|
|
|
path
|
|
|
|
|> Enum.at(0)
|
|
|
|
|> elem(0)
|
|
|
|
|
2020-10-18 12:13:51 +13:00
|
|
|
subquery = from(row in subquery(lateral_join_query), select: %{})
|
|
|
|
|
|
|
|
query =
|
|
|
|
Enum.reduce(
|
|
|
|
aggregates,
|
|
|
|
subquery,
|
|
|
|
&add_subquery_aggregate_select(&2, &1, destination_resource)
|
|
|
|
)
|
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
{:ok, repo(source_resource).one(query, repo_opts(:query))}
|
2020-10-18 12:13:51 +13:00
|
|
|
end
|
|
|
|
|
2020-08-26 16:28:55 +12:00
|
|
|
@impl true
|
|
|
|
def run_query_with_lateral_join(
|
|
|
|
query,
|
|
|
|
root_data,
|
|
|
|
_destination_resource,
|
2021-04-30 09:31:19 +12:00
|
|
|
path
|
2020-08-26 16:28:55 +12:00
|
|
|
) do
|
2020-10-18 12:13:51 +13:00
|
|
|
query =
|
|
|
|
lateral_join_query(
|
|
|
|
query,
|
|
|
|
root_data,
|
2021-04-30 09:31:19 +12:00
|
|
|
path
|
2020-10-18 12:13:51 +13:00
|
|
|
)
|
|
|
|
|
2021-04-30 09:31:19 +12:00
|
|
|
source_resource =
|
|
|
|
path
|
|
|
|
|> Enum.at(0)
|
|
|
|
|> elem(0)
|
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
{:ok, repo(source_resource).all(query, repo_opts(query))}
|
2020-10-18 12:13:51 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
defp lateral_join_query(
|
|
|
|
query,
|
|
|
|
root_data,
|
2021-04-30 09:31:19 +12:00
|
|
|
[{source_resource, source_field, destination_field, relationship}]
|
2020-10-18 12:13:51 +13:00
|
|
|
) do
|
2020-08-26 16:28:55 +12:00
|
|
|
source_values = Enum.map(root_data, &Map.get(&1, source_field))
|
|
|
|
|
|
|
|
subquery =
|
|
|
|
subquery(
|
|
|
|
from(destination in query,
|
|
|
|
where:
|
|
|
|
field(destination, ^destination_field) ==
|
|
|
|
field(parent_as(:source_record), ^source_field)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2020-11-03 16:59:51 +13:00
|
|
|
source_resource
|
|
|
|
|> Ash.Query.new()
|
2021-04-30 09:31:19 +12:00
|
|
|
|> Ash.Query.set_context(relationship.context)
|
2021-05-04 17:36:25 +12:00
|
|
|
|> set_lateral_join_prefix(query)
|
2021-04-30 09:31:19 +12:00
|
|
|
|> Ash.Query.do_filter(relationship.filter)
|
2020-11-03 16:59:51 +13:00
|
|
|
|> Ash.Query.data_layer_query()
|
|
|
|
|> case do
|
|
|
|
{:ok, data_layer_query} ->
|
|
|
|
from(source in data_layer_query,
|
|
|
|
as: :source_record,
|
|
|
|
where: field(source, ^source_field) in ^source_values,
|
|
|
|
inner_lateral_join: destination in ^subquery,
|
|
|
|
on: field(source, ^source_field) == field(destination, ^destination_field),
|
|
|
|
select: destination
|
|
|
|
)
|
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
2020-08-26 16:28:55 +12:00
|
|
|
end
|
|
|
|
|
2021-04-30 09:31:19 +12:00
|
|
|
defp lateral_join_query(
|
|
|
|
query,
|
|
|
|
root_data,
|
|
|
|
[
|
|
|
|
{source_resource, source_field, source_field_on_join_table, relationship},
|
|
|
|
{through_resource, destination_field_on_join_table, destination_field,
|
|
|
|
through_relationship}
|
|
|
|
]
|
|
|
|
) do
|
|
|
|
source_values = Enum.map(root_data, &Map.get(&1, source_field))
|
|
|
|
|
|
|
|
through_resource
|
|
|
|
|> Ash.Query.new()
|
|
|
|
|> Ash.Query.set_context(through_relationship.context)
|
2021-05-04 17:36:25 +12:00
|
|
|
|> set_lateral_join_prefix(query)
|
2021-04-30 09:31:19 +12:00
|
|
|
|> Ash.Query.do_filter(through_relationship.filter)
|
|
|
|
|> Ash.Query.data_layer_query()
|
|
|
|
|> case do
|
|
|
|
{:ok, through_query} ->
|
|
|
|
source_resource
|
|
|
|
|> Ash.Query.new()
|
|
|
|
|> Ash.Query.set_context(relationship.context)
|
2021-05-04 17:36:25 +12:00
|
|
|
|> set_lateral_join_prefix(query)
|
2021-04-30 09:31:19 +12:00
|
|
|
|> Ash.Query.do_filter(relationship.filter)
|
|
|
|
|> Ash.Query.data_layer_query()
|
|
|
|
|> case do
|
|
|
|
{:ok, data_layer_query} ->
|
2021-05-04 18:14:24 +12:00
|
|
|
subquery =
|
|
|
|
subquery(
|
|
|
|
from(destination in query,
|
|
|
|
join: through in ^through_query,
|
|
|
|
on:
|
|
|
|
field(through, ^destination_field_on_join_table) ==
|
|
|
|
field(destination, ^destination_field),
|
|
|
|
where:
|
|
|
|
field(through, ^source_field_on_join_table) ==
|
|
|
|
field(parent_as(:source_record), ^source_field)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2021-04-30 09:31:19 +12:00
|
|
|
from(source in data_layer_query,
|
|
|
|
as: :source_record,
|
2021-05-04 18:14:24 +12:00
|
|
|
where: field(source, ^source_field) in ^source_values,
|
|
|
|
inner_lateral_join: through in ^subquery,
|
|
|
|
select: through
|
2021-04-30 09:31:19 +12:00
|
|
|
)
|
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-05-04 17:36:25 +12:00
|
|
|
defp set_lateral_join_prefix(ash_query, query) do
|
|
|
|
if Ash.Resource.Info.multitenancy_strategy(ash_query.resource) == :context do
|
|
|
|
Ash.Query.set_tenant(ash_query, query.prefix)
|
|
|
|
else
|
|
|
|
ash_query
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-14 19:04:18 +12:00
|
|
|
@impl true
|
2020-12-24 08:46:49 +13:00
|
|
|
def resource_to_query(resource, _),
|
2021-02-06 12:59:33 +13:00
|
|
|
do: Ecto.Queryable.to_query({AshPostgres.table(resource) || "", resource})
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
@impl true
|
|
|
|
def create(resource, changeset) do
|
2020-07-13 16:41:38 +12:00
|
|
|
changeset.data
|
2021-02-06 12:59:33 +13:00
|
|
|
|> Map.update!(:__meta__, &Map.put(&1, :source, table(resource, changeset)))
|
2021-03-20 11:41:16 +13:00
|
|
|
|> ecto_changeset(changeset, :create)
|
2020-10-29 15:26:45 +13:00
|
|
|
|> repo(resource).insert(repo_opts(changeset))
|
2021-01-22 09:32:26 +13:00
|
|
|
|> handle_errors()
|
2020-10-29 15:26:45 +13:00
|
|
|
|> case do
|
|
|
|
{:ok, result} ->
|
2021-01-27 09:07:26 +13:00
|
|
|
maybe_create_tenant!(resource, result)
|
2020-10-29 15:26:45 +13:00
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
{:ok, result}
|
2020-10-29 15:26:45 +13:00
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp maybe_create_tenant!(resource, result) do
|
2020-10-29 15:26:45 +13:00
|
|
|
if AshPostgres.manage_tenant_create?(resource) do
|
|
|
|
tenant_name = tenant_name(resource, result)
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
AshPostgres.MultiTenancy.create_tenant!(tenant_name, repo(resource))
|
2020-10-29 15:26:45 +13:00
|
|
|
else
|
|
|
|
:ok
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp maybe_update_tenant(resource, changeset, result) do
|
|
|
|
if AshPostgres.manage_tenant_update?(resource) do
|
|
|
|
changing_tenant_name? =
|
|
|
|
resource
|
|
|
|
|> AshPostgres.manage_tenant_template()
|
|
|
|
|> Enum.filter(&is_atom/1)
|
|
|
|
|> Enum.any?(&Ash.Changeset.changing_attribute?(changeset, &1))
|
|
|
|
|
|
|
|
if changing_tenant_name? do
|
|
|
|
old_tenant_name = tenant_name(resource, changeset.data)
|
|
|
|
|
|
|
|
new_tenant_name = tenant_name(resource, result)
|
|
|
|
AshPostgres.MultiTenancy.rename_tenant(repo(resource), old_tenant_name, new_tenant_name)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
:ok
|
|
|
|
end
|
|
|
|
|
|
|
|
defp tenant_name(resource, result) do
|
|
|
|
resource
|
|
|
|
|> AshPostgres.manage_tenant_template()
|
|
|
|
|> Enum.map_join(fn item ->
|
|
|
|
if is_binary(item) do
|
|
|
|
item
|
|
|
|
else
|
|
|
|
result
|
|
|
|
|> Map.get(item)
|
|
|
|
|> to_string()
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2021-01-22 09:32:26 +13:00
|
|
|
defp handle_errors({:error, %Ecto.Changeset{errors: errors}}) do
|
|
|
|
{:error, Enum.map(errors, &to_ash_error/1)}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp handle_errors({:ok, val}), do: {:ok, val}
|
|
|
|
|
|
|
|
defp to_ash_error({field, {message, vars}}) do
|
|
|
|
Ash.Error.Changes.InvalidAttribute.exception(field: field, message: message, vars: vars)
|
|
|
|
end
|
|
|
|
|
2021-03-20 11:41:16 +13:00
|
|
|
defp ecto_changeset(record, changeset, type) do
|
|
|
|
ecto_changeset =
|
|
|
|
record
|
2021-03-22 10:58:47 +13:00
|
|
|
|> set_table(changeset, type)
|
2021-03-20 11:41:16 +13:00
|
|
|
|> Ecto.Changeset.change(changeset.attributes)
|
2021-04-20 06:26:41 +12:00
|
|
|
|> add_configured_foreign_key_constraints(record.__struct__)
|
2021-04-28 09:16:56 +12:00
|
|
|
|> add_unique_indexes(record.__struct__, changeset)
|
2021-04-20 06:26:41 +12:00
|
|
|
|> add_check_constraints(record.__struct__)
|
2021-03-20 11:41:16 +13:00
|
|
|
|
|
|
|
case type do
|
|
|
|
:create ->
|
|
|
|
ecto_changeset
|
|
|
|
|> add_my_foreign_key_constraints(record.__struct__)
|
|
|
|
|
|
|
|
type when type in [:upsert, :update] ->
|
|
|
|
ecto_changeset
|
|
|
|
|> add_my_foreign_key_constraints(record.__struct__)
|
|
|
|
|> add_related_foreign_key_constraints(record.__struct__)
|
|
|
|
|
|
|
|
:delete ->
|
|
|
|
ecto_changeset
|
|
|
|
|> add_related_foreign_key_constraints(record.__struct__)
|
|
|
|
end
|
2021-01-22 09:32:26 +13:00
|
|
|
end
|
|
|
|
|
2021-03-22 10:58:47 +13:00
|
|
|
defp set_table(record, changeset, operation) do
|
2021-01-29 13:42:55 +13:00
|
|
|
if AshPostgres.polymorphic?(record.__struct__) do
|
|
|
|
table = changeset.context[:data_layer][:table] || AshPostgres.table(record.__struct)
|
|
|
|
|
|
|
|
if table do
|
|
|
|
Ecto.put_meta(record, source: table)
|
|
|
|
else
|
2021-03-22 10:58:47 +13:00
|
|
|
raise_table_error!(changeset.resource, operation)
|
2021-01-29 13:42:55 +13:00
|
|
|
end
|
|
|
|
else
|
|
|
|
record
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-20 06:26:41 +12:00
|
|
|
defp add_check_constraints(changeset, resource) do
|
|
|
|
resource
|
|
|
|
|> AshPostgres.check_constraints()
|
|
|
|
|> Enum.reduce(changeset, fn constraint, changeset ->
|
|
|
|
constraint.attribute
|
|
|
|
|> List.wrap()
|
|
|
|
|> Enum.reduce(changeset, fn attribute, changeset ->
|
|
|
|
Ecto.Changeset.check_constraint(changeset, attribute,
|
|
|
|
name: constraint.name,
|
|
|
|
message: constraint.message || "is invalid"
|
|
|
|
)
|
|
|
|
end)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2021-03-20 11:41:16 +13:00
|
|
|
defp add_related_foreign_key_constraints(changeset, resource) do
|
|
|
|
# TODO: this doesn't guarantee us to get all of them, because if something is related to this
|
|
|
|
# schema and there is no back-relation, then this won't catch it's foreign key constraints
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.Info.relationships()
|
|
|
|
|> Enum.map(& &1.destination)
|
|
|
|
|> Enum.uniq()
|
|
|
|
|> Enum.flat_map(fn related ->
|
|
|
|
related
|
|
|
|
|> Ash.Resource.Info.relationships()
|
|
|
|
|> Enum.filter(&(&1.destination == resource))
|
|
|
|
|> Enum.map(&Map.take(&1, [:source, :source_field, :destination_field]))
|
|
|
|
end)
|
|
|
|
|> Enum.uniq()
|
|
|
|
|> Enum.reduce(changeset, fn %{
|
|
|
|
source: source,
|
|
|
|
source_field: source_field,
|
|
|
|
destination_field: destination_field
|
|
|
|
},
|
|
|
|
changeset ->
|
|
|
|
Ecto.Changeset.foreign_key_constraint(changeset, destination_field,
|
|
|
|
name: "#{AshPostgres.table(source)}_#{source_field}_fkey",
|
|
|
|
message: "would leave records behind"
|
|
|
|
)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp add_my_foreign_key_constraints(changeset, resource) do
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.Info.relationships()
|
|
|
|
|> Enum.reduce(changeset, &Ecto.Changeset.foreign_key_constraint(&2, &1.source_field))
|
|
|
|
end
|
|
|
|
|
|
|
|
defp add_configured_foreign_key_constraints(changeset, resource) do
|
|
|
|
resource
|
|
|
|
|> AshPostgres.foreign_key_names()
|
|
|
|
|> case do
|
|
|
|
{m, f, a} -> List.wrap(apply(m, f, [changeset | a]))
|
|
|
|
value -> List.wrap(value)
|
|
|
|
end
|
|
|
|
|> Enum.reduce(changeset, fn
|
|
|
|
{key, name}, changeset ->
|
|
|
|
Ecto.Changeset.foreign_key_constraint(changeset, key, name: name)
|
|
|
|
|
|
|
|
{key, name, message}, changeset ->
|
|
|
|
Ecto.Changeset.foreign_key_constraint(changeset, key, name: name, message: message)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2021-04-28 09:16:56 +12:00
|
|
|
defp add_unique_indexes(changeset, resource, ash_changeset) do
|
2021-01-22 09:32:26 +13:00
|
|
|
changeset =
|
|
|
|
resource
|
2021-02-23 17:53:18 +13:00
|
|
|
|> Ash.Resource.Info.identities()
|
2021-01-22 09:32:26 +13:00
|
|
|
|> Enum.reduce(changeset, fn identity, changeset ->
|
2021-01-27 09:07:26 +13:00
|
|
|
name =
|
2021-04-28 09:16:56 +12:00
|
|
|
AshPostgres.identity_index_names(resource)[identity.name] ||
|
|
|
|
"#{table(resource, ash_changeset)}_#{identity.name}_index"
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
opts =
|
|
|
|
if Map.get(identity, :message) do
|
|
|
|
[name: name, message: identity.message]
|
|
|
|
else
|
|
|
|
[name: name]
|
|
|
|
end
|
|
|
|
|
|
|
|
Ecto.Changeset.unique_constraint(changeset, identity.keys, opts)
|
2021-01-22 09:32:26 +13:00
|
|
|
end)
|
|
|
|
|
|
|
|
names =
|
|
|
|
resource
|
|
|
|
|> AshPostgres.unique_index_names()
|
|
|
|
|> case do
|
2021-01-27 09:07:26 +13:00
|
|
|
{m, f, a} -> List.wrap(apply(m, f, [changeset | a]))
|
2021-01-22 09:32:26 +13:00
|
|
|
value -> List.wrap(value)
|
|
|
|
end
|
|
|
|
|
2021-02-06 12:59:33 +13:00
|
|
|
names = [
|
2021-02-23 17:53:18 +13:00
|
|
|
{Ash.Resource.Info.primary_key(resource), table(resource, ash_changeset) <> "_pkey"} | names
|
2021-02-06 12:59:33 +13:00
|
|
|
]
|
2021-01-27 09:07:26 +13:00
|
|
|
|
2021-03-20 11:41:16 +13:00
|
|
|
Enum.reduce(names, changeset, fn
|
|
|
|
{keys, name}, changeset ->
|
|
|
|
Ecto.Changeset.unique_constraint(changeset, List.wrap(keys), name: name)
|
|
|
|
|
|
|
|
{keys, name, message}, changeset ->
|
|
|
|
Ecto.Changeset.unique_constraint(changeset, List.wrap(keys), name: name, message: message)
|
2021-01-22 09:32:26 +13:00
|
|
|
end)
|
2020-07-13 16:41:38 +12:00
|
|
|
end
|
|
|
|
|
2020-06-14 19:04:18 +12:00
|
|
|
@impl true
|
|
|
|
def upsert(resource, changeset) do
|
2020-10-29 15:26:45 +13:00
|
|
|
repo_opts =
|
|
|
|
changeset
|
|
|
|
|> repo_opts()
|
|
|
|
|> Keyword.put(:on_conflict, {:replace, Map.keys(changeset.attributes)})
|
2021-02-23 17:53:18 +13:00
|
|
|
|> Keyword.put(:conflict_target, Ash.Resource.Info.primary_key(resource))
|
2020-10-29 15:26:45 +13:00
|
|
|
|
|
|
|
if AshPostgres.manage_tenant_update?(resource) do
|
|
|
|
{:error, "Cannot currently upsert a resource that owns a tenant"}
|
|
|
|
else
|
|
|
|
changeset.data
|
2021-02-06 12:59:33 +13:00
|
|
|
|> Map.update!(:__meta__, &Map.put(&1, :source, table(resource, changeset)))
|
2021-03-20 11:41:16 +13:00
|
|
|
|> ecto_changeset(changeset, :upsert)
|
2020-10-29 15:26:45 +13:00
|
|
|
|> repo(resource).insert(repo_opts)
|
2021-01-22 09:32:26 +13:00
|
|
|
|> handle_errors()
|
2020-10-29 15:26:45 +13:00
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def update(resource, changeset) do
|
2020-07-13 16:41:38 +12:00
|
|
|
changeset.data
|
2021-02-06 12:59:33 +13:00
|
|
|
|> Map.update!(:__meta__, &Map.put(&1, :source, table(resource, changeset)))
|
2021-03-20 11:41:16 +13:00
|
|
|
|> ecto_changeset(changeset, :update)
|
2020-10-29 15:26:45 +13:00
|
|
|
|> repo(resource).update(repo_opts(changeset))
|
2021-01-22 09:32:26 +13:00
|
|
|
|> handle_errors()
|
2020-10-29 15:26:45 +13:00
|
|
|
|> case do
|
|
|
|
{:ok, result} ->
|
|
|
|
maybe_update_tenant(resource, changeset, result)
|
|
|
|
|
|
|
|
{:ok, result}
|
|
|
|
|
|
|
|
{:error, error} ->
|
|
|
|
{:error, error}
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
2020-10-29 15:26:45 +13:00
|
|
|
def destroy(resource, %{data: record} = changeset) do
|
2021-03-20 11:41:16 +13:00
|
|
|
record
|
|
|
|
|> ecto_changeset(changeset, :delete)
|
|
|
|
|> repo(resource).delete(repo_opts(changeset))
|
|
|
|
|> case do
|
2020-10-29 15:26:45 +13:00
|
|
|
{:ok, _record} ->
|
|
|
|
:ok
|
|
|
|
|
|
|
|
{:error, error} ->
|
2021-01-22 09:32:26 +13:00
|
|
|
handle_errors({:error, error})
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
2020-09-02 16:01:34 +12:00
|
|
|
def sort(query, sort, resource) do
|
|
|
|
query = default_bindings(query, resource)
|
2020-07-23 17:13:47 +12:00
|
|
|
|
2020-08-26 16:28:55 +12:00
|
|
|
sort
|
|
|
|
|> sanitize_sort()
|
2020-10-18 12:13:51 +13:00
|
|
|
|> Enum.reduce({:ok, query}, fn {order, sort}, {:ok, query} ->
|
2020-08-26 16:28:55 +12:00
|
|
|
binding =
|
|
|
|
case Map.fetch(query.__ash_bindings__.aggregates, sort) do
|
|
|
|
{:ok, binding} ->
|
|
|
|
binding
|
|
|
|
|
|
|
|
:error ->
|
|
|
|
0
|
|
|
|
end
|
|
|
|
|
2020-10-18 12:13:51 +13:00
|
|
|
new_query =
|
|
|
|
Map.update!(query, :order_bys, fn order_bys ->
|
|
|
|
order_bys = order_bys || []
|
|
|
|
|
|
|
|
sort_expr = %Ecto.Query.QueryExpr{
|
|
|
|
expr: [
|
|
|
|
{order, {{:., [], [{:&, [], [binding]}, sort]}, [], []}}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
order_bys ++ [sort_expr]
|
|
|
|
end)
|
|
|
|
|
|
|
|
{:ok, new_query}
|
2020-08-26 16:28:55 +12:00
|
|
|
end)
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2021-04-09 16:53:50 +12:00
|
|
|
@impl true
|
|
|
|
def select(query, select, resource) do
|
|
|
|
query = default_bindings(query, resource)
|
|
|
|
|
|
|
|
{:ok,
|
|
|
|
from(row in query,
|
|
|
|
select: struct(row, ^select)
|
|
|
|
)}
|
|
|
|
end
|
|
|
|
|
2021-04-01 19:19:30 +13:00
|
|
|
@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
|
|
|
|
|
2020-06-14 19:04:18 +12:00
|
|
|
defp sanitize_sort(sort) do
|
|
|
|
sort
|
|
|
|
|> List.wrap()
|
|
|
|
|> Enum.map(fn
|
2020-12-29 13:26:04 +13:00
|
|
|
{sort, :asc_nils_last} -> {:asc_nulls_last, sort}
|
|
|
|
{sort, :asc_nils_first} -> {:asc_nulls_first, sort}
|
|
|
|
{sort, :desc_nils_last} -> {:desc_nulls_last, sort}
|
|
|
|
{sort, :desc_nils_first} -> {:desc_nulls_first, sort}
|
2020-06-14 19:04:18 +12:00
|
|
|
{sort, order} -> {order, sort}
|
|
|
|
sort -> sort
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
2020-06-19 15:04:41 +12:00
|
|
|
def filter(query, %{expression: false}, _resource) do
|
|
|
|
impossible_query = from(row in query, where: false)
|
|
|
|
{:ok, Map.put(impossible_query, :__impossible__, true)}
|
|
|
|
end
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
def filter(query, filter, _resource) do
|
2020-06-19 15:04:41 +12:00
|
|
|
relationship_paths =
|
|
|
|
filter
|
|
|
|
|> Filter.relationship_paths()
|
|
|
|
|> Enum.map(fn path ->
|
2020-09-02 16:01:34 +12:00
|
|
|
if can_inner_join?(path, filter) do
|
|
|
|
{:inner, relationship_path_to_relationships(filter.resource, path)}
|
|
|
|
else
|
|
|
|
{:left, relationship_path_to_relationships(filter.resource, path)}
|
|
|
|
end
|
2020-06-19 15:04:41 +12:00
|
|
|
end)
|
|
|
|
|
2020-06-14 19:04:18 +12:00
|
|
|
new_query =
|
|
|
|
query
|
2020-09-02 16:01:34 +12:00
|
|
|
|> join_all_relationships(relationship_paths)
|
2020-06-14 19:04:18 +12:00
|
|
|
|> add_filter_expression(filter)
|
|
|
|
|
2020-06-19 15:04:41 +12:00
|
|
|
{:ok, new_query}
|
|
|
|
end
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
defp default_bindings(query, resource) do
|
2020-07-23 17:13:47 +12:00
|
|
|
Map.put_new(query, :__ash_bindings__, %{
|
|
|
|
current: Enum.count(query.joins) + 1,
|
|
|
|
aggregates: %{},
|
2020-09-02 16:01:34 +12:00
|
|
|
bindings: %{0 => %{path: [], type: :root, source: resource}}
|
2020-07-23 17:13:47 +12:00
|
|
|
})
|
|
|
|
end
|
|
|
|
|
2021-03-05 16:50:12 +13:00
|
|
|
@known_inner_join_operators [
|
|
|
|
Eq,
|
|
|
|
GreaterThan,
|
|
|
|
GreaterThanOrEqual,
|
|
|
|
In,
|
|
|
|
LessThanOrEqual,
|
|
|
|
LessThan,
|
|
|
|
NotEq
|
|
|
|
]
|
|
|
|
|> Enum.map(&Module.concat(Ash.Query.Operator, &1))
|
|
|
|
|
|
|
|
@known_inner_join_functions [
|
|
|
|
Ago,
|
2021-03-05 16:54:40 +13:00
|
|
|
Contains
|
2021-03-05 16:50:12 +13:00
|
|
|
]
|
|
|
|
|> Enum.map(&Module.concat(Ash.Query.Function, &1))
|
|
|
|
|
|
|
|
@known_inner_join_predicates @known_inner_join_functions ++ @known_inner_join_operators
|
|
|
|
|
2021-02-25 07:59:49 +13:00
|
|
|
# For consistency's sake, this logic was removed.
|
|
|
|
# We can revisit it sometime though.
|
2021-03-05 16:50:12 +13:00
|
|
|
defp can_inner_join?(path, expr, seen_an_or? \\ false)
|
|
|
|
|
|
|
|
defp can_inner_join?(path, %{expression: expr}, seen_an_or?),
|
|
|
|
do: can_inner_join?(path, expr, seen_an_or?)
|
|
|
|
|
|
|
|
defp can_inner_join?(_path, expr, _seen_an_or?) when expr in [nil, true, false], do: true
|
|
|
|
|
|
|
|
defp can_inner_join?(path, %BooleanExpression{op: :and, left: left, right: right}, seen_an_or?) do
|
|
|
|
can_inner_join?(path, left, seen_an_or?) || can_inner_join?(path, right, seen_an_or?)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp can_inner_join?(path, %BooleanExpression{op: :or, left: left, right: right}, _) do
|
|
|
|
can_inner_join?(path, left, true) && can_inner_join?(path, right, true)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp can_inner_join?(
|
2021-03-05 16:54:40 +13:00
|
|
|
_,
|
|
|
|
%Not{},
|
|
|
|
_
|
2021-03-05 16:50:12 +13:00
|
|
|
) do
|
2021-03-05 16:54:40 +13:00
|
|
|
false
|
2021-03-05 16:50:12 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
defp can_inner_join?(
|
|
|
|
search_path,
|
|
|
|
%struct{__operator__?: true, left: %Ref{relationship_path: relationship_path}},
|
|
|
|
seen_an_or?
|
|
|
|
)
|
|
|
|
when search_path == relationship_path and struct in @known_inner_join_predicates do
|
|
|
|
not seen_an_or?
|
|
|
|
end
|
|
|
|
|
|
|
|
defp can_inner_join?(
|
|
|
|
search_path,
|
|
|
|
%struct{__operator__?: true, right: %Ref{relationship_path: relationship_path}},
|
|
|
|
seen_an_or?
|
|
|
|
)
|
|
|
|
when search_path == relationship_path and struct in @known_inner_join_predicates do
|
|
|
|
not seen_an_or?
|
|
|
|
end
|
|
|
|
|
|
|
|
defp can_inner_join?(
|
|
|
|
search_path,
|
|
|
|
%struct{__function__?: true, arguments: arguments},
|
|
|
|
seen_an_or?
|
|
|
|
)
|
|
|
|
when struct in @known_inner_join_predicates do
|
|
|
|
if Enum.any?(arguments, &match?(%Ref{relationship_path: ^search_path}, &1)) do
|
|
|
|
not seen_an_or?
|
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp can_inner_join?(_, _, _), do: false
|
2020-09-02 16:01:34 +12:00
|
|
|
|
2020-07-23 17:13:47 +12:00
|
|
|
@impl true
|
2020-09-02 16:01:34 +12:00
|
|
|
def add_aggregate(query, aggregate, _resource) do
|
|
|
|
resource = aggregate.resource
|
|
|
|
query = default_bindings(query, resource)
|
2020-07-23 17:13:47 +12:00
|
|
|
|
|
|
|
{query, binding} =
|
|
|
|
case get_binding(resource, aggregate.relationship_path, query, :aggregate) do
|
|
|
|
nil ->
|
2021-02-23 17:53:18 +13:00
|
|
|
relationship = Ash.Resource.Info.relationship(resource, aggregate.relationship_path)
|
2020-07-23 17:13:47 +12:00
|
|
|
subquery = aggregate_subquery(relationship, aggregate)
|
|
|
|
|
|
|
|
new_query =
|
2020-09-02 16:01:34 +12:00
|
|
|
join_all_relationships(
|
2020-07-23 17:13:47 +12:00
|
|
|
query,
|
2020-09-02 16:01:34 +12:00
|
|
|
[
|
|
|
|
{{:aggregate, aggregate.name, subquery},
|
|
|
|
relationship_path_to_relationships(resource, aggregate.relationship_path)}
|
|
|
|
]
|
2020-07-23 17:13:47 +12:00
|
|
|
)
|
|
|
|
|
|
|
|
{new_query, get_binding(resource, aggregate.relationship_path, new_query, :aggregate)}
|
|
|
|
|
|
|
|
binding ->
|
|
|
|
{query, binding}
|
|
|
|
end
|
|
|
|
|
|
|
|
query_with_aggregate_binding =
|
|
|
|
put_in(
|
|
|
|
query.__ash_bindings__.aggregates,
|
|
|
|
Map.put(query.__ash_bindings__.aggregates, aggregate.name, binding)
|
|
|
|
)
|
|
|
|
|
|
|
|
new_query =
|
|
|
|
query_with_aggregate_binding
|
|
|
|
|> add_aggregate_to_subquery(resource, aggregate, binding)
|
|
|
|
|> select_aggregate(resource, aggregate)
|
|
|
|
|
|
|
|
{:ok, new_query}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp select_aggregate(query, resource, aggregate) do
|
|
|
|
binding = get_binding(resource, aggregate.relationship_path, query, :aggregate)
|
|
|
|
|
|
|
|
query =
|
|
|
|
if query.select do
|
|
|
|
query
|
|
|
|
else
|
|
|
|
from(row in query,
|
|
|
|
select: row,
|
|
|
|
select_merge: %{aggregates: %{}}
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
%{query | select: add_to_select(query.select, binding, aggregate)}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp add_to_select(
|
|
|
|
%{expr: {:merge, _, [first, {:%{}, _, [{:aggregates, {:%{}, [], fields}}]}]}} = select,
|
|
|
|
binding,
|
2020-08-09 08:19:18 +12:00
|
|
|
%{load: nil} = aggregate
|
2020-07-23 17:13:47 +12:00
|
|
|
) do
|
2021-04-27 08:45:47 +12:00
|
|
|
accessed = {{:., [], [{:&, [], [binding]}, aggregate.name]}, [], []}
|
2020-12-29 13:26:04 +13:00
|
|
|
|
2020-07-23 17:13:47 +12:00
|
|
|
field =
|
|
|
|
{:type, [],
|
|
|
|
[
|
2020-12-29 13:26:04 +13:00
|
|
|
accessed,
|
2020-07-23 17:13:47 +12:00
|
|
|
Ash.Type.ecto_type(aggregate.type)
|
|
|
|
]}
|
|
|
|
|
|
|
|
field_with_default =
|
2020-12-29 13:26:04 +13:00
|
|
|
if is_nil(aggregate.default_value) do
|
|
|
|
field
|
|
|
|
else
|
2020-07-23 17:13:47 +12:00
|
|
|
{:coalesce, [],
|
|
|
|
[
|
|
|
|
field,
|
2021-04-27 08:45:47 +12:00
|
|
|
{:type, [],
|
|
|
|
[
|
|
|
|
aggregate.default_value,
|
|
|
|
Ash.Type.ecto_type(aggregate.type)
|
|
|
|
]}
|
2020-07-23 17:13:47 +12:00
|
|
|
]}
|
|
|
|
end
|
|
|
|
|
|
|
|
new_fields = [
|
|
|
|
{aggregate.name, field_with_default}
|
|
|
|
| fields
|
|
|
|
]
|
|
|
|
|
|
|
|
%{select | expr: {:merge, [], [first, {:%{}, [], [{:aggregates, {:%{}, [], new_fields}}]}]}}
|
|
|
|
end
|
|
|
|
|
2020-08-09 08:19:18 +12:00
|
|
|
defp add_to_select(
|
|
|
|
%{expr: expr} = select,
|
|
|
|
binding,
|
|
|
|
%{load: load_as} = aggregate
|
|
|
|
) do
|
2021-04-27 08:45:47 +12:00
|
|
|
accessed = {{:., [], [{:&, [], [binding]}, aggregate.name]}, [], []}
|
2020-12-29 13:26:04 +13:00
|
|
|
|
2020-08-09 08:19:18 +12:00
|
|
|
field =
|
|
|
|
{:type, [],
|
|
|
|
[
|
2020-12-29 13:26:04 +13:00
|
|
|
accessed,
|
2020-08-09 08:19:18 +12:00
|
|
|
Ash.Type.ecto_type(aggregate.type)
|
|
|
|
]}
|
|
|
|
|
|
|
|
field_with_default =
|
2020-12-29 13:26:04 +13:00
|
|
|
if is_nil(aggregate.default_value) do
|
|
|
|
field
|
|
|
|
else
|
2020-08-09 08:19:18 +12:00
|
|
|
{:coalesce, [],
|
|
|
|
[
|
|
|
|
field,
|
2021-04-27 08:45:47 +12:00
|
|
|
{:type, [],
|
|
|
|
[
|
|
|
|
aggregate.default_value,
|
|
|
|
Ash.Type.ecto_type(aggregate.type)
|
|
|
|
]}
|
2020-08-09 08:19:18 +12:00
|
|
|
]}
|
|
|
|
end
|
|
|
|
|
|
|
|
%{select | expr: {:merge, [], [expr, {:%{}, [], [{load_as, field_with_default}]}]}}
|
|
|
|
end
|
|
|
|
|
2020-07-23 17:13:47 +12:00
|
|
|
defp add_aggregate_to_subquery(query, resource, aggregate, binding) do
|
|
|
|
new_joins =
|
|
|
|
List.update_at(query.joins, binding - 1, fn join ->
|
|
|
|
aggregate_query =
|
|
|
|
if aggregate.authorization_filter do
|
|
|
|
{:ok, filter} =
|
|
|
|
filter(
|
|
|
|
join.source.from.source.query,
|
|
|
|
aggregate.authorization_filter,
|
2021-02-23 17:53:18 +13:00
|
|
|
Ash.Resource.Info.related(resource, aggregate.relationship_path)
|
2020-07-23 17:13:47 +12:00
|
|
|
)
|
|
|
|
|
|
|
|
filter
|
|
|
|
else
|
|
|
|
join.source.from.source.query
|
|
|
|
end
|
|
|
|
|
|
|
|
new_aggregate_query = add_subquery_aggregate_select(aggregate_query, aggregate, resource)
|
|
|
|
|
|
|
|
put_in(join.source.from.source.query, new_aggregate_query)
|
|
|
|
end)
|
|
|
|
|
|
|
|
%{
|
|
|
|
query
|
|
|
|
| joins: new_joins
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
defp aggregate_subquery(relationship, aggregate) do
|
|
|
|
query =
|
|
|
|
from(row in relationship.destination,
|
|
|
|
group_by: ^relationship.destination_field,
|
|
|
|
select: field(row, ^relationship.destination_field)
|
|
|
|
)
|
|
|
|
|
|
|
|
if aggregate.query && aggregate.query.tenant do
|
|
|
|
Ecto.Query.put_query_prefix(query, aggregate.query.tenant)
|
|
|
|
else
|
|
|
|
query
|
|
|
|
end
|
2020-07-23 17:13:47 +12:00
|
|
|
end
|
|
|
|
|
2020-12-29 13:26:04 +13:00
|
|
|
defp order_to_postgres_order(dir) do
|
|
|
|
case dir do
|
|
|
|
:asc -> nil
|
|
|
|
:asc_nils_last -> " ASC NULLS LAST"
|
|
|
|
:asc_nils_first -> " ASC NULLS FIRST"
|
|
|
|
:desc -> " DESC"
|
|
|
|
:desc_nils_last -> " DESC NULLS LAST"
|
|
|
|
:desc_nils_first -> " DESC NULLS FIRST"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-27 08:45:47 +12:00
|
|
|
defp add_subquery_aggregate_select(query, %{kind: kind} = aggregate, _resource)
|
|
|
|
when kind in [:first, :list] do
|
|
|
|
query = default_bindings(query, aggregate.resource)
|
|
|
|
key = aggregate.field
|
|
|
|
type = Ash.Type.ecto_type(aggregate.type)
|
|
|
|
|
|
|
|
field =
|
|
|
|
if aggregate.query && aggregate.query.sort && aggregate.query.sort != [] do
|
|
|
|
sort_expr =
|
|
|
|
aggregate.query.sort
|
|
|
|
|> Enum.map(fn {sort, order} ->
|
|
|
|
case order_to_postgres_order(order) do
|
|
|
|
nil ->
|
|
|
|
[expr: {{:., [], [{:&, [], [0]}, sort]}, [], []}]
|
|
|
|
|
|
|
|
order ->
|
|
|
|
[expr: {{:., [], [{:&, [], [0]}, sort]}, [], []}, raw: order]
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|> Enum.intersperse(raw: ", ")
|
|
|
|
|> List.flatten()
|
|
|
|
|
|
|
|
{:fragment, [],
|
|
|
|
[
|
|
|
|
raw: "array_agg(",
|
|
|
|
expr: {{:., [], [{:&, [], [0]}, key]}, [], []},
|
|
|
|
raw: " ORDER BY "
|
|
|
|
] ++
|
|
|
|
sort_expr ++ [raw: ")"]}
|
|
|
|
else
|
|
|
|
{:fragment, [],
|
|
|
|
[
|
|
|
|
raw: "array_agg(",
|
|
|
|
expr: {{:., [], [{:&, [], [0]}, key]}, [], []},
|
|
|
|
raw: ")"
|
|
|
|
]}
|
|
|
|
end
|
|
|
|
|
|
|
|
{params, filtered} =
|
|
|
|
if aggregate.query && aggregate.query.filter &&
|
|
|
|
not match?(%Ash.Filter{expression: nil}, aggregate.query.filter) do
|
|
|
|
{params, expr} =
|
|
|
|
filter_to_expr(
|
|
|
|
aggregate.query.filter,
|
|
|
|
query.__ash_bindings__.bindings,
|
|
|
|
query.select.params
|
|
|
|
)
|
|
|
|
|
|
|
|
{params, {:filter, [], [field, expr]}}
|
|
|
|
else
|
|
|
|
{[], field}
|
|
|
|
end
|
|
|
|
|
|
|
|
casted =
|
|
|
|
if kind == :first do
|
|
|
|
{:type, [],
|
|
|
|
[
|
|
|
|
{:fragment, [],
|
|
|
|
[
|
|
|
|
raw: "(",
|
|
|
|
expr: filtered,
|
|
|
|
raw: ")[1]"
|
|
|
|
]},
|
|
|
|
type
|
|
|
|
]}
|
|
|
|
else
|
|
|
|
{:type, [],
|
|
|
|
[
|
|
|
|
filtered,
|
|
|
|
{:array, type}
|
|
|
|
]}
|
|
|
|
end
|
|
|
|
|
|
|
|
new_expr = {:merge, [], [query.select.expr, {:%{}, [], [{aggregate.name, casted}]}]}
|
|
|
|
|
|
|
|
%{query | select: %{query.select | expr: new_expr, params: params}}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp add_subquery_aggregate_select(query, %{kind: :list} = aggregate, _resource) do
|
2020-12-29 13:26:04 +13:00
|
|
|
query = default_bindings(query, aggregate.resource)
|
|
|
|
key = aggregate.field
|
|
|
|
type = Ash.Type.ecto_type(aggregate.type)
|
|
|
|
|
|
|
|
field =
|
|
|
|
if aggregate.query && aggregate.query.sort && aggregate.query.sort != [] do
|
|
|
|
sort_expr =
|
|
|
|
aggregate.query.sort
|
|
|
|
|> Enum.map(fn {sort, order} ->
|
|
|
|
case order_to_postgres_order(order) do
|
|
|
|
nil ->
|
|
|
|
[expr: {{:., [], [{:&, [], [0]}, sort]}, [], []}]
|
|
|
|
|
|
|
|
order ->
|
|
|
|
[expr: {{:., [], [{:&, [], [0]}, sort]}, [], []}, raw: order]
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|> Enum.intersperse(raw: ", ")
|
|
|
|
|> List.flatten()
|
|
|
|
|
|
|
|
{:fragment, [],
|
|
|
|
[
|
|
|
|
raw: "array_agg(",
|
|
|
|
expr: {{:., [], [{:&, [], [0]}, key]}, [], []},
|
2021-04-27 08:45:47 +12:00
|
|
|
raw: " ORDER BY "
|
2020-12-29 13:26:04 +13:00
|
|
|
] ++
|
|
|
|
sort_expr ++ [raw: ")"]}
|
|
|
|
else
|
|
|
|
{:fragment, [],
|
|
|
|
[
|
|
|
|
raw: "array_agg(",
|
|
|
|
expr: {{:., [], [{:&, [], [0]}, key]}, [], []},
|
|
|
|
raw: ")"
|
|
|
|
]}
|
|
|
|
end
|
|
|
|
|
|
|
|
{params, filtered} =
|
|
|
|
if aggregate.query && aggregate.query.filter &&
|
|
|
|
not match?(%Ash.Filter{expression: nil}, aggregate.query.filter) do
|
|
|
|
{params, expr} =
|
|
|
|
filter_to_expr(
|
|
|
|
aggregate.query.filter,
|
|
|
|
query.__ash_bindings__.bindings,
|
|
|
|
query.select.params
|
|
|
|
)
|
|
|
|
|
|
|
|
{params, {:filter, [], [field, expr]}}
|
|
|
|
else
|
|
|
|
{[], field}
|
|
|
|
end
|
|
|
|
|
|
|
|
cast = {:type, [], [filtered, {:array, type}]}
|
|
|
|
|
|
|
|
new_expr = {:merge, [], [query.select.expr, {:%{}, [], [{aggregate.name, cast}]}]}
|
|
|
|
|
|
|
|
%{query | select: %{query.select | expr: new_expr, params: params}}
|
|
|
|
end
|
|
|
|
|
2021-04-05 08:05:41 +12:00
|
|
|
defp add_subquery_aggregate_select(query, %{kind: kind} = aggregate, resource)
|
|
|
|
when kind in [:count, :sum] do
|
2020-09-02 16:01:34 +12:00
|
|
|
query = default_bindings(query, aggregate.resource)
|
2021-02-23 17:53:18 +13:00
|
|
|
key = aggregate.field || List.first(Ash.Resource.Info.primary_key(resource))
|
2020-07-23 17:13:47 +12:00
|
|
|
type = Ash.Type.ecto_type(aggregate.type)
|
|
|
|
|
2021-04-05 08:05:41 +12:00
|
|
|
field = {kind, [], [{{:., [], [{:&, [], [0]}, key]}, [], []}]}
|
2020-07-23 17:13:47 +12:00
|
|
|
|
|
|
|
{params, filtered} =
|
2020-12-29 13:26:04 +13:00
|
|
|
if aggregate.query && aggregate.query.filter &&
|
|
|
|
not match?(%Ash.Filter{expression: nil}, aggregate.query.filter) do
|
2020-07-23 17:13:47 +12:00
|
|
|
{params, expr} =
|
|
|
|
filter_to_expr(
|
|
|
|
aggregate.query.filter,
|
|
|
|
query.__ash_bindings__.bindings,
|
|
|
|
query.select.params
|
|
|
|
)
|
|
|
|
|
|
|
|
{params, {:filter, [], [field, expr]}}
|
|
|
|
else
|
|
|
|
{[], field}
|
|
|
|
end
|
|
|
|
|
|
|
|
cast = {:type, [], [filtered, type]}
|
|
|
|
|
|
|
|
new_expr = {:merge, [], [query.select.expr, {:%{}, [], [{aggregate.name, cast}]}]}
|
|
|
|
|
|
|
|
%{query | select: %{query.select | expr: new_expr, params: params}}
|
|
|
|
end
|
|
|
|
|
2020-06-19 15:04:41 +12:00
|
|
|
defp relationship_path_to_relationships(resource, path, acc \\ [])
|
|
|
|
defp relationship_path_to_relationships(_resource, [], acc), do: Enum.reverse(acc)
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-06-19 15:04:41 +12:00
|
|
|
defp relationship_path_to_relationships(resource, [relationship | rest], acc) do
|
2021-02-23 17:53:18 +13:00
|
|
|
relationship = Ash.Resource.Info.relationship(resource, relationship)
|
2020-06-19 15:04:41 +12:00
|
|
|
|
|
|
|
relationship_path_to_relationships(relationship.destination, rest, [relationship | acc])
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
defp join_all_relationships(query, relationship_paths, path \\ [], source \\ nil) do
|
|
|
|
query = default_bindings(query, source)
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
Enum.reduce(relationship_paths, query, fn
|
|
|
|
{_join_type, []}, query ->
|
|
|
|
query
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
{join_type, [relationship | rest_rels]}, query ->
|
|
|
|
source = source || relationship.source
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
current_path = path ++ [relationship]
|
2020-07-23 17:13:47 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
current_join_type =
|
|
|
|
case join_type do
|
|
|
|
{:aggregate, _name, _agg} when rest_rels != [] ->
|
|
|
|
:left
|
2020-07-23 17:13:47 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
other ->
|
|
|
|
other
|
|
|
|
end
|
|
|
|
|
|
|
|
if has_binding?(source, Enum.reverse(current_path), query, current_join_type) do
|
|
|
|
query
|
|
|
|
else
|
|
|
|
joined_query =
|
|
|
|
join_relationship(
|
|
|
|
query,
|
|
|
|
relationship,
|
|
|
|
Enum.map(path, & &1.name),
|
|
|
|
current_join_type,
|
|
|
|
source
|
|
|
|
)
|
|
|
|
|
|
|
|
joined_query_with_distinct = add_distinct(relationship, join_type, joined_query)
|
|
|
|
|
|
|
|
join_all_relationships(
|
|
|
|
joined_query_with_distinct,
|
|
|
|
[{join_type, rest_rels}],
|
|
|
|
current_path,
|
|
|
|
source
|
|
|
|
)
|
|
|
|
end
|
2020-07-23 17:13:47 +12:00
|
|
|
end)
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
defp has_binding?(resource, path, query, {:aggregate, _, _}),
|
|
|
|
do: has_binding?(resource, path, query, :aggregate)
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
defp has_binding?(resource, candidate_path, %{__ash_bindings__: _} = query, type) do
|
|
|
|
Enum.any?(query.__ash_bindings__.bindings, fn
|
|
|
|
{_, %{path: path, source: source, type: ^type}} ->
|
|
|
|
Ash.SatSolver.synonymous_relationship_paths?(resource, path, candidate_path, source)
|
2020-07-23 17:13:47 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
_ ->
|
|
|
|
false
|
|
|
|
end)
|
2020-07-23 17:13:47 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
defp has_binding?(_, _, _, _), do: false
|
|
|
|
|
|
|
|
defp get_binding(resource, path, %{__ash_bindings__: _} = query, type) do
|
|
|
|
paths =
|
|
|
|
Enum.flat_map(query.__ash_bindings__.bindings, fn
|
|
|
|
{binding, %{path: path, type: ^type}} ->
|
|
|
|
[{binding, path}]
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
[]
|
|
|
|
end)
|
|
|
|
|
|
|
|
Enum.find_value(paths, fn {binding, candidate_path} ->
|
|
|
|
Ash.SatSolver.synonymous_relationship_paths?(resource, candidate_path, path) && binding
|
2020-06-14 19:04:18 +12:00
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2020-07-23 17:13:47 +12:00
|
|
|
defp get_binding(_, _, _, _), do: nil
|
|
|
|
|
|
|
|
defp add_distinct(relationship, join_type, joined_query) do
|
2020-06-14 19:04:18 +12:00
|
|
|
if relationship.cardinality == :many and join_type == :left && !joined_query.distinct do
|
|
|
|
from(row in joined_query,
|
2021-02-23 17:53:18 +13:00
|
|
|
distinct: ^Ash.Resource.Info.primary_key(relationship.destination)
|
2020-06-14 19:04:18 +12:00
|
|
|
)
|
|
|
|
else
|
|
|
|
joined_query
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
defp join_relationship(query, relationship, path, join_type, source) do
|
|
|
|
case Map.get(query.__ash_bindings__.bindings, path) do
|
2020-06-14 19:04:18 +12:00
|
|
|
%{type: existing_join_type} when join_type != existing_join_type ->
|
|
|
|
raise "unreachable?"
|
|
|
|
|
|
|
|
nil ->
|
2020-09-02 16:01:34 +12:00
|
|
|
do_join_relationship(query, relationship, path, join_type, source)
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
_ ->
|
|
|
|
query
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
defp do_join_relationship(query, %{type: :many_to_many} = relationship, path, kind, source) do
|
2021-04-30 09:31:19 +12:00
|
|
|
join_relationship = Ash.Resource.Info.relationship(source, relationship.join_relationship)
|
|
|
|
relationship_through = maybe_get_resource_query(relationship.through, join_relationship)
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
relationship_destination =
|
2021-04-30 09:31:19 +12:00
|
|
|
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination, relationship))
|
2020-09-02 16:01:34 +12:00
|
|
|
|
|
|
|
current_binding =
|
|
|
|
Enum.find_value(query.__ash_bindings__.bindings, 0, fn {binding, data} ->
|
|
|
|
if data.type == kind && data.path == Enum.reverse(path) do
|
|
|
|
binding
|
|
|
|
end
|
|
|
|
end)
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
new_query =
|
2020-09-02 16:01:34 +12:00
|
|
|
case kind do
|
|
|
|
{:aggregate, _, subquery} ->
|
2020-07-23 17:13:47 +12:00
|
|
|
subquery =
|
|
|
|
subquery(
|
|
|
|
from(destination in subquery,
|
|
|
|
where:
|
|
|
|
field(destination, ^relationship.destination_field) ==
|
|
|
|
field(
|
|
|
|
parent_as(:rel_through),
|
|
|
|
^relationship.destination_field_on_join_table
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
from([{row, current_binding}] in query,
|
2020-07-23 17:13:47 +12:00
|
|
|
left_join: through in ^relationship_through,
|
|
|
|
as: :rel_through,
|
|
|
|
on:
|
|
|
|
field(row, ^relationship.source_field) ==
|
|
|
|
field(through, ^relationship.source_field_on_join_table),
|
|
|
|
left_lateral_join: destination in ^subquery,
|
|
|
|
on:
|
|
|
|
field(destination, ^relationship.destination_field) ==
|
|
|
|
field(through, ^relationship.destination_field_on_join_table)
|
|
|
|
)
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
:inner ->
|
|
|
|
from([{row, current_binding}] in query,
|
|
|
|
join: through in ^relationship_through,
|
|
|
|
on:
|
|
|
|
field(row, ^relationship.source_field) ==
|
|
|
|
field(through, ^relationship.source_field_on_join_table),
|
|
|
|
join: destination in ^relationship_destination,
|
|
|
|
on:
|
|
|
|
field(destination, ^relationship.destination_field) ==
|
|
|
|
field(through, ^relationship.destination_field_on_join_table)
|
|
|
|
)
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
from([{row, current_binding}] in query,
|
2020-07-23 17:13:47 +12:00
|
|
|
left_join: through in ^relationship_through,
|
|
|
|
on:
|
|
|
|
field(row, ^relationship.source_field) ==
|
|
|
|
field(through, ^relationship.source_field_on_join_table),
|
|
|
|
left_join: destination in ^relationship_destination,
|
|
|
|
on:
|
|
|
|
field(destination, ^relationship.destination_field) ==
|
|
|
|
field(through, ^relationship.destination_field_on_join_table)
|
|
|
|
)
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
join_path =
|
|
|
|
Enum.reverse([String.to_existing_atom(to_string(relationship.name) <> "_join_assoc") | path])
|
|
|
|
|
|
|
|
full_path = Enum.reverse([relationship.name | path])
|
|
|
|
|
2020-07-23 17:13:47 +12:00
|
|
|
binding_data =
|
2020-09-02 16:01:34 +12:00
|
|
|
case kind do
|
|
|
|
{:aggregate, name, _agg} ->
|
|
|
|
%{type: :aggregate, name: name, path: full_path, source: source}
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
%{type: kind, path: full_path, source: source}
|
2020-07-23 17:13:47 +12:00
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
new_query
|
2020-09-02 16:01:34 +12:00
|
|
|
|> add_binding(%{path: join_path, type: :left, source: source})
|
2020-07-23 17:13:47 +12:00
|
|
|
|> add_binding(binding_data)
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
defp do_join_relationship(query, relationship, path, kind, source) do
|
2020-06-14 19:04:18 +12:00
|
|
|
relationship_destination =
|
2021-04-30 09:31:19 +12:00
|
|
|
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination, relationship))
|
2020-09-02 16:01:34 +12:00
|
|
|
|
|
|
|
current_binding =
|
|
|
|
Enum.find_value(query.__ash_bindings__.bindings, 0, fn {binding, data} ->
|
|
|
|
if data.type == kind && data.path == Enum.reverse(path) do
|
|
|
|
binding
|
|
|
|
end
|
|
|
|
end)
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
new_query =
|
2020-09-02 16:01:34 +12:00
|
|
|
case kind do
|
|
|
|
{:aggregate, _, subquery} ->
|
2020-07-23 17:13:47 +12:00
|
|
|
subquery =
|
|
|
|
from(
|
|
|
|
sub in subquery(
|
|
|
|
from(destination in subquery,
|
|
|
|
where:
|
|
|
|
field(destination, ^relationship.destination_field) ==
|
|
|
|
field(parent_as(:rel_source), ^relationship.source_field)
|
|
|
|
)
|
|
|
|
),
|
|
|
|
select: field(sub, ^relationship.destination_field)
|
|
|
|
)
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
from([{row, current_binding}] in query,
|
2020-07-23 17:13:47 +12:00
|
|
|
as: :rel_source,
|
|
|
|
left_lateral_join: destination in ^subquery,
|
|
|
|
on:
|
|
|
|
field(row, ^relationship.source_field) ==
|
|
|
|
field(destination, ^relationship.destination_field)
|
|
|
|
)
|
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
:inner ->
|
|
|
|
from([{row, current_binding}] in query,
|
|
|
|
join: destination in ^relationship_destination,
|
|
|
|
on:
|
|
|
|
field(row, ^relationship.source_field) ==
|
|
|
|
field(destination, ^relationship.destination_field)
|
|
|
|
)
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
from([{row, current_binding}] in query,
|
2020-07-23 17:13:47 +12:00
|
|
|
left_join: destination in ^relationship_destination,
|
|
|
|
on:
|
|
|
|
field(row, ^relationship.source_field) ==
|
|
|
|
field(destination, ^relationship.destination_field)
|
|
|
|
)
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
full_path = Enum.reverse([relationship.name | path])
|
|
|
|
|
2020-07-23 17:13:47 +12:00
|
|
|
binding_data =
|
2020-09-02 16:01:34 +12:00
|
|
|
case kind do
|
|
|
|
{:aggregate, name, _agg} ->
|
|
|
|
%{type: :aggregate, name: name, path: full_path, source: source}
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
%{type: kind, path: full_path, source: source}
|
2020-07-23 17:13:47 +12:00
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
|
|
|
|
new_query
|
2020-07-23 17:13:47 +12:00
|
|
|
|> add_binding(binding_data)
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
defp add_filter_expression(query, filter) do
|
2020-09-20 10:08:09 +12:00
|
|
|
wheres =
|
|
|
|
filter
|
|
|
|
|> split_and_statements()
|
|
|
|
|> Enum.map(fn filter ->
|
|
|
|
{params, expr} = filter_to_expr(filter, query.__ash_bindings__.bindings, [])
|
|
|
|
|
|
|
|
%Ecto.Query.BooleanExpr{
|
|
|
|
expr: expr,
|
|
|
|
op: :and,
|
|
|
|
params: params
|
|
|
|
}
|
|
|
|
end)
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-09-20 10:08:09 +12:00
|
|
|
%{query | wheres: query.wheres ++ wheres}
|
2020-09-02 16:01:34 +12:00
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2020-09-02 16:01:34 +12:00
|
|
|
defp split_and_statements(%Filter{expression: expression}) do
|
|
|
|
split_and_statements(expression)
|
|
|
|
end
|
|
|
|
|
2021-01-22 09:32:26 +13:00
|
|
|
defp split_and_statements(%BooleanExpression{op: :and, left: left, right: right}) do
|
2020-09-02 16:01:34 +12:00
|
|
|
split_and_statements(left) ++ split_and_statements(right)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp split_and_statements(%Not{expression: %Not{expression: expression}}) do
|
|
|
|
split_and_statements(expression)
|
|
|
|
end
|
|
|
|
|
2020-10-07 18:45:58 +13:00
|
|
|
defp split_and_statements(%Not{
|
2021-01-22 09:32:26 +13:00
|
|
|
expression: %BooleanExpression{op: :or, left: left, right: right}
|
2020-10-07 18:45:58 +13:00
|
|
|
}) do
|
2021-01-22 09:32:26 +13:00
|
|
|
split_and_statements(%BooleanExpression{
|
2020-09-02 16:01:34 +12:00
|
|
|
op: :and,
|
|
|
|
left: %Not{expression: left},
|
|
|
|
right: %Not{expression: right}
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
defp split_and_statements(other), do: [other]
|
|
|
|
|
2021-01-22 09:32:26 +13:00
|
|
|
defp filter_to_expr(expr, bindings, params, embedded? \\ false, type \\ nil)
|
|
|
|
|
|
|
|
defp filter_to_expr(%Filter{expression: expression}, bindings, params, embedded?, type) do
|
|
|
|
filter_to_expr(expression, bindings, params, embedded?, type)
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2020-06-19 15:04:41 +12:00
|
|
|
# A nil filter means "everything"
|
2021-01-22 09:32:26 +13:00
|
|
|
defp filter_to_expr(nil, _, _, _, _), do: {[], true}
|
2020-06-19 15:04:41 +12:00
|
|
|
# A true filter means "everything"
|
2021-01-22 09:32:26 +13:00
|
|
|
defp filter_to_expr(true, _, _, _, _), do: {[], true}
|
2020-06-19 15:04:41 +12:00
|
|
|
# A false filter means "nothing"
|
2021-01-22 09:32:26 +13:00
|
|
|
defp filter_to_expr(false, _, _, _, _), do: {[], false}
|
2020-06-19 15:04:41 +12:00
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp filter_to_expr(expression, bindings, params, embedded?, type) do
|
|
|
|
do_filter_to_expr(expression, bindings, params, embedded?, type)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp do_filter_to_expr(expr, bindings, params, embedded?, type \\ nil)
|
|
|
|
|
|
|
|
defp do_filter_to_expr(
|
2021-01-22 09:32:26 +13:00
|
|
|
%BooleanExpression{op: op, left: left, right: right},
|
|
|
|
bindings,
|
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
_type
|
|
|
|
) do
|
2021-01-27 09:07:26 +13:00
|
|
|
{params, left_expr} = do_filter_to_expr(left, bindings, params, embedded?)
|
|
|
|
{params, right_expr} = do_filter_to_expr(right, bindings, params, embedded?)
|
2020-06-19 15:04:41 +12:00
|
|
|
{params, {op, [], [left_expr, right_expr]}}
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(%Not{expression: expression}, bindings, params, embedded?, _type) do
|
|
|
|
{params, new_expression} = do_filter_to_expr(expression, bindings, params, embedded?)
|
2020-06-19 15:04:41 +12:00
|
|
|
{params, {:not, [], [new_expression]}}
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-22 09:32:26 +13:00
|
|
|
%TrigramSimilarity{arguments: [arg1, arg2], embedded?: pred_embedded?},
|
2020-12-24 08:46:49 +13:00
|
|
|
bindings,
|
2021-01-22 09:32:26 +13:00
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
_type
|
2020-12-24 08:46:49 +13:00
|
|
|
) do
|
2021-01-27 09:07:26 +13:00
|
|
|
{params, arg1} = do_filter_to_expr(arg1, bindings, params, pred_embedded? || embedded?)
|
|
|
|
{params, arg2} = do_filter_to_expr(arg2, bindings, params, pred_embedded? || embedded?)
|
2021-01-22 09:32:26 +13:00
|
|
|
|
|
|
|
{params, {:fragment, [], [raw: "similarity(", expr: arg1, raw: ", ", expr: arg2, raw: ")"]}}
|
2020-12-24 08:46:49 +13:00
|
|
|
end
|
|
|
|
|
2021-02-25 07:59:49 +13:00
|
|
|
defp do_filter_to_expr(
|
|
|
|
%Type{arguments: [arg1, arg2], embedded?: pred_embedded?},
|
|
|
|
bindings,
|
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
_type
|
|
|
|
)
|
|
|
|
when pred_embedded? or embedded? do
|
|
|
|
{params, arg1} = do_filter_to_expr(arg1, bindings, params, true)
|
|
|
|
{params, arg2} = do_filter_to_expr(arg2, bindings, params, true)
|
|
|
|
|
|
|
|
case maybe_ecto_type(arg2) do
|
|
|
|
nil ->
|
|
|
|
{params, {:type, [], [arg1, arg2]}}
|
|
|
|
|
|
|
|
type ->
|
|
|
|
case arg1 do
|
|
|
|
%{__predicate__?: _} ->
|
|
|
|
{params, {:type, [], [arg1, arg2]}}
|
|
|
|
|
|
|
|
value ->
|
|
|
|
{params, %Ecto.Query.Tagged{value: value, type: type}}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-22 09:32:26 +13:00
|
|
|
%Type{arguments: [arg1, arg2], embedded?: pred_embedded?},
|
2020-09-29 01:10:13 +13:00
|
|
|
bindings,
|
2021-01-22 09:32:26 +13:00
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
_type
|
2020-09-29 01:10:13 +13:00
|
|
|
) do
|
2021-01-27 09:07:26 +13:00
|
|
|
{params, arg1} = do_filter_to_expr(arg1, bindings, params, pred_embedded? || embedded?)
|
|
|
|
{params, arg2} = do_filter_to_expr(arg2, bindings, params, pred_embedded? || embedded?)
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-02-25 07:59:49 +13:00
|
|
|
arg2 = maybe_ecto_type(arg2)
|
|
|
|
|
2021-01-22 09:32:26 +13:00
|
|
|
{params, {:type, [], [arg1, arg2]}}
|
2020-09-29 01:10:13 +13:00
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-22 09:32:26 +13:00
|
|
|
%Fragment{arguments: arguments, embedded?: pred_embedded?},
|
2020-10-06 18:39:47 +13:00
|
|
|
bindings,
|
2021-01-22 09:32:26 +13:00
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
_type
|
2020-09-20 10:08:09 +12:00
|
|
|
) do
|
2021-01-22 09:32:26 +13:00
|
|
|
{params, fragment_data} =
|
|
|
|
Enum.reduce(arguments, {params, []}, fn
|
|
|
|
{:raw, str}, {params, fragment_data} ->
|
|
|
|
{params, fragment_data ++ [{:raw, str}]}
|
|
|
|
|
|
|
|
{:expr, expr}, {params, fragment_data} ->
|
2021-01-27 09:07:26 +13:00
|
|
|
{params, expr} = do_filter_to_expr(expr, bindings, params, pred_embedded? || embedded?)
|
2021-01-22 09:32:26 +13:00
|
|
|
{params, fragment_data ++ [{:expr, expr}]}
|
|
|
|
end)
|
|
|
|
|
|
|
|
{params, {:fragment, [], fragment_data}}
|
2020-06-29 15:47:07 +12:00
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-22 09:32:26 +13:00
|
|
|
%IsNil{left: left, right: right, embedded?: pred_embedded?},
|
2020-10-06 18:39:47 +13:00
|
|
|
bindings,
|
2021-01-22 09:32:26 +13:00
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
_type
|
2020-09-20 10:08:09 +12:00
|
|
|
) do
|
2021-01-27 09:07:26 +13:00
|
|
|
{params, left_expr} = do_filter_to_expr(left, bindings, params, pred_embedded? || embedded?)
|
|
|
|
{params, right_expr} = do_filter_to_expr(right, bindings, params, pred_embedded? || embedded?)
|
2021-01-22 09:32:26 +13:00
|
|
|
|
|
|
|
{params,
|
|
|
|
{:==, [],
|
|
|
|
[
|
|
|
|
{:is_nil, [], [left_expr]},
|
|
|
|
right_expr
|
|
|
|
]}}
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-22 09:32:26 +13:00
|
|
|
%Ago{arguments: [left, right], embedded?: _pred_embedded?},
|
|
|
|
_bindings,
|
|
|
|
params,
|
|
|
|
_embedded?,
|
|
|
|
_type
|
|
|
|
)
|
|
|
|
when is_integer(left) and (is_binary(right) or is_atom(right)) do
|
|
|
|
{params ++ [{DateTime.utc_now(), {:param, :any_datetime}}],
|
2021-01-25 07:24:43 +13:00
|
|
|
{:datetime_add, [], [{:^, [], [Enum.count(params)]}, left * -1, to_string(right)]}}
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-24 16:45:15 +13:00
|
|
|
%Contains{arguments: [left, %Ash.CiString{} = right], embedded?: pred_embedded?},
|
|
|
|
bindings,
|
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
type
|
|
|
|
) do
|
2021-01-27 09:07:26 +13:00
|
|
|
do_filter_to_expr(
|
2021-01-24 16:45:15 +13:00
|
|
|
%Fragment{
|
|
|
|
embedded?: pred_embedded?,
|
|
|
|
arguments: [
|
|
|
|
raw: "strpos(",
|
|
|
|
expr: left,
|
|
|
|
raw: "::citext, ",
|
|
|
|
expr: right,
|
|
|
|
raw: ") > 0"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
bindings,
|
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
type
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-24 16:45:15 +13:00
|
|
|
%Contains{arguments: [left, right], embedded?: pred_embedded?},
|
|
|
|
bindings,
|
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
type
|
|
|
|
) do
|
2021-01-27 09:07:26 +13:00
|
|
|
do_filter_to_expr(
|
2021-01-24 16:45:15 +13:00
|
|
|
%Fragment{
|
|
|
|
embedded?: pred_embedded?,
|
|
|
|
arguments: [
|
|
|
|
raw: "strpos(",
|
|
|
|
expr: left,
|
|
|
|
raw: ", ",
|
|
|
|
expr: right,
|
|
|
|
raw: ") > 0"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
bindings,
|
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
type
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-22 09:32:26 +13:00
|
|
|
%mod{
|
|
|
|
__predicate__?: _,
|
|
|
|
left: left,
|
|
|
|
right: right,
|
|
|
|
embedded?: pred_embedded?,
|
|
|
|
operator: op
|
|
|
|
},
|
2020-10-06 18:39:47 +13:00
|
|
|
bindings,
|
2021-01-22 09:32:26 +13:00
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
_type
|
2020-09-20 10:08:09 +12:00
|
|
|
) do
|
2021-04-28 04:09:00 +12:00
|
|
|
[left_type, right_type] = determine_types(mod, [left, right])
|
2021-01-22 09:32:26 +13:00
|
|
|
|
|
|
|
{params, left_expr} =
|
2021-01-27 09:07:26 +13:00
|
|
|
do_filter_to_expr(left, bindings, params, pred_embedded? || embedded?, left_type)
|
2021-01-22 09:32:26 +13:00
|
|
|
|
|
|
|
{params, right_expr} =
|
2021-01-27 09:07:26 +13:00
|
|
|
do_filter_to_expr(right, bindings, params, pred_embedded? || embedded?, right_type)
|
2020-10-06 18:39:47 +13:00
|
|
|
|
2020-12-24 08:46:49 +13:00
|
|
|
{params,
|
|
|
|
{op, [],
|
|
|
|
[
|
2021-01-22 09:32:26 +13:00
|
|
|
left_expr,
|
|
|
|
right_expr
|
2020-12-24 08:46:49 +13:00
|
|
|
]}}
|
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(
|
2021-01-22 09:32:26 +13:00
|
|
|
%Ref{attribute: %{name: name}} = ref,
|
|
|
|
bindings,
|
|
|
|
params,
|
|
|
|
_embedded?,
|
|
|
|
_type
|
|
|
|
) do
|
|
|
|
{params, {{:., [], [{:&, [], [ref_binding(ref, bindings)]}, name]}, [], []}}
|
2020-06-29 15:47:07 +12:00
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr({:embed, other}, _bindings, params, _true, _type) do
|
2021-01-22 09:32:26 +13:00
|
|
|
{params, other}
|
2020-09-20 10:08:09 +12:00
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(%Ash.CiString{string: string}, bindings, params, embedded?, type) do
|
|
|
|
do_filter_to_expr(
|
2021-01-24 16:45:15 +13:00
|
|
|
%Fragment{
|
|
|
|
embedded?: embedded?,
|
|
|
|
arguments: [
|
|
|
|
raw: "",
|
|
|
|
expr: string,
|
|
|
|
raw: "::citext"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
bindings,
|
|
|
|
params,
|
|
|
|
embedded?,
|
|
|
|
type
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(%MapSet{} = mapset, bindings, params, embedded?, type) do
|
|
|
|
do_filter_to_expr(Enum.to_list(mapset), bindings, params, embedded?, type)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp do_filter_to_expr(other, _bindings, params, true, _type) do
|
2021-01-22 09:32:26 +13:00
|
|
|
{params, other}
|
2020-09-24 03:20:26 +12:00
|
|
|
end
|
|
|
|
|
2021-01-27 09:07:26 +13:00
|
|
|
defp do_filter_to_expr(value, _bindings, params, false, type) do
|
2021-01-22 09:32:26 +13:00
|
|
|
type = type || :any
|
2021-04-28 05:16:44 +12:00
|
|
|
value = last_ditch_cast(value, type)
|
2021-01-22 09:32:26 +13:00
|
|
|
|
|
|
|
{params ++ [{value, type}], {:^, [], [Enum.count(params)]}}
|
2020-09-24 03:20:26 +12:00
|
|
|
end
|
|
|
|
|
2021-02-25 07:59:49 +13:00
|
|
|
defp maybe_ecto_type({:array, type}), do: {:array, maybe_ecto_type(type)}
|
|
|
|
|
|
|
|
defp maybe_ecto_type(type) when is_atom(type) do
|
|
|
|
if Ash.Type.ash_type?(type) do
|
|
|
|
Ash.Type.ecto_type(type)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp maybe_ecto_type(_type), do: nil
|
|
|
|
|
2021-04-28 05:03:02 +12:00
|
|
|
defp last_ditch_cast(value, {:in, type}) when is_list(value) do
|
|
|
|
Enum.map(value, &last_ditch_cast(&1, type))
|
|
|
|
end
|
|
|
|
|
|
|
|
defp last_ditch_cast(value, _) when is_atom(value) do
|
2021-01-22 09:32:26 +13:00
|
|
|
to_string(value)
|
2020-09-20 10:08:09 +12:00
|
|
|
end
|
|
|
|
|
2021-01-22 09:32:26 +13:00
|
|
|
defp last_ditch_cast(value, _type) do
|
|
|
|
value
|
2020-09-20 10:08:09 +12:00
|
|
|
end
|
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp determine_types(mod, values) do
|
|
|
|
mod.types()
|
|
|
|
|> Enum.map(fn types ->
|
|
|
|
case types do
|
|
|
|
:same ->
|
|
|
|
types =
|
|
|
|
for _ <- values do
|
|
|
|
:same
|
|
|
|
end
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
closest_fitting_type(types, values)
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
:any ->
|
|
|
|
for _ <- values do
|
|
|
|
:any
|
|
|
|
end
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
types ->
|
|
|
|
closest_fitting_type(types, values)
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|> Enum.min_by(fn types ->
|
|
|
|
types
|
|
|
|
|> Enum.map(&vagueness/1)
|
|
|
|
|> Enum.sum()
|
|
|
|
end)
|
|
|
|
end
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp closest_fitting_type(types, values) do
|
|
|
|
types_with_values = Enum.zip(types, values)
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
types_with_values
|
|
|
|
|> fill_in_known_types()
|
|
|
|
|> clarify_types()
|
|
|
|
end
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp clarify_types(types) do
|
|
|
|
basis =
|
|
|
|
types
|
|
|
|
|> Enum.map(&elem(&1, 0))
|
|
|
|
|> Enum.min_by(&vagueness(&1))
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
Enum.map(types, fn {type, _value} ->
|
|
|
|
replace_same(type, basis)
|
|
|
|
end)
|
|
|
|
end
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp replace_same({:in, type}, basis) do
|
|
|
|
{:in, replace_same(type, basis)}
|
|
|
|
end
|
2021-01-27 09:07:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp replace_same(:same, :same) do
|
|
|
|
:any
|
|
|
|
end
|
2021-01-27 09:07:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp replace_same(:same, {:in, :same}) do
|
|
|
|
{:in, :any}
|
|
|
|
end
|
2021-01-27 09:07:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp replace_same(:same, basis) do
|
|
|
|
basis
|
|
|
|
end
|
2021-01-27 09:07:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp replace_same(other, _basis) do
|
|
|
|
other
|
2021-01-22 09:32:26 +13:00
|
|
|
end
|
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp fill_in_known_types(types) do
|
|
|
|
Enum.map(types, &fill_in_known_type/1)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp fill_in_known_type({vague_type, %Ref{attribute: %{type: type}}} = ref)
|
|
|
|
when vague_type in [:any, :same] do
|
|
|
|
if Ash.Type.ash_type?(type) do
|
|
|
|
{type |> Ash.Type.storage_type() |> array_to_in(), ref}
|
|
|
|
else
|
|
|
|
{type |> array_to_in(), ref}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp fill_in_known_type(
|
|
|
|
{{:array, type}, %Ref{attribute: %{type: {:array, type}} = attribute} = ref}
|
|
|
|
) do
|
|
|
|
{:in, fill_in_known_type({type, %{ref | attribute: %{attribute | type: type}}})}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp fill_in_known_type({type, value}), do: {array_to_in(type), value}
|
|
|
|
|
|
|
|
defp array_to_in({:array, v}), do: {:in, array_to_in(v)}
|
|
|
|
defp array_to_in(v), do: v
|
2021-01-22 09:32:26 +13:00
|
|
|
|
2021-04-28 04:09:00 +12:00
|
|
|
defp vagueness({:in, type}), do: vagueness(type)
|
|
|
|
defp vagueness(:same), do: 2
|
|
|
|
defp vagueness(:any), do: 1
|
|
|
|
defp vagueness(_), do: 0
|
2021-01-22 09:32:26 +13:00
|
|
|
|
|
|
|
defp ref_binding(ref, bindings) do
|
|
|
|
case ref.attribute do
|
|
|
|
%Ash.Resource.Attribute{} ->
|
|
|
|
Enum.find_value(bindings, fn {binding, data} ->
|
|
|
|
data.path == ref.relationship_path && data.type in [:inner, :left, :root] && binding
|
|
|
|
end)
|
|
|
|
|
|
|
|
%Ash.Query.Aggregate{} = aggregate ->
|
|
|
|
Enum.find_value(bindings, fn {binding, data} ->
|
|
|
|
data.path == aggregate.relationship_path && data.type == :aggregate && binding
|
|
|
|
end)
|
2020-09-20 10:08:09 +12:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-07-23 17:13:47 +12:00
|
|
|
defp add_binding(query, data) do
|
2020-06-14 19:04:18 +12:00
|
|
|
current = query.__ash_bindings__.current
|
|
|
|
bindings = query.__ash_bindings__.bindings
|
|
|
|
|
|
|
|
new_ash_bindings = %{
|
|
|
|
query.__ash_bindings__
|
2020-07-23 17:13:47 +12:00
|
|
|
| bindings: Map.put(bindings, current, data),
|
2020-06-14 19:04:18 +12:00
|
|
|
current: current + 1
|
|
|
|
}
|
|
|
|
|
|
|
|
%{query | __ash_bindings__: new_ash_bindings}
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
2020-07-08 12:01:01 +12:00
|
|
|
def transaction(resource, func) do
|
|
|
|
repo(resource).transaction(func)
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
2020-07-08 12:01:01 +12:00
|
|
|
def rollback(resource, term) do
|
|
|
|
repo(resource).rollback(term)
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
|
|
|
|
2021-04-30 09:31:19 +12:00
|
|
|
defp maybe_get_resource_query(resource, relationship) do
|
|
|
|
resource
|
|
|
|
|> Ash.Query.new()
|
|
|
|
|> Ash.Query.set_context(relationship.context)
|
|
|
|
|> Ash.Query.do_filter(Map.get(relationship, :filter))
|
|
|
|
|> Ash.Query.data_layer_query(only_validate_filter?: false)
|
|
|
|
|> case do
|
2020-09-20 10:08:09 +12:00
|
|
|
{:ok, query} -> query
|
|
|
|
{:error, error} -> {:error, error}
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|
2021-02-06 12:59:33 +13:00
|
|
|
|
|
|
|
defp table(resource, changeset) do
|
|
|
|
changeset.context[:data_layer][:table] || AshPostgres.table(resource)
|
|
|
|
end
|
2021-03-22 10:58:47 +13:00
|
|
|
|
|
|
|
defp raise_table_error!(resource, operation) do
|
|
|
|
if AshPostgres.polymorphic?(resource) do
|
|
|
|
raise """
|
|
|
|
Could not determine table for #{operation} on #{inspect(resource)}.
|
|
|
|
|
|
|
|
Polymorphic resources require that the `data_layer[:table]` context is provided.
|
|
|
|
See the guide on polymorphic resources for more information.
|
|
|
|
"""
|
|
|
|
else
|
|
|
|
raise """
|
|
|
|
Could not determine table for #{operation} on #{inspect(resource)}.
|
|
|
|
"""
|
|
|
|
end
|
|
|
|
end
|
2020-06-14 19:04:18 +12:00
|
|
|
end
|