@@ -89,7 +89,15 @@ defmodule AshPostgres.DataLayer do
8989default: [ ] ,
9090doc: """
9191 A list of unique index names that could raise errors, or an mfa to a function that takes a changeset
92- and returns a list of names in the format `{[:affected, :keys], "name_of_constraint"}`
92+ and returns the list. Must be in the format `{[:affected, :keys], "name_of_constraint"}` or `{[:affected, :keys], "name_of_constraint", "custom error message"}`
93+ """
94+ ] ,
95+ foreign_key_names: [
96+ type: :any ,
97+ default: [ ] ,
98+ doc: """
99+ A list of foreign keys that could raise errors, or an mfa to a function that takes a changeset and returns the list.
100+ Must be in the format `{:key, "name_of_constraint"}` or `{:key, "name_of_constraint", "custom error message"}`
93101 """
94102] ,
95103table: [
@@ -403,7 +411,7 @@ defmodule AshPostgres.DataLayer do
403411def create ( resource , changeset ) do
404412changeset . data
405413|> Map . update! ( :__meta__ , & Map . put ( & 1 , :source , table ( resource , changeset ) ) )
406- |> ecto_changeset ( changeset )
414+ |> ecto_changeset ( changeset , :create )
407415|> repo ( resource ) . insert ( repo_opts ( changeset ) )
408416|> handle_errors ( )
409417|> case do
@@ -470,11 +478,32 @@ defmodule AshPostgres.DataLayer do
470478Ash.Error.Changes.InvalidAttribute . exception ( field: field , message: message , vars: vars )
471479end
472480
473- defp ecto_changeset ( record , changeset ) do
474- record
475- |> set_table ( changeset )
476- |> Ecto.Changeset . change ( changeset . attributes )
477- |> add_unique_indexes ( record . __struct__ , changeset . tenant , changeset )
481+ defp ecto_changeset ( record , changeset , type ) do
482+ ecto_changeset =
483+ record
484+ |> set_table ( changeset )
485+ |> Ecto.Changeset . change ( changeset . attributes )
486+
487+ case type do
488+ :create ->
489+ ecto_changeset
490+ |> add_unique_indexes ( record . __struct__ , changeset . tenant , changeset )
491+ |> add_my_foreign_key_constraints ( record . __struct__ )
492+ |> add_configured_foreign_key_constraints ( record . __struct__ )
493+
494+ type when type in [ :upsert , :update ] ->
495+ ecto_changeset
496+ |> add_unique_indexes ( record . __struct__ , changeset . tenant , changeset )
497+ |> add_my_foreign_key_constraints ( record . __struct__ )
498+ |> add_related_foreign_key_constraints ( record . __struct__ )
499+ |> add_configured_foreign_key_constraints ( record . __struct__ )
500+
501+ :delete ->
502+ ecto_changeset
503+ |> add_unique_indexes ( record . __struct__ , changeset . tenant , changeset )
504+ |> add_related_foreign_key_constraints ( record . __struct__ )
505+ |> add_configured_foreign_key_constraints ( record . __struct__ )
506+ end
478507end
479508
480509defp set_table ( record , changeset ) do
@@ -494,6 +523,55 @@ defmodule AshPostgres.DataLayer do
494523end
495524end
496525
526+ defp add_related_foreign_key_constraints ( changeset , resource ) do
527+ # TODO: this doesn't guarantee us to get all of them, because if something is related to this
528+ # schema and there is no back-relation, then this won't catch it's foreign key constraints
529+ resource
530+ |> Ash.Resource.Info . relationships ( )
531+ |> Enum . map ( & & 1 . destination )
532+ |> Enum . uniq ( )
533+ |> Enum . flat_map ( fn related ->
534+ related
535+ |> Ash.Resource.Info . relationships ( )
536+ |> Enum . filter ( & ( & 1 . destination == resource ) )
537+ |> Enum . map ( & Map . take ( & 1 , [ :source , :source_field , :destination_field ] ) )
538+ end )
539+ |> Enum . uniq ( )
540+ |> Enum . reduce ( changeset , fn % {
541+ source: source ,
542+ source_field: source_field ,
543+ destination_field: destination_field
544+ } ,
545+ changeset ->
546+ Ecto.Changeset . foreign_key_constraint ( changeset , destination_field ,
547+ name: "#{ AshPostgres . table ( source ) } _#{ source_field } _fkey" ,
548+ message: "would leave records behind"
549+ )
550+ end )
551+ end
552+
553+ defp add_my_foreign_key_constraints ( changeset , resource ) do
554+ resource
555+ |> Ash.Resource.Info . relationships ( )
556+ |> Enum . reduce ( changeset , & Ecto.Changeset . foreign_key_constraint ( & 2 , & 1 . source_field ) )
557+ end
558+
559+ defp add_configured_foreign_key_constraints ( changeset , resource ) do
560+ resource
561+ |> AshPostgres . foreign_key_names ( )
562+ |> case do
563+ { m , f , a } -> List . wrap ( apply ( m , f , [ changeset | a ] ) )
564+ value -> List . wrap ( value )
565+ end
566+ |> Enum . reduce ( changeset , fn
567+ { key , name } , changeset ->
568+ Ecto.Changeset . foreign_key_constraint ( changeset , key , name: name )
569+
570+ { key , name , message } , changeset ->
571+ Ecto.Changeset . foreign_key_constraint ( changeset , key , name: name , message: message )
572+ end )
573+ end
574+
497575defp add_unique_indexes ( changeset , resource , tenant , ash_changeset ) do
498576changeset =
499577resource
@@ -528,8 +606,12 @@ defmodule AshPostgres.DataLayer do
528606{ Ash.Resource.Info . primary_key ( resource ) , table ( resource , ash_changeset ) <> "_pkey" } | names
529607]
530608
531- Enum . reduce ( names , changeset , fn { keys , name } , changeset ->
532- Ecto.Changeset . unique_constraint ( changeset , List . wrap ( keys ) , name: name )
609+ Enum . reduce ( names , changeset , fn
610+ { keys , name } , changeset ->
611+ Ecto.Changeset . unique_constraint ( changeset , List . wrap ( keys ) , name: name )
612+
613+ { keys , name , message } , changeset ->
614+ Ecto.Changeset . unique_constraint ( changeset , List . wrap ( keys ) , name: name , message: message )
533615end )
534616end
535617
@@ -546,7 +628,7 @@ defmodule AshPostgres.DataLayer do
546628else
547629changeset . data
548630|> Map . update! ( :__meta__ , & Map . put ( & 1 , :source , table ( resource , changeset ) ) )
549- |> ecto_changeset ( changeset )
631+ |> ecto_changeset ( changeset , :upsert )
550632|> repo ( resource ) . insert ( repo_opts )
551633|> handle_errors ( )
552634end
@@ -556,7 +638,7 @@ defmodule AshPostgres.DataLayer do
556638def update ( resource , changeset ) do
557639changeset . data
558640|> Map . update! ( :__meta__ , & Map . put ( & 1 , :source , table ( resource , changeset ) ) )
559- |> ecto_changeset ( changeset )
641+ |> ecto_changeset ( changeset , :update )
560642|> repo ( resource ) . update ( repo_opts ( changeset ) )
561643|> handle_errors ( )
562644|> case do
@@ -572,7 +654,10 @@ defmodule AshPostgres.DataLayer do
572654
573655@ impl true
574656def destroy ( resource , % { data: record } = changeset ) do
575- case repo ( resource ) . delete ( record , repo_opts ( changeset ) ) do
657+ record
658+ |> ecto_changeset ( changeset , :delete )
659+ |> repo ( resource ) . delete ( repo_opts ( changeset ) )
660+ |> case do
576661{ :ok , _record } ->
577662:ok
578663