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-09-21 08:38:36 +12:00
@index % Ash.Dsl.Entity {
name : :index ,
describe : """
Add an index to be managed by the migration generator .
""" ,
examples : [
" index [ \" column \" , \" column2 \" ], unique: true, where: \" thing = TRUE \" "
] ,
target : AshPostgres.CustomIndex ,
schema : AshPostgres.CustomIndex . schema ( ) ,
args : [ :fields ]
}
@custom_indexes % Ash.Dsl.Section {
name : :custom_indexes ,
describe : """
A section for configuring indexes to be created by the migration generator .
In general , prefer to use ` identities ` for simple unique constraints . This is a tool to allow
for declaring more complex indexes .
""" ,
examples : [
"""
custom_indexes do
index [ :column1 , :column2 ] , unique : true , where : " thing = TRUE "
end
"""
] ,
entities : [
@index
]
}
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-09-21 08:38:36 +12:00
@custom_indexes ,
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` "
] ,
2021-11-10 22:18:36 +13:00
migration_types : [
type : :keyword_list ,
default : [ ] ,
doc :
" A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults. "
] ,
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-06-04 17:48:35 +12:00
alias Ash.Query.Function . { Ago , Contains , If }
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 ]
2021-06-04 17:48:35 +12:00
# This creates the atoms 0..500, which are used for calculations
2021-07-02 17:01:56 +12:00
# If you know of a way to get around the fact that subquery `parent_as` must be
# an atom, let me know.
2021-07-02 17:08:53 +12:00
@atoms Enum . into ( 0 . . 500 , %{ } , fn i ->
{ i , String . to_atom ( to_string ( i ) ) }
end )
2021-06-04 17:48:35 +12:00
2020-12-27 19:20:12 +13:00
@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 )
2021-10-07 21:16:33 +13:00
data_layer == other_data_layer and repo ( resource ) == repo ( other_resource )
2020-08-26 16:28:55 +12:00
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
2021-05-09 15:25:28 +12:00
def can? ( _ , { :aggregate , :first } ) , do : true
def can? ( _ , { :aggregate , :list } ) , do : true
2020-07-23 17:13:47 +12:00
def can? ( _ , :aggregate_filter ) , do : true
def can? ( _ , :aggregate_sort ) , do : true
2021-06-04 17:48:35 +12:00
def can? ( _ , :expression_calculation ) , do : true
2021-06-06 10:13:20 +12:00
def can? ( _ , :expression_calculation_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-06-04 17:48:35 +12:00
data_layer_query =
if context [ :data_layer ] [ :table ] do
%{
data_layer_query
| from : %{ data_layer_query . from | source : { context [ :data_layer ] [ :table ] , resource } }
}
else
data_layer_query
end
data_layer_query =
data_layer_query
|> default_bindings ( resource , context )
{ :ok , data_layer_query }
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
2021-07-25 03:28:58 +12:00
defp repo_opts ( %{ tenant : tenant , resource : resource } ) when not is_nil ( tenant ) do
if Ash.Resource.Info . multitenancy_strategy ( resource ) == :context do
[ prefix : tenant ]
else
[ ]
end
end
defp repo_opts ( _ ) , do : [ ]
defp lateral_join_repo_opts ( %{ tenant : tenant } , 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
2021-07-25 03:28:58 +12:00
defp lateral_join_repo_opts ( _ , _ ) , do : [ ]
2020-10-29 15:26:45 +13:00
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
2021-05-07 19:09:49 +12:00
case lateral_join_query (
query ,
root_data ,
path
) do
{ :ok , lateral_join_query } ->
source_resource =
path
|> Enum . at ( 0 )
|> elem ( 0 )
2021-05-08 03:04:09 +12:00
|> Map . get ( :resource )
2021-05-07 19:09:49 +12: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-18 12:13:51 +13:00
2021-07-25 03:28:58 +12:00
{ :ok , repo ( source_resource ) . one ( query , lateral_join_repo_opts ( query , source_resource ) ) }
2020-10-18 12:13:51 +13:00
2021-05-07 19:09:49 +12:00
{ :error , error } ->
{ :error , error }
end
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
2021-07-25 08:59:23 +12:00
source_query =
path
|> Enum . at ( 0 )
|> elem ( 0 )
2021-05-07 19:09:49 +12:00
case lateral_join_query (
query ,
root_data ,
path
) do
{ :ok , query } ->
source_resource =
path
|> Enum . at ( 0 )
|> elem ( 0 )
2021-05-08 03:04:09 +12:00
|> Map . get ( :resource )
2021-05-07 19:09:49 +12:00
2021-07-25 08:59:23 +12:00
{ :ok ,
repo ( source_resource ) . all ( query , lateral_join_repo_opts ( source_query , source_resource ) ) }
2021-04-30 09:31:19 +12:00
2021-05-07 19:09:49 +12:00
{ :error , error } ->
{ :error , error }
end
2020-10-18 12:13:51 +13:00
end
defp lateral_join_query (
query ,
root_data ,
2021-07-25 08:59:23 +12:00
[ { source_query , 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 ) )
2021-05-08 03:04:09 +12:00
source_query = Ash.Query . new ( source_query )
2020-08-26 16:28:55 +12:00
subquery =
2021-07-20 05:56:36 +12:00
if query . windows [ :order ] do
subquery (
from ( destination in query ,
select_merge : %{ __order__ : over ( row_number ( ) , :order ) } ,
where :
field ( destination , ^ destination_field ) ==
field ( parent_as ( :source_record ) , ^ source_field )
)
2021-07-28 15:03:39 +12:00
|> set_subquery_prefix ( source_query , relationship . destination )
2020-08-26 16:28:55 +12:00
)
2021-07-20 05:56:36 +12:00
else
subquery (
from ( destination in query ,
where :
field ( destination , ^ destination_field ) ==
field ( parent_as ( :source_record ) , ^ source_field )
)
2021-07-28 15:03:39 +12:00
|> set_subquery_prefix ( source_query , relationship . destination )
2021-07-20 05:56:36 +12:00
)
end
2020-08-26 16:28:55 +12:00
2021-05-08 03:04:09 +12:00
source_query . resource
|> Ash.Query . set_context ( %{ :data_layer = > source_query . context [ :data_layer ] } )
|> Ash.Query . set_tenant ( source_query . tenant )
2021-05-04 17:36:25 +12:00
|> set_lateral_join_prefix ( query )
2021-05-07 19:09:49 +12:00
|> case do
%{ valid? : true } = query ->
Ash.Query . data_layer_query ( query )
query ->
{ :error , query }
end
2020-11-03 16:59:51 +13:00
|> case do
{ :ok , data_layer_query } ->
2021-07-20 05:56:36 +12:00
if query . windows [ :order ] do
{ :ok ,
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 ) ,
order_by : destination . __order__ ,
select : destination ,
distinct : true
) }
else
{ :ok ,
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 ,
distinct : true
) }
end
2020-11-03 16:59:51 +13:00
{ :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 ,
[
2021-05-08 03:04:09 +12:00
{ source_query , source_field , source_field_on_join_table , relationship } ,
2021-04-30 09:31:19 +12:00
{ through_resource , destination_field_on_join_table , destination_field ,
through_relationship }
]
) do
2021-05-08 03:04:09 +12:00
source_query = Ash.Query . new ( source_query )
2021-04-30 09:31:19 +12:00
source_values = Enum . map ( root_data , & Map . get ( &1 , source_field ) )
2021-05-08 04:09:09 +12:00
through_resource
2021-04-30 09:31:19 +12:00
|> Ash.Query . new ( )
|> Ash.Query . set_context ( through_relationship . context )
|> Ash.Query . do_filter ( through_relationship . filter )
2021-05-08 03:04:09 +12:00
|> Ash.Query . sort ( through_relationship . sort )
|> Ash.Query . set_tenant ( source_query . tenant )
|> set_lateral_join_prefix ( query )
2021-05-07 19:09:49 +12:00
|> case do
%{ valid? : true } = query ->
Ash.Query . data_layer_query ( query )
query ->
{ :error , query }
end
2021-04-30 09:31:19 +12:00
|> case do
{ :ok , through_query } ->
2021-05-08 03:04:09 +12:00
source_query . resource
2021-04-30 09:31:19 +12:00
|> Ash.Query . new ( )
|> Ash.Query . set_context ( relationship . context )
2021-05-08 03:04:09 +12:00
|> Ash.Query . set_context ( %{ :data_layer = > source_query . context [ :data_layer ] } )
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 )
2021-05-07 19:09:49 +12:00
|> case do
%{ valid? : true } = query ->
Ash.Query . data_layer_query ( query )
query ->
{ :error , query }
end
2021-04-30 09:31:19 +12:00
|> case do
{ :ok , data_layer_query } ->
2021-07-20 05:56:36 +12:00
if query . windows [ :order ] do
subquery =
subquery (
2021-07-25 08:59:23 +12:00
from (
2021-07-28 03:13:48 +12:00
destination in query ,
2021-07-20 05:56:36 +12:00
select_merge : %{ __order__ : over ( row_number ( ) , :order ) } ,
2021-07-25 08:59:23 +12:00
join :
through in ^ set_subquery_prefix (
through_query ,
source_query ,
relationship . through
) ,
2021-07-20 05:56:36 +12:00
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-07-28 15:03:39 +12:00
|> set_subquery_prefix (
source_query ,
relationship . destination
)
2021-07-28 03:13:48 +12:00
)
2021-07-20 05:56:36 +12:00
{ :ok ,
from ( source in data_layer_query ,
as : :source_record ,
where : field ( source , ^ source_field ) in ^ source_values ,
inner_lateral_join : destination in ^ subquery ,
select : destination ,
order_by : destination . __order__ ,
distinct : true
) }
else
subquery =
subquery (
2021-07-25 08:59:23 +12:00
from (
2021-07-28 03:13:48 +12:00
destination in query ,
2021-07-25 08:59:23 +12:00
join :
through in ^ set_subquery_prefix (
through_query ,
source_query ,
relationship . through
) ,
2021-07-20 05:56:36 +12:00
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-07-28 15:03:39 +12:00
|> set_subquery_prefix (
source_query ,
relationship . destination
)
2021-07-28 03:13:48 +12:00
)
2021-05-04 18:14:24 +12:00
2021-07-20 05:56:36 +12:00
{ :ok ,
from ( source in data_layer_query ,
as : :source_record ,
where : field ( source , ^ source_field ) in ^ source_values ,
inner_lateral_join : destination in ^ subquery ,
select : destination ,
distinct : true
) }
end
2021-04-30 09:31:19 +12:00
{ :error , error } ->
{ :error , error }
end
{ :error , error } ->
{ :error , error }
end
end
2021-07-25 08:59:23 +12:00
defp set_subquery_prefix ( data_layer_query , source_query , resource ) do
2021-07-28 15:03:39 +12:00
config = repo ( resource ) . config ( )
2021-07-25 08:59:23 +12:00
if Ash.Resource.Info . multitenancy_strategy ( resource ) == :context do
2021-07-28 15:03:39 +12:00
%{
data_layer_query
| prefix :
to_string (
source_query . tenant || config [ :default_prefix ] ||
" public "
)
}
2021-07-25 08:59:23 +12:00
else
2021-07-28 15:03:39 +12:00
%{
data_layer_query
| prefix :
to_string (
config [ :default_prefix ] ||
" public "
)
}
2021-07-25 08:59:23 +12:00
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
2021-06-24 09:21:09 +12:00
Ash.Error.Changes.InvalidAttribute . exception (
field : field ,
message : message ,
private_vars : vars
)
2021-01-22 09:32:26 +13:00
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
2021-07-06 06:12:21 +12:00
table = changeset . context [ :data_layer ] [ :table ] || AshPostgres . table ( record . __struct__ )
2021-01-29 13:42:55 +13:00
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
2021-05-19 15:04:37 +12:00
def upsert ( resource , changeset , keys \\ nil ) do
keys = keys || Ash.Resource.Info . primary_key ( resource )
2021-11-25 13:31:59 +13:00
attributes = Map . keys ( changeset . attributes ) -- Map . get ( changeset , :defaults , [ ] ) -- keys
2021-05-19 15:04:37 +12:00
2020-10-29 15:26:45 +13:00
repo_opts =
changeset
|> repo_opts ( )
2021-11-25 13:31:59 +13:00
|> Keyword . put ( :on_conflict , { :replace , attributes } )
2021-05-19 15:04:37 +12:00
|> Keyword . put ( :conflict_target , keys )
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 )
2021-11-26 15:35:26 +13:00
|> repo ( resource ) . insert ( Keyword . put ( repo_opts , :returning , true ) )
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 ( )
2021-07-20 05:56:36 +12:00
|> Enum . reduce_while ( { :ok , % Ecto.Query.QueryExpr { expr : [ ] , params : [ ] } } , fn
{ order , % Ash.Query.Calculation { } = calc } , { :ok , query_expr } ->
2021-06-06 10:13:20 +12:00
type =
if calc . type do
2021-09-14 04:58:23 +12:00
parameterized_type ( calc . type , [ ] )
2021-06-06 10:13:20 +12:00
else
nil
end
calc . opts
|> calc . module . expression ( calc . context )
|> Ash.Filter . hydrate_refs ( %{
resource : resource ,
aggregates : query . __ash_bindings__ . aggregate_defs ,
calculations : %{ } ,
public? : false
} )
|> case do
{ :ok , expr } ->
2021-07-20 05:56:36 +12:00
{ params , expr } =
do_filter_to_expr ( expr , query . __ash_bindings__ , query_expr . params , false , type )
2021-06-06 10:13:20 +12:00
2021-07-20 05:56:36 +12:00
{ :cont ,
{ :ok , %{ query_expr | expr : query_expr . expr ++ [ { order , expr } ] , params : params } } }
2021-06-06 10:13:20 +12:00
{ :error , error } ->
{ :halt , { :error , error } }
2020-08-26 16:28:55 +12:00
end
2021-07-20 05:56:36 +12:00
{ order , sort } , { :ok , query_expr } ->
2021-07-02 07:28:27 +12:00
expr =
2021-06-06 10:13:20 +12:00
case Map . fetch ( query . __ash_bindings__ . aggregates , sort ) do
{ :ok , binding } ->
2021-07-02 07:28:27 +12:00
aggregate =
Ash.Resource.Info . aggregate ( resource , sort ) ||
raise " No such aggregate for query aggregate #{ inspect ( sort ) } "
{ :ok , field_type } =
if aggregate . field do
related = Ash.Resource.Info . related ( resource , aggregate . relationship_path )
attr = Ash.Resource.Info . attribute ( related , aggregate . field )
if attr && related do
2021-09-14 04:58:23 +12:00
{ :ok , parameterized_type ( attr . type , attr . constraints ) }
2021-07-02 07:28:27 +12:00
else
{ :ok , nil }
end
else
{ :ok , nil }
end
2021-10-26 11:53:34 +13:00
default_value =
aggregate . default || Ash.Query.Aggregate . default_value ( aggregate . kind )
2021-07-02 07:28:27 +12:00
if is_nil ( default_value ) do
{ { :. , [ ] , [ { :& , [ ] , [ binding ] } , sort ] } , [ ] , [ ] }
else
if field_type do
{ :coalesce , [ ] ,
[
{ { :. , [ ] , [ { :& , [ ] , [ binding ] } , sort ] } , [ ] , [ ] } ,
{ :type , [ ] ,
[
default_value ,
2021-09-14 04:58:23 +12:00
field_type
2021-07-02 07:28:27 +12:00
] }
] }
else
{ :coalesce , [ ] ,
[
{ { :. , [ ] , [ { :& , [ ] , [ binding ] } , sort ] } , [ ] , [ ] } ,
default_value
] }
end
end
2020-10-18 12:13:51 +13:00
2021-06-06 10:13:20 +12:00
:error ->
2021-07-02 07:28:27 +12:00
{ { :. , [ ] , [ { :& , [ ] , [ 0 ] } , sort ] } , [ ] , [ ] }
2021-06-06 10:13:20 +12:00
end
2020-10-18 12:13:51 +13:00
2021-07-20 09:12:01 +12:00
{ :cont , { :ok , %{ query_expr | expr : query_expr . expr ++ [ { order , expr } ] } } }
2021-07-20 05:56:36 +12:00
end )
|> case do
{ :ok , %{ expr : [ ] } } ->
{ :ok , query }
{ :ok , sort_expr } ->
2021-06-06 10:13:20 +12:00
new_query =
2021-07-20 05:56:36 +12:00
query
|> Map . update! ( :order_bys , fn order_bys ->
2021-06-06 10:13:20 +12:00
order_bys = order_bys || [ ]
order_bys ++ [ sort_expr ]
end )
2021-07-20 05:56:36 +12:00
|> Map . update! ( :windows , fn windows ->
order_by_expr = %{ sort_expr | expr : [ order_by : sort_expr . expr ] }
Keyword . put ( windows , :order , order_by_expr )
end )
2021-06-06 10:13:20 +12:00
2021-07-20 05:56:36 +12:00
{ :ok , new_query }
{ :error , error } ->
{ :error , error }
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
2021-06-06 10:13:20 +12:00
{ sort , { order , context } } ->
{ ash_to_ecto_order ( order ) , { sort , context } }
{ sort , order } ->
{ ash_to_ecto_order ( order ) , sort }
sort ->
sort
2020-06-14 19:04:18 +12:00
end )
end
2021-06-06 10:13:20 +12:00
defp ash_to_ecto_order ( :asc_nils_last ) , do : :asc_nulls_last
defp ash_to_ecto_order ( :asc_nils_first ) , do : :asc_nulls_first
defp ash_to_ecto_order ( :desc_nils_last ) , do : :desc_nulls_last
defp ash_to_ecto_order ( :desc_nils_first ) , do : :desc_nulls_first
defp ash_to_ecto_order ( other ) , do : other
2020-06-14 19:04:18 +12:00
@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 )
2021-05-07 19:09:49 +12:00
query
2021-05-09 15:25:28 +12:00
|> join_all_relationships ( relationship_paths , filter )
2021-05-07 19:09:49 +12:00
|> case do
{ :ok , query } ->
{ :ok , add_filter_expression ( query , filter ) }
2020-06-14 19:04:18 +12:00
2021-05-07 19:09:49 +12:00
{ :error , error } ->
{ :error , error }
end
2020-06-19 15:04:41 +12:00
end
2021-06-04 17:48:35 +12:00
defp default_bindings ( query , resource , context \\ %{ } ) do
2020-07-23 17:13:47 +12:00
Map . put_new ( query , :__ash_bindings__ , %{
current : Enum . count ( query . joins ) + 1 ,
2021-06-04 17:48:35 +12:00
calculations : %{ } ,
2020-07-23 17:13:47 +12:00
aggregates : %{ } ,
2021-06-06 10:13:20 +12:00
aggregate_defs : %{ } ,
2021-06-04 17:48:35 +12:00
context : context ,
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
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
2021-06-04 17:48:35 +12:00
def add_aggregate ( query , aggregate , _resource , add_base? \\ true ) do
2020-09-02 16:01:34 +12:00
resource = aggregate . resource
query = default_bindings ( query , resource )
2020-07-23 17:13:47 +12:00
2021-05-07 19:09:49 +12:00
query_and_binding =
2020-07-23 17:13:47 +12:00
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
2021-05-13 05:17:26 +12:00
if relationship . type == :many_to_many do
subquery = aggregate_subquery ( relationship , aggregate , query )
case join_all_relationships (
query ,
[
{ { :aggregate , aggregate . name , subquery } ,
relationship_path_to_relationships ( resource , aggregate . relationship_path ) }
] ,
nil
) do
{ :ok , new_query } ->
{ :ok ,
{ new_query ,
get_binding ( resource , aggregate . relationship_path , new_query , :aggregate ) } }
{ :error , error } ->
{ :error , error }
end
else
subquery = aggregate_subquery ( relationship , aggregate , query )
case join_all_relationships (
query ,
[
{ { :aggregate , aggregate . name , subquery } ,
relationship_path_to_relationships ( resource , aggregate . relationship_path ) }
] ,
nil
) do
{ :ok , new_query } ->
{ :ok ,
{ new_query ,
get_binding ( resource , aggregate . relationship_path , new_query , :aggregate ) } }
{ :error , error } ->
{ :error , error }
end
2021-05-07 19:09:49 +12:00
end
2020-07-23 17:13:47 +12:00
binding ->
2021-05-07 19:09:49 +12:00
{ :ok , { query , binding } }
2020-07-23 17:13:47 +12:00
end
2021-05-07 19:09:49 +12:00
case query_and_binding do
{ :ok , { query , binding } } ->
query_with_aggregate_binding =
put_in (
query . __ash_bindings__ . aggregates ,
Map . put ( query . __ash_bindings__ . aggregates , aggregate . name , binding )
)
2021-06-06 10:13:20 +12:00
query_with_aggregate_defs =
put_in (
query_with_aggregate_binding . __ash_bindings__ . aggregate_defs ,
Map . put (
query_with_aggregate_binding . __ash_bindings__ . aggregate_defs ,
aggregate . name ,
aggregate
)
)
2021-05-07 19:09:49 +12:00
new_query =
2021-06-06 10:13:20 +12:00
query_with_aggregate_defs
2021-05-07 19:09:49 +12:00
|> add_aggregate_to_subquery ( resource , aggregate , binding )
2021-06-04 17:48:35 +12:00
|> select_aggregate ( resource , aggregate , add_base? )
2020-07-23 17:13:47 +12:00
2021-05-07 19:09:49 +12:00
{ :ok , new_query }
2020-07-23 17:13:47 +12:00
2021-05-07 19:09:49 +12:00
{ :error , error } ->
{ :error , error }
end
2020-07-23 17:13:47 +12:00
end
2021-06-04 17:48:35 +12:00
@impl true
def add_calculation ( query , calculation , expression , resource ) do
query = default_bindings ( query , resource )
2020-07-23 17:13:47 +12:00
query =
if query . select do
query
else
from ( row in query ,
select : row ,
2021-06-04 17:48:35 +12:00
select_merge : %{ aggregates : %{ } , calculations : %{ } }
2020-07-23 17:13:47 +12:00
)
end
2021-06-04 17:48:35 +12:00
{ params , expr } =
do_filter_to_expr (
expression ,
query . __ash_bindings__ ,
query . select . params
)
{ :ok ,
query
|> Map . update! ( :select , & add_to_calculation_select ( &1 , expr , List . wrap ( params ) , calculation ) ) }
end
defp select_aggregate ( query , resource , aggregate , add_base? ) do
binding = get_binding ( resource , aggregate . relationship_path , query , :aggregate )
query =
if query . select do
query
else
if add_base? do
from ( row in query ,
select : row ,
select_merge : %{ aggregates : %{ } , calculations : %{ } }
)
else
from ( row in query , select : row )
end
end
%{ query | select : add_to_aggregate_select ( query . select , binding , aggregate ) }
end
defp add_to_calculation_select (
%{
expr :
{ :merge , _ ,
[
first ,
{ :%{} , _ ,
[ { :aggregates , { :%{} , [ ] , agg_fields } } , { :calculations , { :%{} , [ ] , fields } } ] }
] }
} = select ,
expr ,
params ,
%{ load : nil } = calculation
) do
field =
{ :type , [ ] ,
[
expr ,
2021-09-14 04:58:23 +12:00
parameterized_type ( calculation . type , [ ] )
2021-06-04 17:48:35 +12:00
] }
name =
if calculation . sequence == 0 do
calculation . name
else
String . to_existing_atom ( " #{ calculation . sequence } " )
end
new_fields = [
{ name , field }
| fields
]
%{
select
| expr :
{ :merge , [ ] ,
[
first ,
{ :%{} , [ ] ,
[ { :aggregates , { :%{} , [ ] , agg_fields } } , { :calculations , { :%{} , [ ] , new_fields } } ] }
] } ,
params : params
}
2020-07-23 17:13:47 +12:00
end
2021-06-04 17:48:35 +12:00
defp add_to_calculation_select (
%{ expr : select_expr } = select ,
expr ,
params ,
%{ load : load_as } = calculation
) do
field =
{ :type , [ ] ,
[
expr ,
2021-09-14 04:58:23 +12:00
parameterized_type ( calculation . type , [ ] )
2021-06-04 17:48:35 +12:00
] }
load_as =
if calculation . sequence == 0 do
load_as
else
" #{ load_as } _ #{ calculation . sequence } "
end
%{
select
| expr : { :merge , [ ] , [ select_expr , { :%{} , [ ] , [ { load_as , field } ] } ] } ,
params : params
}
end
2021-09-14 04:58:23 +12:00
defp parameterized_type ( { :array , type } , constraints ) do
{ :array , parameterized_type ( type , constraints [ :items ] || [ ] ) }
end
2021-12-18 07:25:14 +13:00
defp parameterized_type ( Ash.Type.CiString , constraints ) do
parameterized_type ( Ash.Type.CiStringWrapper , constraints )
end
2021-09-14 04:58:23 +12:00
defp parameterized_type ( type , constraints ) do
if Ash.Type . ash_type? ( type ) do
2021-11-01 21:41:28 +13:00
parameterized_type ( Ash.Type . ecto_type ( type ) , constraints )
2021-09-14 04:58:23 +12:00
else
2021-11-01 21:41:28 +13:00
if is_atom ( type ) && :erlang . function_exported ( type , :type , 1 ) do
{ :parameterized , type , constraints }
2021-09-14 04:58:23 +12:00
else
type
end
end
end
2021-06-04 17:48:35 +12:00
defp add_to_aggregate_select (
%{
expr :
{ :merge , _ ,
[
first ,
{ :%{} , _ ,
[ { :aggregates , { :%{} , [ ] , fields } } , { :calculations , { :%{} , [ ] , calc_fields } } ] }
] }
} = select ,
2020-07-23 17:13:47 +12:00
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 ,
2021-09-14 04:58:23 +12:00
parameterized_type ( aggregate . type , [ ] )
2020-07-23 17:13:47 +12:00
] }
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 ,
2021-09-14 04:58:23 +12:00
parameterized_type ( aggregate . type , [ ] )
2021-04-27 08:45:47 +12:00
] }
2020-07-23 17:13:47 +12:00
] }
end
new_fields = [
{ aggregate . name , field_with_default }
| fields
]
2021-06-04 17:48:35 +12:00
%{
select
| expr :
{ :merge , [ ] ,
[
first ,
{ :%{} , [ ] ,
[ { :aggregates , { :%{} , [ ] , new_fields } } , { :calculations , { :%{} , [ ] , calc_fields } } ] }
] }
}
2020-07-23 17:13:47 +12:00
end
2021-06-04 17:48:35 +12:00
defp add_to_aggregate_select (
2020-08-09 08:19:18 +12:00
%{ 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 ,
2021-09-14 04:58:23 +12:00
parameterized_type ( aggregate . type , [ ] )
2020-08-09 08:19:18 +12:00
] }
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 ,
2021-09-14 04:58:23 +12:00
parameterized_type ( aggregate . type , [ ] )
2021-04-27 08:45:47 +12:00
] }
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
2021-05-13 05:17:26 +12:00
defp aggregate_subquery ( %{ type : :many_to_many } = relationship , aggregate , root_query ) do
2021-07-03 04:41:44 +12:00
destination =
case maybe_get_resource_query ( relationship . destination , relationship , root_query ) do
{ :ok , query } ->
query
_ ->
relationship . destination
end
join_relationship =
Ash.Resource.Info . relationship ( relationship . source , relationship . join_relationship )
through =
case maybe_get_resource_query ( relationship . through , join_relationship , root_query ) do
{ :ok , query } ->
query
_ ->
relationship . through
end
2021-05-13 05:17:26 +12:00
query =
2021-07-03 04:41:44 +12:00
from ( destination in destination ,
join : through in ^ through ,
2021-05-13 05:17:26 +12:00
on :
field ( through , ^ relationship . destination_field_on_join_table ) ==
field ( destination , ^ relationship . destination_field ) ,
group_by : field ( through , ^ relationship . source_field_on_join_table ) ,
select : %{ __source_field : field ( through , ^ relationship . source_field_on_join_table ) }
)
query_tenant = aggregate . query && aggregate . query . tenant
root_tenant = root_query . prefix
2021-07-03 15:43:30 +12:00
if Ash.Resource.Info . multitenancy_strategy ( relationship . destination ) &&
( root_tenant ||
query_tenant ) do
2021-05-13 05:17:26 +12:00
Ecto.Query . put_query_prefix ( query , query_tenant || root_tenant )
else
2021-07-12 18:43:39 +12:00
%{ query | prefix : repo ( relationship . destination ) . config ( ) [ :default_prefix ] || " public " }
2021-05-13 05:17:26 +12:00
end
end
defp aggregate_subquery ( relationship , aggregate , root_query ) do
2021-07-03 04:41:44 +12:00
destination =
case maybe_get_resource_query ( relationship . destination , relationship , root_query ) do
{ :ok , query } ->
query
_ ->
relationship . destination
end
2020-10-29 15:26:45 +13:00
query =
2021-07-03 04:41:44 +12:00
from ( row in destination ,
2020-10-29 15:26:45 +13:00
group_by : ^ relationship . destination_field ,
select : field ( row , ^ relationship . destination_field )
)
2021-05-13 05:17:26 +12:00
query_tenant = aggregate . query && aggregate . query . tenant
root_tenant = root_query . prefix
2021-07-03 15:43:30 +12:00
if Ash.Resource.Info . multitenancy_strategy ( relationship . destination ) &&
( root_tenant ||
query_tenant ) do
2021-05-13 05:17:26 +12:00
Ecto.Query . put_query_prefix ( query , query_tenant || root_tenant )
2020-10-29 15:26:45 +13:00
else
2021-07-12 18:43:39 +12:00
%{
query
| prefix : repo ( relationship . destination ) . config ( ) [ :default_prefix ] || " public "
}
2020-10-29 15:26:45 +13:00
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-07-02 07:28:27 +12:00
defp add_subquery_aggregate_select ( query , %{ kind : :first } = aggregate , _resource ) do
2021-04-27 08:45:47 +12:00
query = default_bindings ( query , aggregate . resource )
key = aggregate . field
2021-09-14 04:58:23 +12:00
type = parameterized_type ( aggregate . type , [ ] )
2021-04-27 08:45:47 +12:00
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 "
] ++
2021-10-27 10:01:57 +13:00
close_paren ( sort_expr ) }
2021-04-27 08:45:47 +12:00
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 ,
2021-06-04 17:48:35 +12:00
query . __ash_bindings__ ,
2021-04-27 08:45:47 +12:00
query . select . params
)
{ params , { :filter , [ ] , [ field , expr ] } }
else
{ [ ] , field }
end
2021-07-02 07:28:27 +12:00
value =
{ :fragment , [ ] ,
[
raw : " ( " ,
expr : filtered ,
raw : " )[1] "
] }
with_default =
if aggregate . default_value do
{ :coalesce , [ ] , [ value , { :type , [ ] , [ aggregate . default_value , type ] } ] }
2021-04-27 08:45:47 +12:00
else
2021-07-02 07:28:27 +12:00
value
2021-04-27 08:45:47 +12:00
end
2021-07-02 07:28:27 +12:00
casted =
{ :type , [ ] ,
[
with_default ,
type
] }
2021-04-27 08:45:47 +12:00
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
2021-09-14 04:58:23 +12:00
type = parameterized_type ( aggregate . type , [ ] )
2020-12-29 13:26:04 +13:00
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
] ++
2021-10-27 10:01:57 +13:00
close_paren ( sort_expr ) }
2020-12-29 13:26:04 +13:00
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 ,
2021-06-04 17:48:35 +12:00
query . __ash_bindings__ ,
2020-12-29 13:26:04 +13:00
query . select . params
)
{ params , { :filter , [ ] , [ field , expr ] } }
else
{ [ ] , field }
end
2021-07-02 07:28:27 +12:00
with_default =
if aggregate . default_value do
{ :coalesce , [ ] , [ filtered , { :type , [ ] , [ aggregate . default_value , type ] } ] }
else
filtered
end
cast = { :type , [ ] , [ with_default , { :array , type } ] }
2020-12-29 13:26:04 +13:00
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 ) )
2021-09-14 04:58:23 +12:00
type = parameterized_type ( aggregate . type , [ ] )
2020-07-23 17:13:47 +12:00
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 ,
2021-06-04 17:48:35 +12:00
query . __ash_bindings__ ,
2020-07-23 17:13:47 +12:00
query . select . params
)
{ params , { :filter , [ ] , [ field , expr ] } }
else
{ [ ] , field }
end
2021-07-02 07:28:27 +12:00
with_default =
if aggregate . default_value do
{ :coalesce , [ ] , [ filtered , { :type , [ ] , [ aggregate . default_value , type ] } ] }
else
filtered
end
cast = { :type , [ ] , [ with_default , type ] }
2020-07-23 17:13:47 +12:00
new_expr = { :merge , [ ] , [ query . select . expr , { :%{} , [ ] , [ { aggregate . name , cast } ] } ] }
%{ query | select : %{ query . select | expr : new_expr , params : params } }
end
2021-10-27 10:01:57 +13:00
defp close_paren ( list ) do
2021-09-14 04:58:23 +12:00
count = length ( list )
2021-10-27 10:01:57 +13:00
case List . last ( list ) do
{ :raw , _ } ->
List . update_at ( list , count - 1 , fn { :raw , str } ->
{ :raw , str <> " ) " }
end )
_ ->
list ++ [ { :raw , " ) " } ]
end
2021-09-14 04:58:23 +12:00
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
2021-05-09 15:25:28 +12:00
defp join_all_relationships ( query , relationship_paths , filter , path \\ [ ] , source \\ nil ) do
2020-09-02 16:01:34 +12:00
query = default_bindings ( query , source )
2020-06-14 19:04:18 +12:00
2021-05-07 19:09:49 +12:00
Enum . reduce_while ( relationship_paths , { :ok , query } , fn
{ _join_type , [ ] } , { :ok , query } ->
{ :cont , { :ok , query } }
2020-06-14 19:04:18 +12:00
2021-05-07 19:09:49 +12:00
{ join_type , [ relationship | rest_rels ] } , { :ok , query } ->
2020-09-02 16:01:34 +12:00
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
2021-05-07 19:09:49 +12:00
{ :cont , { :ok , query } }
2020-09-02 16:01:34 +12:00
else
2021-05-07 19:09:49 +12:00
case join_relationship (
query ,
relationship ,
Enum . map ( path , & &1 . name ) ,
current_join_type ,
2021-05-09 15:25:28 +12:00
source ,
filter
2021-05-07 19:09:49 +12:00
) do
{ :ok , joined_query } ->
joined_query_with_distinct = add_distinct ( relationship , join_type , joined_query )
case join_all_relationships (
joined_query_with_distinct ,
[ { join_type , rest_rels } ] ,
2021-05-09 15:25:28 +12:00
filter ,
2021-05-07 19:09:49 +12:00
current_path ,
source
) do
{ :ok , query } ->
{ :cont , { :ok , query } }
{ :error , error } ->
{ :halt , { :error , error } }
end
2020-09-02 16:01:34 +12:00
2021-05-07 19:09:49 +12:00
{ :error , error } ->
{ :halt , { :error , error } }
end
2020-09-02 16:01:34 +12:00
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
2021-05-09 15:25:28 +12:00
defp join_relationship ( query , relationship , path , join_type , source , filter ) do
2020-09-02 16:01:34 +12:00
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 ->
2021-05-09 15:25:28 +12:00
do_join_relationship ( query , relationship , path , join_type , source , filter )
2020-06-14 19:04:18 +12:00
_ ->
2021-05-07 19:09:49 +12:00
{ :ok , query }
2020-06-14 19:04:18 +12:00
end
end
2021-05-09 15:25:28 +12:00
defp do_join_relationship (
query ,
%{ type : :many_to_many } = relationship ,
path ,
kind ,
source ,
filter
) do
2021-04-30 09:31:19 +12:00
join_relationship = Ash.Resource.Info . relationship ( source , relationship . join_relationship )
2020-06-14 19:04:18 +12:00
2021-05-07 19:09:49 +12:00
with { :ok , relationship_through } <-
2021-06-04 17:48:35 +12:00
maybe_get_resource_query ( relationship . through , join_relationship , query ) ,
2021-05-07 19:09:49 +12:00
{ :ok , relationship_destination } <-
2021-06-04 17:48:35 +12:00
maybe_get_resource_query ( relationship . destination , relationship , query ) do
2021-05-23 15:56:52 +12:00
relationship_through =
relationship_through
|> Ecto.Queryable . to_query ( )
|> set_join_prefix ( query , relationship . through )
relationship_destination =
relationship_destination
|> Ecto.Queryable . to_query ( )
|> set_join_prefix ( query , relationship . destination )
2020-09-02 16:01:34 +12:00
2021-07-06 06:12:21 +12:00
binding_kind =
case kind do
{ :aggregate , _ , _ } ->
:left
other ->
other
end
2021-05-07 19:09:49 +12:00
current_binding =
Enum . find_value ( query . __ash_bindings__ . bindings , 0 , fn { binding , data } ->
2021-07-06 06:12:21 +12:00
if data . type == binding_kind && data . path == Enum . reverse ( path ) do
2021-05-07 19:09:49 +12:00
binding
end
end )
2020-06-14 19:04:18 +12:00
2021-06-04 17:48:35 +12:00
used_calculations =
Ash.Filter . used_calculations (
filter ,
relationship . destination ,
path ++ [ relationship . name ]
)
used_aggregates = used_aggregates ( filter , relationship , used_calculations , path )
2021-05-09 15:25:28 +12:00
Enum . reduce_while ( used_aggregates , { :ok , relationship_destination } , fn agg , { :ok , query } ->
agg = %{ agg | load : agg . name }
2021-06-04 17:48:35 +12:00
case add_aggregate ( query , agg , relationship . destination , false ) do
2021-05-09 15:25:28 +12:00
{ :ok , query } ->
{ :cont , { :ok , query } }
{ :error , error } ->
{ :halt , { :error , error } }
end
end )
|> case do
{ :ok , relationship_destination } ->
2021-06-04 17:48:35 +12:00
relationship_destination =
case used_aggregates do
[ ] ->
relationship_destination
_ ->
subquery ( relationship_destination )
end
2021-05-09 15:25:28 +12:00
new_query =
case kind do
{ :aggregate , _ , subquery } ->
2021-07-06 06:12:21 +12:00
{ subquery , alias_name } =
2021-07-28 03:13:48 +12:00
agg_subquery_for_lateral_join ( current_binding , query , subquery , relationship )
2021-07-06 06:12:21 +12:00
2021-05-09 15:25:28 +12:00
from ( [ { row , current_binding } ] in query ,
2021-07-06 06:12:21 +12:00
left_lateral_join : through in ^ subquery
2021-05-07 19:09:49 +12:00
)
2021-07-06 06:12:21 +12:00
|> Map . update! ( :aliases , & Map . put ( &1 , alias_name , current_binding ) )
2020-07-23 17:13:47 +12:00
2021-05-09 15:25:28 +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 )
)
2020-06-14 19:04:18 +12:00
2021-05-09 15:25:28 +12:00
_ ->
from ( [ { row , current_binding } ] in query ,
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
2021-05-09 15:25:28 +12:00
join_path =
Enum . reverse ( [
String . to_existing_atom ( to_string ( relationship . name ) <> " _join_assoc " ) | path
] )
2020-06-14 19:04:18 +12:00
2021-05-09 15:25:28 +12:00
full_path = Enum . reverse ( [ relationship . name | path ] )
2020-09-02 16:01:34 +12:00
2021-05-09 15:25:28 +12:00
binding_data =
case kind do
{ :aggregate , name , _agg } ->
%{ type : :aggregate , name : name , path : full_path , source : source }
2020-06-14 19:04:18 +12:00
2021-05-09 15:25:28 +12:00
_ ->
%{ type : kind , path : full_path , source : source }
end
2021-05-13 05:17:26 +12:00
case kind do
{ :aggregate , _ , _subquery } ->
{ :ok ,
new_query
|> add_binding ( binding_data ) }
_ ->
{ :ok ,
new_query
|> add_binding ( %{ path : join_path , type : :left , source : source } )
|> add_binding ( binding_data ) }
end
2021-05-09 15:25:28 +12:00
{ :error , error } ->
{ :error , error }
end
2021-05-07 19:09:49 +12:00
end
2020-06-14 19:04:18 +12:00
end
2021-05-09 15:25:28 +12:00
defp do_join_relationship ( query , relationship , path , kind , source , filter ) do
2021-06-04 17:48:35 +12:00
case maybe_get_resource_query ( relationship . destination , relationship , query ) do
2021-05-07 19:09:49 +12:00
{ :error , error } ->
{ :error , error }
2020-09-02 16:01:34 +12:00
2021-05-07 19:09:49 +12:00
{ :ok , relationship_destination } ->
2021-05-23 15:56:52 +12:00
relationship_destination =
relationship_destination
|> Ecto.Queryable . to_query ( )
|> set_join_prefix ( query , relationship . destination )
2020-06-14 19:04:18 +12:00
2021-07-06 06:12:21 +12:00
binding_kind =
case kind do
{ :aggregate , _ , _ } ->
:left
other ->
other
end
2021-05-07 19:09:49 +12:00
current_binding =
Enum . find_value ( query . __ash_bindings__ . bindings , 0 , fn { binding , data } ->
2021-07-06 06:12:21 +12:00
if data . type == binding_kind && data . path == Enum . reverse ( path ) do
2021-05-07 19:09:49 +12:00
binding
end
end )
2021-06-04 17:48:35 +12:00
used_calculations =
Ash.Filter . used_calculations (
filter ,
relationship . destination ,
path ++ [ relationship . name ]
)
used_aggregates = used_aggregates ( filter , relationship , used_calculations , path )
2020-07-23 17:13:47 +12:00
2021-05-09 15:25:28 +12:00
Enum . reduce_while ( used_aggregates , { :ok , relationship_destination } , fn agg ,
{ :ok , query } ->
agg = %{ agg | load : agg . name }
2020-07-23 17:13:47 +12:00
2021-06-04 17:48:35 +12:00
case add_aggregate ( query , agg , relationship . destination , false ) do
2021-05-09 15:25:28 +12:00
{ :ok , query } ->
{ :cont , { :ok , query } }
2020-09-02 16:01:34 +12:00
2021-05-09 15:25:28 +12:00
{ :error , error } ->
{ :halt , { :error , error } }
2021-05-07 19:09:49 +12:00
end
2021-05-09 15:25:28 +12:00
end )
|> case do
{ :ok , relationship_destination } ->
relationship_destination =
case used_aggregates do
[ ] ->
relationship_destination
_ ->
2021-06-04 17:48:35 +12:00
subquery ( relationship_destination )
2021-05-09 15:25:28 +12:00
end
2020-06-14 19:04:18 +12:00
2021-07-06 06:12:21 +12:00
new_query =
2021-05-09 15:25:28 +12:00
case kind do
{ :aggregate , _ , subquery } ->
2021-07-06 06:12:21 +12:00
{ subquery , alias_name } =
2021-07-28 03:13:48 +12:00
agg_subquery_for_lateral_join ( current_binding , query , subquery , relationship )
2021-05-09 15:25:28 +12:00
2021-07-06 06:12:21 +12:00
from ( [ { row , current_binding } ] in query ,
left_lateral_join : destination in ^ subquery ,
on :
field ( row , ^ relationship . source_field ) ==
field ( destination , ^ relationship . destination_field )
)
|> Map . update! ( :aliases , & Map . put ( &1 , alias_name , current_binding ) )
2021-07-02 17:01:56 +12:00
2021-07-06 06:12:21 +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 )
)
2021-05-09 15:25:28 +12:00
_ ->
2021-07-06 06:12:21 +12:00
from ( [ { row , current_binding } ] in query ,
left_join : destination in ^ relationship_destination ,
on :
field ( row , ^ relationship . source_field ) ==
field ( destination , ^ relationship . destination_field )
)
2021-05-09 15:25:28 +12:00
end
2020-09-02 16:01:34 +12:00
2021-05-09 15:25:28 +12:00
full_path = Enum . reverse ( [ relationship . name | path ] )
2020-06-14 19:04:18 +12:00
2021-05-09 15:25:28 +12:00
binding_data =
case kind do
{ :aggregate , name , _agg } ->
%{ type : :aggregate , name : name , path : full_path , source : source }
_ ->
%{ type : kind , path : full_path , source : source }
end
{ :ok ,
new_query
2021-07-06 06:12:21 +12:00
|> add_binding ( binding_data ) }
2021-05-09 15:25:28 +12:00
{ :error , error } ->
{ :error , error }
end
2021-05-07 19:09:49 +12:00
end
2020-06-14 19:04:18 +12:00
end
2021-07-28 03:13:48 +12:00
defp agg_subquery_for_lateral_join ( current_binding , query , subquery , relationship ) do
2021-07-06 06:12:21 +12:00
alias_name = @atoms [ current_binding ]
inner_sub = from ( destination in subquery , [ ] )
{ dest_binding , dest_field } =
case relationship . type do
:many_to_many ->
{ 1 , relationship . source_field_on_join_table }
_ ->
{ 0 , relationship . destination_field }
end
inner_sub_with_where =
Map . put ( inner_sub , :wheres , [
% Ecto.Query.BooleanExpr {
expr :
{ :== , [ ] ,
[
{ { :. , [ ] , [ { :& , [ ] , [ dest_binding ] } , dest_field ] } , [ ] , [ ] } ,
{ { :. , [ ] , [ { :parent_as , [ ] , [ alias_name ] } , relationship . source_field ] } , [ ] , [ ] }
] } ,
op : :and
}
] )
subquery =
from (
sub in subquery ( inner_sub_with_where ) ,
select : field ( sub , ^ dest_field )
)
2021-07-28 03:13:48 +12:00
|> set_join_prefix ( query , relationship . destination )
2021-07-06 06:12:21 +12:00
{ subquery , alias_name }
end
2021-06-04 17:48:35 +12:00
defp used_aggregates ( filter , relationship , used_calculations , path ) do
Ash.Filter . used_aggregates ( filter , path ++ [ relationship . name ] ) ++
Enum . flat_map (
used_calculations ,
fn calculation ->
case Ash.Filter . hydrate_refs (
calculation . module . expression ( calculation . opts , calculation . context ) ,
%{
resource : relationship . destination ,
aggregates : %{ } ,
calculations : %{ } ,
public? : false
}
) do
{ :ok , hydrated } ->
Ash.Filter . used_aggregates ( hydrated )
_ ->
[ ]
end
end
)
end
2021-05-23 15:56:52 +12:00
defp set_join_prefix ( join_query , query , resource ) do
if Ash.Resource.Info . multitenancy_strategy ( resource ) == :context do
2021-07-12 18:43:39 +12:00
%{ join_query | prefix : query . prefix || " public " }
2021-05-23 15:56:52 +12:00
else
2021-07-12 18:43:39 +12:00
%{
join_query
| prefix : repo ( resource ) . config ( ) [ :default_prefix ] || " public "
}
2021-05-23 15:56:52 +12:00
end
end
2020-06-14 19:04:18 +12:00
defp add_filter_expression ( query , filter ) do
2020-09-20 10:08:09 +12:00
wheres =
filter
|> split_and_statements ( )
|> Enum . map ( fn filter ->
2021-06-04 17:48:35 +12:00
{ params , expr } = filter_to_expr ( filter , query . __ash_bindings__ , [ ] )
2020-09-20 10:08:09 +12:00
% 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
2021-06-04 17:48:35 +12:00
defp do_filter_to_expr ( expr , bindings , params , embedded? \\ false , type \\ nil )
2021-01-27 09:07:26 +13:00
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
2021-09-14 04:58:23 +12:00
) do
{ params , arg1 } = do_filter_to_expr ( arg1 , bindings , params , false )
{ params , arg2 } = do_filter_to_expr ( arg2 , bindings , params , pred_embedded? || embedded? )
2021-02-25 07:59:49 +13:00
2021-09-14 04:58:23 +12:00
{ params , { :type , [ ] , [ arg1 , parameterized_type ( arg2 , [ ] ) ] } }
2021-02-25 07:59:49 +13:00
end
2021-01-27 09:07:26 +13:00
defp do_filter_to_expr (
2021-09-14 04:58:23 +12:00
% Type { arguments : [ arg1 , arg2 , constraints ] , 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-09-14 04:58:23 +12:00
{ params , { :type , [ ] , [ arg1 , parameterized_type ( arg2 , constraints ) ] } }
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-06-04 17:48:35 +12:00
arguments =
case arguments do
[ { :raw , _ } | _ ] ->
arguments
arguments ->
[ { :raw , " " } | arguments ]
end
arguments =
case List . last ( arguments ) do
nil ->
arguments
{ :raw , _ } ->
arguments
_ ->
arguments ++ [ { :raw , " " } ]
end
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 } ] }
2021-06-04 17:48:35 +12:00
{ :casted_expr , expr } , { params , fragment_data } ->
{ params , fragment_data ++ [ { :expr , expr } ] }
2021-01-22 09:32:26 +13:00
{ :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( " ,
2021-07-28 11:15:28 +12:00
expr : left ,
2021-01-24 16:45:15 +13:00
raw : " ::citext, " ,
2021-07-28 11:15:28 +12:00
expr : right ,
2021-01-24 16:45:15 +13:00
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( " ,
2021-07-28 11:15:28 +12:00
expr : left ,
2021-01-24 16:45:15 +13:00
raw : " , " ,
2021-07-28 11:15:28 +12:00
expr : right ,
2021-01-24 16:45:15 +13:00
raw : " ) > 0 "
]
} ,
bindings ,
params ,
embedded? ,
type
)
end
2021-06-04 17:48:35 +12:00
defp do_filter_to_expr (
% If { arguments : [ condition , when_true , when_false ] , embedded? : pred_embedded? } ,
bindings ,
params ,
embedded? ,
type
) do
[ condition_type , when_true_type , when_false_type ] =
2021-11-14 08:48:14 +13:00
case determine_types ( If , [ condition , when_true , when_false ] ) do
[ condition_type , when_true ] ->
[ condition_type , when_true , nil ]
[ condition_type , when_true , when_false ] ->
[ condition_type , when_true , when_false ]
end
2021-06-04 17:48:35 +12:00
{ params , condition } =
do_filter_to_expr ( condition , bindings , params , pred_embedded? || embedded? , condition_type )
{ params , when_true } =
do_filter_to_expr ( when_true , bindings , params , pred_embedded? || embedded? , when_true_type )
{ params , when_false } =
do_filter_to_expr (
when_false ,
bindings ,
params ,
pred_embedded? || embedded? ,
when_false_type
)
do_filter_to_expr (
% Fragment {
embedded? : pred_embedded? ,
arguments : [
raw : " CASE WHEN " ,
casted_expr : condition ,
raw : " THEN " ,
casted_expr : when_true ,
raw : " ELSE " ,
casted_expr : when_false ,
raw : " END "
]
} ,
bindings ,
params ,
embedded? ,
type
)
end
defp do_filter_to_expr (
% mod {
__predicate__? : _ ,
left : left ,
right : right ,
embedded? : pred_embedded? ,
operator : :<>
} ,
bindings ,
params ,
embedded? ,
type
) do
[ left_type , right_type ] = determine_types ( mod , [ left , right ] )
{ params , left_expr } =
do_filter_to_expr ( left , bindings , params , pred_embedded? || embedded? , left_type )
{ params , right_expr } =
do_filter_to_expr ( right , bindings , params , pred_embedded? || embedded? , right_type )
do_filter_to_expr (
% Fragment {
embedded? : pred_embedded? ,
arguments : [
casted_expr : left_expr ,
raw : " || " ,
casted_expr : right_expr
]
} ,
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-06-04 17:48:35 +12:00
defp do_filter_to_expr (
% Ref {
attribute : % Ash.Query.Calculation { } = calculation ,
relationship_path : [ ] ,
resource : resource
} ,
bindings ,
params ,
embedded? ,
type
) do
calculation = %{ calculation | load : calculation . name }
case Ash.Filter . hydrate_refs (
calculation . module . expression ( calculation . opts , calculation . context ) ,
%{
resource : resource ,
aggregates : %{ } ,
calculations : %{ } ,
public? : false
}
) do
{ :ok , expression } ->
do_filter_to_expr (
expression ,
bindings ,
params ,
embedded? ,
type
)
{ :error , _error } ->
{ params , nil }
end
end
defp do_filter_to_expr (
% Ref {
attribute : % Ash.Query.Calculation { } = calculation ,
relationship_path : relationship_path
} = ref ,
bindings ,
params ,
embedded? ,
type
) do
binding_to_replace =
Enum . find_value ( bindings . bindings , fn { i , binding } ->
if binding . path == relationship_path do
i
end
end )
temp_bindings =
bindings . bindings
|> Map . delete ( 0 )
|> Map . update! ( binding_to_replace , & Map . merge ( &1 , %{ path : [ ] , type : :root } ) )
case Ash.Filter . hydrate_refs (
calculation . module . expression ( calculation . opts , calculation . context ) ,
%{
resource : ref . resource ,
aggregates : %{ } ,
calculations : %{ } ,
public? : false
}
) do
{ :ok , hydrated } ->
hydrated
|> Ash.Filter . update_aggregates ( fn aggregate , _ ->
%{ aggregate | relationship_path : [ ] }
end )
|> do_filter_to_expr (
%{ bindings | bindings : temp_bindings } ,
params ,
embedded? ,
type
)
_ ->
{ params , nil }
end
end
2021-07-02 07:28:27 +12:00
defp do_filter_to_expr (
% Ref { attribute : % Ash.Query.Aggregate { } = aggregate } = ref ,
bindings ,
params ,
_embedded? ,
_type
) do
expr = { { :. , [ ] , [ { :& , [ ] , [ ref_binding ( ref , bindings ) ] } , aggregate . name ] } , [ ] , [ ] }
2021-09-14 04:58:23 +12:00
type = parameterized_type ( aggregate . type , [ ] )
2021-07-02 07:28:27 +12:00
type =
if aggregate . kind == :list do
{ :array , type }
else
type
end
with_default =
if aggregate . default_value do
{ :coalesce , [ ] , [ expr , { :type , [ ] , [ aggregate . default_value , type ] } ] }
else
expr
end
{ params , with_default }
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
2021-07-28 03:13:48 +12:00
{ params , string } = do_filter_to_expr ( string , bindings , params , embedded? )
2021-01-27 09:07:26 +13:00
do_filter_to_expr (
2021-01-24 16:45:15 +13:00
% Fragment {
embedded? : embedded? ,
arguments : [
raw : " " ,
2021-06-04 17:48:35 +12:00
casted_expr : string ,
2021-01-24 16:45:15 +13:00
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-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
2021-05-07 17:49:04 +12:00
defp last_ditch_cast ( value , _ ) when is_boolean ( value ) do
value
end
2021-04-28 05:03:02 +12:00
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
2021-06-04 17:48:35 +12:00
Code . ensure_compiled ( mod )
cond do
:erlang . function_exported ( mod , :types , 0 ) ->
mod . types ( )
:erlang . function_exported ( mod , :args , 0 ) ->
mod . args ( )
true ->
[ :any ]
end
2021-04-28 04:09:00 +12:00
|> 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
2021-09-14 04:58:23 +12:00
defp fill_in_known_type (
{ vague_type , % Ref { attribute : %{ type : type , constraints : constraints } } } = ref
)
2021-04-28 04:09:00 +12:00
when vague_type in [ :any , :same ] do
if Ash.Type . ash_type? ( type ) do
2021-11-01 21:41:28 +13:00
type = type |> Ash.Type . ecto_type ( ) |> parameterized_type ( constraints ) |> array_to_in ( )
2021-09-14 04:58:23 +12:00
{ type , ref }
2021-04-28 04:09:00 +12:00
else
2021-09-14 04:58:23 +12:00
type =
if is_atom ( type ) && :erlang . function_exported ( type , :type , 1 ) do
{ :parameterized , type , [ ] } |> array_to_in ( )
else
type |> array_to_in ( )
end
{ type , ref }
2021-04-28 04:09:00 +12:00
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 ) }
2021-09-14 04:58:23 +12:00
defp array_to_in ( { :parameterized , type , constraints } ) ,
do : { :parameterized , array_to_in ( type ) , constraints }
2021-04-28 04:09:00 +12:00
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
2021-05-09 15:25:28 +12:00
defp ref_binding (
%{ attribute : % Ash.Query.Aggregate { } = aggregate , relationship_path : [ ] } ,
bindings
) do
2021-06-04 17:48:35 +12:00
Enum . find_value ( bindings . bindings , fn { binding , data } ->
2021-05-09 15:25:28 +12:00
data . path == aggregate . relationship_path && data . type == :aggregate && binding
2021-06-04 17:48:35 +12:00
end ) ||
Enum . find_value ( bindings . bindings , fn { binding , data } ->
data . path == aggregate . relationship_path && data . type in [ :inner , :left , :root ] && binding
end )
end
defp ref_binding (
%{ attribute : % Ash.Query.Calculation { } } = ref ,
bindings
) do
Enum . find_value ( bindings . bindings , fn { binding , data } ->
data . path == ref . relationship_path && data . type in [ :inner , :left , :root ] && binding
2021-05-09 15:25:28 +12:00
end )
end
2021-01-22 09:32:26 +13:00
2021-05-09 15:25:28 +12:00
defp ref_binding ( %{ attribute : % Ash.Resource.Attribute { } } = ref , bindings ) do
2021-06-04 17:48:35 +12:00
Enum . find_value ( bindings . bindings , fn { binding , data } ->
2021-05-09 15:25:28 +12:00
data . path == ref . relationship_path && data . type in [ :inner , :left , :root ] && binding
end )
end
defp ref_binding ( %{ attribute : % Ash.Query.Aggregate { } } = ref , bindings ) do
2021-06-04 17:48:35 +12:00
Enum . find_value ( bindings . bindings , fn { binding , data } ->
2021-05-09 15:25:28 +12:00
data . path == ref . relationship_path && data . type in [ :inner , :left , :root ] && binding
end )
2020-09-20 10:08:09 +12:00
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-06-04 17:48:35 +12:00
defp maybe_get_resource_query ( resource , relationship , root_query ) do
2021-04-30 09:31:19 +12:00
resource
|> Ash.Query . new ( )
2021-06-04 17:48:35 +12:00
|> Map . put ( :context , root_query . __ash_bindings__ . context )
2021-04-30 09:31:19 +12:00
|> Ash.Query . set_context ( relationship . context )
2021-05-08 03:04:09 +12:00
|> Ash.Query . do_filter ( relationship . filter )
2021-05-07 17:49:04 +12:00
|> Ash.Query . sort ( Map . get ( relationship , :sort ) )
2021-04-30 09:31:19 +12:00
|> case do
2021-05-07 19:09:49 +12:00
%{ valid? : true } = query ->
2021-11-03 10:00:13 +13:00
initial_query = %{ resource_to_query ( resource , nil ) | prefix : Map . get ( root_query , :prefix ) }
Ash.Query . data_layer_query ( query ,
only_validate_filter? : false ,
initial_query : initial_query
)
2021-05-07 19:09:49 +12:00
query ->
{ :error , query }
2020-09-20 10:08:09 +12:00
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