@@ -211,6 +211,50 @@ defmodule Ecto.RepoTest do
211211 end
212212 end
213213
214+ defmodule InsertSelectRenamed do
215+ use Ecto.Schema
216+
217+ @ primary_key false
218+ schema "insert_select_renamed" do
219+ field :name , :string , source: :renamed_name
220+ field :value , :string
221+ end
222+ end
223+
224+ defmodule InsertSelectReadOnly do
225+ use Ecto.Schema
226+
227+ @ primary_key false
228+ schema "insert_select_read_only" do
229+ field :name , :string , source: :source_name , writable: :never
230+ field :value , :string
231+ end
232+ end
233+
234+ defmodule InsertSelectDisjointSource do
235+ use Ecto.Schema
236+
237+ @ primary_key false
238+ schema "insert_select_disjoint_source" do
239+ field :b , :string , source: :src_b
240+ field :c , :string , source: :src_c
241+ field :a , :string , source: :src_a
242+ field :only_source , :string
243+ end
244+ end
245+
246+ defmodule InsertSelectDisjointDestination do
247+ use Ecto.Schema
248+
249+ @ primary_key false
250+ schema "insert_select_disjoint_destination" do
251+ field :only_destination , :string
252+ field :a , :string
253+ field :c , :string , source: :dst_c
254+ field :b , :string , source: :dst_b
255+ end
256+ end
257+
214258 test "defines child_spec/1" do
215259 assert TestRepo . child_spec ( [ ] ) == % {
216260 id: TestRepo ,
@@ -765,6 +809,167 @@ defmodule Ecto.RepoTest do
765809 assert header == [ :id , :x , :yyy , :z , :array , :map ]
766810 end
767811
812+ test "maps read-only source fields through the writable destination schema" do
813+ query = from s in InsertSelectReadOnly , select: s
814+ TestRepo . insert_all ( InsertSelectRenamed , query )
815+
816+ assert_received { :insert_all , % { header: [ :renamed_name , :value ] } ,
817+ { % Ecto.Query { select: % { fields: fields } } , _params } }
818+
819+ assert [
820+ { { :. , _ , [ { :& , _ , [ 0 ] } , :source_name ] } , [ ] , [ ] } ,
821+ { { :. , _ , [ { :& , _ , [ 0 ] } , :value ] } , [ ] , [ ] }
822+ ] = fields
823+ end
824+
825+ test "maps fragment columns through the destination schema" do
826+ query =
827+ from f in fragment ( "select 1 as name, 2 as value" , columns: [ :name , :value ] ) , select: f
828+
829+ TestRepo . insert_all ( InsertSelectRenamed , query )
830+
831+ assert_received { :insert_all , % { header: [ :renamed_name , :value ] } , { % Ecto.Query { } , _params } }
832+ end
833+
834+ test "maps subquery fields through the destination schema" do
835+ inner = from s in InsertSelectReadOnly , select: % { name: s . name , value: s . value }
836+ query = from s in subquery ( inner ) , select: s
837+ TestRepo . insert_all ( InsertSelectRenamed , query )
838+
839+ assert_received { :insert_all , % { header: [ :renamed_name , :value ] } , { % Ecto.Query { } , _params } }
840+ end
841+
842+ test "rejects a map update that repeats a subquery field" do
843+ inner = from s in InsertSelectReadOnly , select: % { name: s . name , value: s . value }
844+ query = from s in subquery ( inner ) , select: % { s | value: "new" }
845+
846+ # This query shape still projects the overwritten field from the subquery.
847+ # Reject the duplicate projection; this does not add support for subquery map updates.
848+ assert_raise ArgumentError ,
849+ ~r/ cannot generate a fields list for insert_all from the given source query:/ ,
850+ fn -> TestRepo . insert_all ( InsertSelectRenamed , query ) end
851+ end
852+
853+ test "maps values fields through the destination schema" do
854+ query =
855+ from v in values ( [ % { name: "n" , value: "v" } ] , % { name: :string , value: :string } ) ,
856+ select: % { v | value: "new" }
857+
858+ TestRepo . insert_all ( InsertSelectRenamed , query )
859+
860+ assert_received { :insert_all , % { header: [ :renamed_name , :value ] } , { % Ecto.Query { } , _params } }
861+ end
862+
863+ test "maps a joined binding through the destination schema" do
864+ query = from x in "other" , join: s in InsertSelectReadOnly , on: true , select: s
865+ TestRepo . insert_all ( InsertSelectRenamed , query )
866+
867+ assert_received { :insert_all , % { header: [ :renamed_name , :value ] } , { % Ecto.Query { } , _params } }
868+ end
869+
870+ test "keeps source columns when the destination has no schema" do
871+ query = from s in InsertSelectReadOnly , select: s
872+ TestRepo . insert_all ( "insert_select_renamed" , query )
873+
874+ assert_received { :insert_all , % { header: [ :source_name , :value ] } , { % Ecto.Query { } , _params } }
875+ end
876+
877+ test "rejects full source fields that are unwritable in the destination" do
878+ query = from s in InsertSelectRenamed , select: s
879+
880+ assert_raise ArgumentError ,
881+ "cannot select unwritable field `:name` for insert_all" ,
882+ fn -> TestRepo . insert_all ( InsertSelectReadOnly , query ) end
883+ end
884+
885+ test "maps unchanged map update fields through the destination schema" do
886+ query = from s in InsertSelectReadOnly , select: % { s | value: s . value }
887+ TestRepo . insert_all ( InsertSelectRenamed , query )
888+
889+ assert_received { :insert_all , % { header: [ :renamed_name , :value ] } ,
890+ { % Ecto.Query { select: % { fields: fields } } , _params } }
891+
892+ assert [
893+ { { :. , _ , [ { :& , _ , [ 0 ] } , :source_name ] } , [ ] , [ ] } ,
894+ { { :. , _ , [ { :& , _ , [ 0 ] } , :value ] } , [ ] , [ ] }
895+ ] = fields
896+ end
897+
898+ test "does not include an overwritten source field twice when columns differ" do
899+ query = from s in InsertSelectReadOnly , select: % { s | name: s . name }
900+ TestRepo . insert_all ( InsertSelectRenamed , query )
901+
902+ assert_received { :insert_all , % { header: [ :value , :renamed_name ] } , { % Ecto.Query { } , _params } }
903+ end
904+
905+ test "rejects unchanged map update fields that are unwritable in the destination" do
906+ query = from s in InsertSelectRenamed , select: % { s | value: s . value }
907+
908+ assert_raise ArgumentError ,
909+ "cannot select unwritable field `:name` for insert_all" ,
910+ fn -> TestRepo . insert_all ( InsertSelectReadOnly , query ) end
911+ end
912+
913+ test "rejects map updates whose values expand to multiple select fields" do
914+ query = from s in InsertSelectReadOnly , select: % { s | value: { s . name , s . value } }
915+
916+ assert_raise ArgumentError ,
917+ ~r/ cannot generate a fields list for insert_all from the given source query:/ ,
918+ fn -> TestRepo . insert_all ( InsertSelectRenamed , query ) end
919+ end
920+
921+ test "maps a reordered source subset independently of destination field order" do
922+ query =
923+ from s in InsertSelectDisjointSource ,
924+ select: % { map ( s , [ :b , :c , :a ] ) | a: fragment ( "'x'" ) }
925+
926+ TestRepo . insert_all ( InsertSelectDisjointDestination , query )
927+
928+ assert_received { :insert_all , % { header: [ :dst_b , :dst_c , :a ] } ,
929+ { % Ecto.Query { select: % { fields: fields } } , _params } }
930+
931+ assert [
932+ { { :. , _ , [ { :& , _ , [ 0 ] } , :src_b ] } , [ ] , [ ] } ,
933+ { { :. , _ , [ { :& , _ , [ 0 ] } , :src_c ] } , [ ] , [ ] } ,
934+ { :fragment , _ , _ }
935+ ] = fields
936+ end
937+
938+ test "allows destination-only map updates" do
939+ query =
940+ from s in InsertSelectDisjointSource ,
941+ select: % { map ( s , [ :b , :c ] ) | a: s . c , only_destination: s . b }
942+
943+ TestRepo . insert_all ( InsertSelectDisjointDestination , query )
944+
945+ assert_received { :insert_all , % { header: [ :dst_b , :dst_c , :a , :only_destination ] } ,
946+ { % Ecto.Query { } , _params } }
947+ end
948+
949+ test "reports source-only fields" do
950+ query = from s in InsertSelectDisjointSource , select: s
951+
952+ assert_raise ArgumentError ,
953+ "cannot select unknown field `:only_source` for insert_all" ,
954+ fn -> TestRepo . insert_all ( InsertSelectDisjointDestination , query ) end
955+ end
956+
957+ test "rejects whole schemaless bindings with a select error" do
958+ query = from s in "insert_select_source" , select: s
959+
960+ assert_raise ArgumentError ,
961+ ~r/ cannot generate a fields list for insert_all from the given source query:/ ,
962+ fn -> TestRepo . insert_all ( InsertSelectRenamed , query ) end
963+ end
964+
965+ test "rejects non-atom insert select keys with a clear error" do
966+ query = from s in InsertSelectReadOnly , select: % { "name" => s . name }
967+
968+ assert_raise ArgumentError ,
969+ "cannot select non-atom field `\" name\" ` for insert_all" ,
970+ fn -> TestRepo . insert_all ( InsertSelectRenamed , query ) end
971+ end
972+
768973 test "takes query selecting on source with join" do
769974 query = from p in MyParent , join: a in MySchemaWithAssoc , on: true , select: a
770975 TestRepo . insert_all ( MySchemaWithAssoc , query )
0 commit comments