2020-06-14 19:04:18 +12:00
defmodule AshPostgres.DataLayer do
2022-08-19 06:56:36 +12:00
@manage_tenant % Spark.Dsl.Section {
2020-10-29 15:26:45 +13:00
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
2022-08-19 06:56:36 +12:00
@index % Spark.Dsl.Entity {
2021-09-21 08:38:36 +12:00
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 ]
}
2022-08-19 06:56:36 +12:00
@custom_indexes % Spark.Dsl.Section {
2021-09-21 08:38:36 +12:00
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
]
}
2022-08-19 06:56:36 +12:00
@statement % Spark.Dsl.Entity {
2022-07-22 05:34:38 +12:00
name : :statement ,
describe : """
Add a custom statement for migrations .
""" ,
examples : [
"""
statement :pgweb_idx do
up " CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body)); "
down " DROP INDEX pgweb_idx; "
end
"""
] ,
target : AshPostgres.Statement ,
schema : AshPostgres.Statement . schema ( ) ,
args : [ :name ]
}
2022-08-19 06:56:36 +12:00
@custom_statements % Spark.Dsl.Section {
2022-07-22 05:34:38 +12:00
name : :custom_statements ,
describe : """
A section for configuring custom statements to be added to migrations .
Changing custom statements may require manual intervention , because Ash can ' t determine what order they should run
in ( i . e if they depend on table structure that you ' ve added, or vice versa). As such, any `down` statements we run
for custom statements happen first , and any ` up ` statements happen last .
Additionally , when changing a custom statement , we must make some assumptions , i . e that we should migrate
the old structure down using the previously configured ` down ` and recreate it .
This may not be desired , and so what you may end up doing is simply modifying the old migration and deleting whatever was
generated by the migration generator . As always : read your migrations after generating them!
""" ,
examples : [
"""
custom_statements do
# the name is used to detect if you remove or modify the statement
custom_statement :pgweb_idx do
up " CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body)); "
down " DROP INDEX pgweb_idx; "
end
end
"""
] ,
entities : [
@statement
]
}
2022-08-19 06:56:36 +12:00
@reference % Spark.Dsl.Entity {
2021-04-01 19:19:30 +13:00
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 ( )
}
2022-08-19 06:56:36 +12:00
@references % Spark.Dsl.Section {
2021-04-01 19:19:30 +13:00
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. "
]
]
}
2022-08-19 06:56:36 +12:00
@check_constraint % Spark.Dsl.Entity {
2021-04-20 06:26:41 +12:00
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 ( )
}
2022-08-19 06:56:36 +12:00
@check_constraints % Spark.Dsl.Section {
2021-04-20 06:26:41 +12:00
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 ]
}
2022-08-19 06:56:36 +12:00
@references % Spark.Dsl.Section {
2021-04-20 06:26:41 +12:00
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. "
]
]
}
2022-08-19 06:56:36 +12:00
@postgres % Spark.Dsl.Section {
2020-06-14 19:04:18 +12:00
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 ,
2022-07-22 05:34:38 +12:00
@custom_statements ,
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.
"""
] ,
2022-03-21 13:35:30 +13:00
exclusion_constraint_names : [
type : :any ,
default : [ ] ,
doc : """
A list of exclusion constraint names that could raise errors . Must be in the format ` { :affected_key , " name_of_constraint " } ` or ` { :affected_key , " name_of_constraint " , " custom error message " } `
"""
] ,
2021-04-28 09:16:56 +12:00
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 ,
2022-05-14 09:41:30 +12:00
doc : """
The table to store and read the resource from . Required unless ` polymorphic? ` is true .
If this is changed , the migration generator will not remove the old table .
"""
] ,
schema : [
type : :string ,
doc : """
The schema that the table is located in .
Multitenancy supersedes this , so this acts as the schema in the cases that ` global? : true ` is set .
If this is changed , the migration generator will not remove the old table in the old schema .
"""
2021-01-29 13:42:55 +13:00
] ,
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-12-21 16:19:24 +13:00
alias Ash.Query . { BooleanExpression , Not }
2020-06-14 19:04:18 +12:00
@behaviour Ash.DataLayer
2020-12-27 19:20:12 +13:00
@sections [ @postgres ]
@moduledoc """
2022-03-29 15:30:27 +13:00
A postgres data layer that leverages Ecto ' s postgres capabilities.
2022-08-24 11:56:46 +12:00
< ! -- - ash - hq - hide - start -- > < ! -- - -- >
## DSL Documentation
### Index
#{Spark.Dsl.Extension.doc_index(@sections)}
### Docs
#{Spark.Dsl.Extension.doc(@sections)}
< ! -- - ash - hq - hide - stop -- > < ! -- - -- >
2020-12-27 19:20:12 +13:00
"""
2022-08-19 06:56:36 +12:00
use Spark.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 )
2022-08-24 11:56:46 +12:00
data_layer == other_data_layer and
AshPostgres.DataLayer.Info . repo ( resource ) == AshPostgres.DataLayer.Info . 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
2022-08-24 11:56:46 +12:00
repo = AshPostgres.DataLayer.Info . 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 ->
2022-08-24 11:56:46 +12:00
Ash.DataLayer . data_layer ( resource ) == data_layer &&
AshPostgres.DataLayer.Info . repo ( resource ) == repo
2021-04-30 09:31:19 +12:00
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
2022-09-14 08:27:39 +12:00
def can? ( _ , { :filter_relationship , %{ manual : { module , _ } } } ) do
Spark . implements_behaviour? ( module , AshPostgres.ManualRelationship )
end
def can? ( _ , { :filter_relationship , _ } ) , do : true
def can? ( _ , { :aggregate_relationship , %{ manual : { module , _ } } } ) do
Spark . implements_behaviour? ( module , AshPostgres.ManualRelationship )
end
def can? ( _ , { :aggregate_relationship , _ } ) , do : true
2022-05-14 18:58:04 +12:00
def can? ( _ , :timeout ) , 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
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info . repo ( resource ) . in_transaction? ( )
2020-06-30 16:16:17 +12:00
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
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info . 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
2022-05-14 09:41:30 +12:00
data_layer_query =
if context [ :data_layer ] [ :schema ] do
Ecto.Query . put_query_prefix ( data_layer_query , to_string ( context [ :data_layer ] [ :schema ] ) )
else
data_layer_query
end
2021-06-04 17:48:35 +12:00
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
2022-05-11 14:47:21 +12:00
query = %{ query | windows : Keyword . delete ( query . windows , :order ) }
2022-08-24 11:56:46 +12:00
if AshPostgres.DataLayer.Info . polymorphic? ( resource ) && no_table? ( query ) do
2021-03-22 10:58:47 +13:00
raise_table_error! ( resource , :read )
else
2022-08-24 11:56:46 +12:00
{ :ok , AshPostgres.DataLayer.Info . repo ( resource ) . all ( query , repo_opts ( nil , nil , resource ) ) }
2021-03-22 10:58:47 +13:00
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
2022-05-23 10:30:20 +12:00
defp repo_opts ( timeout , nil , resource ) do
2022-08-24 11:56:46 +12:00
if schema = AshPostgres.DataLayer.Info . schema ( resource ) do
2022-05-23 10:30:20 +12:00
[ prefix : schema ]
2021-07-25 03:28:58 +12:00
else
2022-05-23 10:30:20 +12:00
[ ]
2021-07-25 03:28:58 +12:00
end
2022-05-23 10:30:20 +12:00
|> add_timeout ( timeout )
2021-07-25 03:28:58 +12:00
end
2022-05-23 10:30:20 +12:00
defp repo_opts ( timeout , tenant , resource ) 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
2022-08-24 11:56:46 +12:00
if schema = AshPostgres.DataLayer.Info . schema ( resource ) do
2022-05-14 09:41:30 +12:00
[ prefix : schema ]
else
[ ]
end
2020-10-29 15:26:45 +13:00
end
2022-05-23 10:30:20 +12:00
|> add_timeout ( timeout )
2020-10-29 15:26:45 +13:00
end
2022-05-23 10:30:20 +12:00
defp add_timeout ( opts , timeout ) when not is_nil ( timeout ) do
Keyword . put ( opts , :timeout , timeout )
end
defp add_timeout ( opts , _ ) , do : opts
2020-10-29 15:26:45 +13:00
2020-10-06 18:39:47 +13:00
@impl true
def functions ( resource ) do
2022-08-24 11:56:46 +12:00
config = AshPostgres.DataLayer.Info . repo ( resource ) . config ( )
2020-10-06 18:39:47 +13:00
2022-10-11 05:06:54 +13:00
functions = [ AshPostgres.Functions.Fragment ]
2021-01-22 09:32:26 +13:00
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 ,
2022-09-29 11:01:20 +13:00
fn agg , subquery ->
has_exists? =
Ash.Filter . find ( agg . query && agg . query . filter , fn
% Ash.Query.Exists { } -> true
_ -> false
end )
AshPostgres.Aggregate . add_subquery_aggregate_select (
subquery ,
agg ,
resource ,
has_exists?
)
end
2020-10-18 12:13:51 +13:00
)
2022-08-24 11:56:46 +12:00
{ :ok , AshPostgres.DataLayer.Info . repo ( resource ) . one ( query , repo_opts ( nil , nil , resource ) ) }
2020-10-29 15:26:45 +13:00
end
@impl true
def set_tenant ( _resource , query , tenant ) do
2022-09-07 10:33:17 +12:00
{ :ok , Map . put ( Ecto.Query . put_query_prefix ( query , to_string ( tenant ) ) , :__tenant__ , 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 ,
2022-09-29 11:01:20 +13:00
fn agg , subquery ->
has_exists? =
Ash.Filter . find ( agg . query && agg . query . filter , fn
% Ash.Query.Exists { } -> true
_ -> false
end )
AshPostgres.Aggregate . add_subquery_aggregate_select (
subquery ,
agg ,
destination_resource ,
has_exists?
)
end
2021-05-07 19:09:49 +12:00
)
2020-10-18 12:13:51 +13:00
2022-08-24 11:56:46 +12:00
{ :ok ,
AshPostgres.DataLayer.Info . repo ( source_resource ) . one (
query ,
repo_opts ( nil , nil , 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-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
2022-08-24 11:56:46 +12:00
{ :ok ,
AshPostgres.DataLayer.Info . repo ( source_resource ) . all (
query ,
repo_opts ( nil , nil , 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 ,
2022-08-19 06:56:36 +12:00
[ { source_query , source_attribute , destination_attribute , relationship } ]
2020-10-18 12:13:51 +13:00
) do
2022-08-19 06:56:36 +12:00
source_values = Enum . map ( root_data , & Map . get ( &1 , source_attribute ) )
2021-05-08 03:04:09 +12:00
source_query = Ash.Query . new ( source_query )
2020-08-26 16:28:55 +12:00
subquery =
2022-05-11 14:47:21 +12:00
if query . __ash_bindings__ [ :__order__? ] do
2021-07-20 05:56:36 +12:00
subquery (
from ( destination in query ,
select_merge : %{ __order__ : over ( row_number ( ) , :order ) } ,
where :
2022-08-19 06:56:36 +12:00
field ( destination , ^ destination_attribute ) ==
field ( parent_as ( ^ 0 ) , ^ source_attribute )
2021-07-20 05:56:36 +12:00
)
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 :
2022-08-19 06:56:36 +12:00
field ( destination , ^ destination_attribute ) ==
field ( parent_as ( ^ 0 ) , ^ source_attribute )
2021-07-20 05:56:36 +12:00
)
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 } ->
2022-05-11 14:47:21 +12:00
if query . __ash_bindings__ [ :__order__? ] do
2021-07-20 05:56:36 +12:00
{ :ok ,
from ( source in data_layer_query ,
2022-08-19 06:56:36 +12:00
where : field ( source , ^ source_attribute ) in ^ source_values ,
2021-07-20 05:56:36 +12:00
inner_lateral_join : destination in ^ subquery ,
2022-08-19 06:56:36 +12:00
on : field ( source , ^ source_attribute ) == field ( destination , ^ destination_attribute ) ,
2021-07-20 05:56:36 +12:00
order_by : destination . __order__ ,
select : destination ,
distinct : true
) }
else
{ :ok ,
from ( source in data_layer_query ,
2022-08-19 06:56:36 +12:00
where : field ( source , ^ source_attribute ) in ^ source_values ,
2021-07-20 05:56:36 +12:00
inner_lateral_join : destination in ^ subquery ,
2022-08-19 06:56:36 +12:00
on : field ( source , ^ source_attribute ) == field ( destination , ^ destination_attribute ) ,
2021-07-20 05:56:36 +12:00
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 ,
[
2022-08-19 06:56:36 +12:00
{ source_query , source_attribute , source_attribute_on_join_resource , relationship } ,
{ through_resource , destination_attribute_on_join_resource , destination_attribute ,
2021-04-30 09:31:19 +12:00
through_relationship }
]
) do
2021-05-08 03:04:09 +12:00
source_query = Ash.Query . new ( source_query )
2022-08-19 06:56:36 +12:00
source_values = Enum . map ( root_data , & Map . get ( &1 , source_attribute ) )
2021-04-30 09:31:19 +12:00
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 )
2022-10-15 18:03:16 +13:00
|> Ash.Query . sort ( through_relationship . sort , prepend? : true )
2021-05-08 03:04:09 +12:00
|> 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 } ->
2022-05-11 14:47:21 +12:00
if query . __ash_bindings__ [ :__order__? ] do
2021-07-20 05:56:36 +12:00
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 :
2022-08-19 06:56:36 +12:00
field ( through , ^ destination_attribute_on_join_resource ) ==
field ( destination , ^ destination_attribute ) ,
2021-07-20 05:56:36 +12:00
where :
2022-08-19 06:56:36 +12:00
field ( through , ^ source_attribute_on_join_resource ) ==
2022-10-15 18:03:16 +13:00
field ( parent_as ( ^ 0 ) , ^ source_attribute ) ,
select_merge : %{
__lateral_join_source__ : field ( through , ^ source_attribute_on_join_resource )
}
2021-07-20 05:56:36 +12:00
)
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 ,
2022-08-19 06:56:36 +12:00
where : field ( source , ^ source_attribute ) in ^ source_values ,
2021-07-20 05:56:36 +12:00
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 :
2022-08-19 06:56:36 +12:00
field ( through , ^ destination_attribute_on_join_resource ) ==
field ( destination , ^ destination_attribute ) ,
2021-07-20 05:56:36 +12:00
where :
2022-08-19 06:56:36 +12:00
field ( through , ^ source_attribute_on_join_resource ) ==
2022-10-15 18:03:16 +13:00
field ( parent_as ( ^ 0 ) , ^ source_attribute ) ,
select_merge : %{
__lateral_join_source__ : field ( through , ^ source_attribute_on_join_resource )
}
2021-07-20 05:56:36 +12:00
)
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 ,
2022-08-19 06:56:36 +12:00
where : field ( source , ^ source_attribute ) in ^ source_values ,
2021-07-20 05:56:36 +12:00
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
2022-08-24 11:56:46 +12:00
config = AshPostgres.DataLayer.Info . repo ( resource ) . config ( )
2021-07-28 15:03:39 +12:00
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 (
2022-08-24 11:56:46 +12:00
source_query . tenant || AshPostgres.DataLayer.Info . schema ( resource ) ||
config [ :default_prefix ] ||
2021-07-28 15:03:39 +12:00
" public "
)
}
2021-07-25 08:59:23 +12:00
else
2021-07-28 15:03:39 +12:00
%{
data_layer_query
| prefix :
to_string (
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info . schema ( resource ) || config [ :default_prefix ] ||
2021-07-28 15:03:39 +12:00
" 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
2021-12-21 16:19:24 +13:00
def resource_to_query ( resource , _ ) do
2022-08-24 11:56:46 +12:00
from ( row in { AshPostgres.DataLayer.Info . table ( resource ) || " " , resource } , as : ^ 0 )
2021-12-21 16:19:24 +13:00
end
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 )
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . repo ( resource ) . insert (
repo_opts ( changeset . timeout , changeset . tenant , changeset . resource )
)
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
2022-08-24 11:56:46 +12:00
if AshPostgres.DataLayer.Info . manage_tenant_create? ( resource ) do
2020-10-29 15:26:45 +13:00
tenant_name = tenant_name ( resource , result )
2022-08-24 11:56:46 +12:00
AshPostgres.MultiTenancy . create_tenant! (
tenant_name ,
AshPostgres.DataLayer.Info . repo ( resource )
)
2020-10-29 15:26:45 +13:00
else
:ok
end
end
defp maybe_update_tenant ( resource , changeset , result ) do
2022-08-24 11:56:46 +12:00
if AshPostgres.DataLayer.Info . manage_tenant_update? ( resource ) do
2020-10-29 15:26:45 +13:00
changing_tenant_name? =
resource
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . manage_tenant_template ( )
2020-10-29 15:26:45 +13:00
|> 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 )
2022-08-24 11:56:46 +12:00
AshPostgres.MultiTenancy . rename_tenant (
AshPostgres.DataLayer.Info . repo ( resource ) ,
old_tenant_name ,
new_tenant_name
)
2020-10-29 15:26:45 +13:00
end
end
:ok
end
defp tenant_name ( resource , result ) do
resource
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . manage_tenant_template ( )
2020-10-29 15:26:45 +13:00
|> 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__ )
2022-03-21 13:35:30 +13:00
|> add_exclusion_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
2022-08-24 11:56:46 +12:00
if AshPostgres.DataLayer.Info . polymorphic? ( record . __struct__ ) do
table =
changeset . context [ :data_layer ] [ :table ] ||
AshPostgres.DataLayer.Info . table ( record . __struct__ )
2021-01-29 13:42:55 +13:00
2022-05-14 09:41:30 +12:00
record =
if table do
Ecto . put_meta ( record , source : table )
else
raise_table_error! ( changeset . resource , operation )
end
2022-08-24 11:56:46 +12:00
prefix =
changeset . context [ :data_layer ] [ :schema ] ||
AshPostgres.DataLayer.Info . schema ( record . __struct__ )
2022-05-14 09:41:30 +12:00
if prefix do
Ecto . put_meta ( record , prefix : table )
2021-01-29 13:42:55 +13:00
else
2022-05-14 09:41:30 +12:00
record
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
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . check_constraints ( )
2021-04-20 06:26:41 +12:00
|> 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
2022-03-21 13:35:30 +13:00
defp add_exclusion_constraints ( changeset , resource ) do
resource
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . exclusion_constraint_names ( )
2022-03-21 13:35:30 +13:00
|> Enum . reduce ( changeset , fn constraint , changeset ->
case constraint do
{ key , name } ->
Ecto.Changeset . exclusion_constraint ( changeset , key , name : name )
{ key , name , message } ->
Ecto.Changeset . exclusion_constraint ( changeset , key , name : name , message : message )
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 ) )
2022-08-19 06:56:36 +12:00
|> Enum . map ( & Map . take ( &1 , [ :source , :source_attribute , :destination_attribute ] ) )
2021-03-20 11:41:16 +13:00
end )
|> Enum . uniq ( )
|> Enum . reduce ( changeset , fn %{
source : source ,
2022-08-19 06:56:36 +12:00
source_attribute : source_attribute ,
destination_attribute : destination_attribute
2021-03-20 11:41:16 +13:00
} ,
changeset ->
2022-08-19 06:56:36 +12:00
Ecto.Changeset . foreign_key_constraint ( changeset , destination_attribute ,
2022-08-24 11:56:46 +12:00
name : " #{ AshPostgres.DataLayer.Info . table ( source ) } _ #{ source_attribute } _fkey " ,
2021-03-20 11:41:16 +13:00
message : " would leave records behind "
)
end )
end
defp add_my_foreign_key_constraints ( changeset , resource ) do
resource
|> Ash.Resource.Info . relationships ( )
2022-08-19 06:56:36 +12:00
|> Enum . reduce ( changeset , & Ecto.Changeset . foreign_key_constraint ( &2 , &1 . source_attribute ) )
2021-03-20 11:41:16 +13:00
end
defp add_configured_foreign_key_constraints ( changeset , resource ) do
resource
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . foreign_key_names ( )
2021-03-20 11:41:16 +13:00
|> 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 =
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info . identity_index_names ( resource ) [ identity . name ] ||
2021-04-28 09:16:56 +12:00
" #{ 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
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . unique_index_names ( )
2021-01-22 09:32:26 +13:00
|> 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 )
2022-07-07 06:44:18 +12:00
explicitly_changing_attributes =
Enum . map (
Map . keys ( changeset . attributes ) -- Map . get ( changeset , :defaults , [ ] ) -- keys ,
fn key ->
{ key , Ash.Changeset . get_attribute ( changeset , key ) }
end
)
on_conflict =
changeset
|> update_defaults ( )
|> Keyword . merge ( explicitly_changing_attributes )
2021-05-19 15:04:37 +12:00
2022-07-02 11:12:14 +12:00
conflict_target =
if Ash.Resource.Info . base_filter ( resource ) do
base_filter_sql =
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info . base_filter_sql ( resource ) ||
2022-07-02 11:12:14 +12:00
raise """
Cannot use upserts with resources that have a base_filter without also adding ` base_filter_sql ` in the postgres section .
"""
{ :unsafe_fragment , " ( " <> Enum . join ( keys , " , " ) <> " ) WHERE ( #{ base_filter_sql } ) " }
else
keys
end
2020-10-29 15:26:45 +13:00
repo_opts =
2022-05-23 10:30:20 +12:00
changeset . timeout
|> repo_opts ( changeset . tenant , changeset . resource )
2022-07-07 06:44:18 +12:00
|> Keyword . put ( :on_conflict , set : on_conflict )
2022-07-02 11:12:14 +12:00
|> Keyword . put ( :conflict_target , conflict_target )
2020-10-29 15:26:45 +13:00
2022-08-24 11:56:46 +12:00
if AshPostgres.DataLayer.Info . manage_tenant_update? ( resource ) do
2020-10-29 15:26:45 +13:00
{ :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 )
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . 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
2022-07-07 06:44:18 +12:00
defp update_defaults ( changeset ) do
attributes =
changeset . resource
|> Ash.Resource.Info . attributes ( )
|> Enum . reject ( & is_nil ( &1 . update_default ) )
attributes
|> static_defaults ( )
|> Enum . concat ( lazy_matching_defaults ( attributes ) )
|> Enum . concat ( lazy_non_matching_defaults ( attributes ) )
end
defp static_defaults ( attributes ) do
attributes
|> Enum . reject ( & get_default_fun ( &1 ) )
|> Enum . map ( & { &1 . name , &1 . update_default } )
end
defp lazy_non_matching_defaults ( attributes ) do
attributes
|> Enum . filter ( & ( ! &1 . match_other_defaults? && get_default_fun ( &1 ) ) )
|> Enum . map ( & { &1 . name , &1 . update_default } )
end
defp lazy_matching_defaults ( attributes ) do
attributes
|> Enum . filter ( & ( &1 . match_other_defaults? && get_default_fun ( &1 ) ) )
|> Enum . group_by ( & &1 . update_default )
|> Enum . flat_map ( fn { default_fun , attributes } ->
default_value =
case default_fun do
function when is_function ( function ) ->
function . ( )
{ m , f , a } when is_atom ( m ) and is_atom ( f ) and is_list ( a ) ->
apply ( m , f , a )
end
Enum . map ( attributes , & { &1 . name , default_value } )
end )
end
defp get_default_fun ( attribute ) do
if is_function ( attribute . update_default ) or match? ( { _ , _ , _ } , attribute . update_default ) do
attribute . update_default
end
end
2020-06-14 19:04:18 +12:00
@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 )
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . repo ( resource ) . update (
repo_opts ( changeset . timeout , changeset . tenant , changeset . resource )
)
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 )
2022-08-24 11:56:46 +12:00
|> AshPostgres.DataLayer.Info . repo ( resource ) . delete (
repo_opts ( changeset . timeout , changeset . tenant , changeset . resource )
)
2021-03-20 11:41:16 +13:00
|> 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
2022-05-11 14:47:21 +12:00
query
|> AshPostgres.Sort . sort ( sort , resource )
|> case do
{ :ok , query } ->
{ :ok ,
Map . update! ( query , :__ash_bindings__ , fn bindings ->
Map . put ( bindings , :sort , sort )
end ) }
other ->
other
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 ,
2022-04-08 11:59:43 +12:00
select : struct ( row , ^ Enum . uniq ( select ) )
2021-04-09 16:53:50 +12:00
) }
end
2022-05-11 14:47:21 +12:00
# If the order by does not match the initial sort clause, then we use a subquery
# to limit to only distinct rows. This may not perform that well, so we may need
# to come up with alternatives here.
2021-04-01 19:19:30 +13:00
@impl true
2022-05-11 14:47:21 +12:00
def distinct ( query , empty , _ ) when empty in [ nil , [ ] ] do
{ :ok , query }
end
2021-04-01 19:19:30 +13:00
2022-05-11 14:47:21 +12:00
def distinct ( query , distinct_on , resource ) do
case get_distinct_statement ( query , distinct_on ) do
{ :ok , distinct_statement } ->
{ :ok , %{ query | distinct : distinct_statement } }
{ :error , distinct_statement } ->
distinct_query =
query
|> default_bindings ( resource )
|> Map . put ( :distinct , distinct_statement )
on =
Enum . reduce ( Ash.Resource.Info . primary_key ( resource ) , nil , fn key , dynamic ->
if dynamic do
Ecto.Query . dynamic (
[ row , distinct ] ,
^ dynamic and field ( row , ^ key ) == field ( distinct , ^ key )
)
else
Ecto.Query . dynamic ( [ row , distinct ] , field ( row , ^ key ) == field ( distinct , ^ key ) )
end
2021-04-01 19:19:30 +13:00
end )
2022-05-11 14:47:21 +12:00
joined_query =
from ( row in query . from . source ,
join : distinct in subquery ( distinct_query ) ,
on : ^ on
)
joined_query =
from ( [ row , distinct ] in joined_query ,
select : distinct
)
|> default_bindings ( resource )
{ :ok ,
Map . update! (
joined_query ,
:__ash_bindings__ ,
& Map . put ( &1 , :__order__? , query . __ash_bindings__ [ :__order__? ] || false )
) }
end
end
defp get_distinct_statement ( query , distinct_on ) do
sort = query . __ash_bindings__ . sort || [ ]
if sort == [ ] do
{ :ok , default_distinct_statement ( query , distinct_on ) }
else
distinct_on
|> Enum . reduce_while ( { sort , [ ] } , fn
_ , { [ ] , _distinct_statement } ->
{ :halt , :error }
distinct_on , { [ order_by | rest_order_by ] , distinct_statement } ->
case order_by do
{ ^ distinct_on , order } ->
binding =
case Map . fetch ( query . __ash_bindings__ . aggregates , distinct_on ) do
{ :ok , binding } ->
binding
:error ->
0
end
{ :cont ,
{ rest_order_by ,
[
{ order , { { :. , [ ] , [ { :& , [ ] , [ binding ] } , distinct_on ] } , [ ] , [ ] } }
| distinct_statement
] } }
_ ->
{ :halt , :error }
end
2021-04-01 19:19:30 +13:00
end )
2022-05-11 14:47:21 +12:00
|> case do
:error ->
{ :error , default_distinct_statement ( query , distinct_on ) }
2021-04-01 19:19:30 +13:00
2022-05-11 14:47:21 +12:00
{ _ , result } ->
distinct =
query . distinct ||
% Ecto.Query.QueryExpr {
expr : [ ]
}
{ :ok , %{ distinct | expr : distinct . expr ++ Enum . reverse ( result ) } }
end
end
2021-04-01 19:19:30 +13:00
end
2022-05-11 14:47:21 +12:00
defp default_distinct_statement ( query , distinct_on ) do
distinct =
query . 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
2020-06-14 19:04:18 +12:00
@impl true
2021-12-21 16:19:24 +13:00
def filter ( query , filter , resource ) do
query = default_bindings ( query , resource )
2020-06-19 15:04:41 +12:00
2021-05-07 19:09:49 +12:00
query
2021-12-21 16:19:24 +13:00
|> AshPostgres.Join . join_all_relationships ( 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-12-21 16:19:24 +13:00
@doc false
def default_bindings ( query , resource , context \\ %{ } ) do
2020-07-23 17:13:47 +12:00
Map . put_new ( query , :__ash_bindings__ , %{
2022-07-21 06:19:06 +12:00
resource : resource ,
2020-07-23 17:13:47 +12:00
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
@impl true
2022-01-14 08:11:30 +13:00
def add_aggregates ( query , aggregates , resource ) do
AshPostgres.Aggregate . add_aggregates ( query , aggregates , resource )
2020-07-23 17:13:47 +12:00
end
2021-06-04 17:48:35 +12:00
@impl true
2022-01-14 08:11:30 +13:00
def add_calculations ( query , calculations , resource ) do
AshPostgres.Calculation . add_calculations ( query , calculations , resource )
2021-06-04 17:48:35 +12:00
end
2021-12-21 16:19:24 +13:00
@doc false
2022-09-29 11:01:20 +13:00
def get_binding ( resource , path , %{ __ash_bindings__ : _ } = query , type , name_match ) do
2021-12-21 16:19:24 +13:00
paths =
Enum . flat_map ( query . __ash_bindings__ . bindings , fn
2022-09-29 11:01:20 +13:00
{ binding , %{ path : path , type : ^ type , name : name } } ->
if name_match do
if name == name_match do
[ { binding , path } ]
else
[ ]
end
else
[ { binding , path } ]
end
2021-06-04 17:48:35 +12:00
2021-12-21 16:19:24 +13:00
_ ->
[ ]
end )
2021-06-04 17:48:35 +12:00
2021-12-21 16:19:24 +13:00
Enum . find_value ( paths , fn { binding , candidate_path } ->
Ash.SatSolver . synonymous_relationship_paths? ( resource , candidate_path , path ) && binding
end )
2020-07-23 17:13:47 +12:00
end
2022-09-29 11:01:20 +13:00
def get_binding ( _ , _ , _ , _ , _ ) , do : nil
2021-06-04 17:48:35 +12:00
2021-12-21 16:19:24 +13:00
defp add_filter_expression ( query , filter ) do
filter
|> split_and_statements ( )
|> Enum . reduce ( query , fn filter , query ->
2022-01-25 11:59:31 +13:00
dynamic = AshPostgres.Expr . dynamic_expr ( query , filter , query . __ash_bindings__ )
2021-06-04 17:48:35 +12:00
2021-12-21 16:19:24 +13:00
Ecto.Query . where ( query , ^ dynamic )
end )
2021-09-14 04:58:23 +12:00
end
2021-12-21 16:19:24 +13:00
defp split_and_statements ( % Filter { expression : expression } ) do
split_and_statements ( expression )
2021-12-18 07:25:14 +13:00
end
2021-12-21 16:19:24 +13:00
defp split_and_statements ( % BooleanExpression { op : :and , left : left , right : right } ) do
split_and_statements ( left ) ++ split_and_statements ( right )
2021-09-14 04:58:23 +12:00
end
2021-12-21 16:19:24 +13:00
defp split_and_statements ( % Not { expression : % Not { expression : expression } } ) do
split_and_statements ( expression )
2020-07-23 17:13:47 +12:00
end
2021-12-21 16:19:24 +13:00
defp split_and_statements ( % Not {
expression : % BooleanExpression { op : :or , left : left , right : right }
} ) do
split_and_statements ( % BooleanExpression {
op : :and ,
left : % Not { expression : left } ,
right : % Not { expression : right }
} )
2020-08-09 08:19:18 +12:00
end
2021-12-21 16:19:24 +13:00
defp split_and_statements ( other ) , do : [ other ]
2020-07-23 17:13:47 +12:00
2021-12-21 16:19:24 +13:00
@doc false
2022-01-25 11:59:31 +13:00
def add_binding ( query , data , additional_bindings \\ 0 ) do
2021-12-21 16:19:24 +13:00
current = query . __ash_bindings__ . current
bindings = query . __ash_bindings__ . bindings
2020-07-23 17:13:47 +12:00
2021-12-21 16:19:24 +13:00
new_ash_bindings = %{
query . __ash_bindings__
| bindings : Map . put ( bindings , current , data ) ,
2022-01-25 11:59:31 +13:00
current : current + 1 + additional_bindings
2020-07-23 17:13:47 +12:00
}
2021-07-02 07:28:27 +12:00
2021-12-21 16:19:24 +13:00
%{ query | __ash_bindings__ : new_ash_bindings }
2021-04-27 08:45:47 +12:00
end
2021-12-21 16:19:24 +13:00
@impl true
2022-04-18 16:23:09 +12:00
def transaction ( resource , func , timeout \\ nil ) do
if timeout do
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info . repo ( resource ) . transaction ( func , timeout : timeout )
2022-04-18 16:23:09 +12:00
else
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info . repo ( resource ) . transaction ( func )
2022-04-18 16:23:09 +12:00
end
2020-06-14 19:04:18 +12:00
end
@impl true
2020-07-08 12:01:01 +12:00
def rollback ( resource , term ) do
2022-08-24 11:56:46 +12:00
AshPostgres.DataLayer.Info . repo ( resource ) . rollback ( term )
2020-06-14 19:04:18 +12:00
end
2021-02-06 12:59:33 +13:00
defp table ( resource , changeset ) do
2022-08-24 11:56:46 +12:00
changeset . context [ :data_layer ] [ :table ] || AshPostgres.DataLayer.Info . table ( resource )
2021-02-06 12:59:33 +13:00
end
2021-03-22 10:58:47 +13:00
defp raise_table_error! ( resource , operation ) do
2022-08-24 11:56:46 +12:00
if AshPostgres.DataLayer.Info . polymorphic? ( resource ) do
2021-03-22 10:58:47 +13:00
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