changed
CHANGELOG.md
|
@@ -1,5 +1,51 @@
|
1
1
|
# Changelog for v3.x
|
2
2
|
|
3
|
+ ## v3.12.0 (2024-08-12)
|
4
|
+
|
5
|
+ ### Enhancements
|
6
|
+
|
7
|
+ * [Ecto.Changeset] Allow `{message, opts}` to be given as message for several validation APIs
|
8
|
+ * [Ecto.Query] Introduce `is_named_binding` guard
|
9
|
+ * [Ecto.Query] Subqueries are now supported in `distinct`, `group_by`, `order_by` and `window` expressions
|
10
|
+ * [Ecto.Query] Allow `select_merge` to be used in more `insert_all` and subquery operations by merging distinct fields
|
11
|
+ * [Ecto.Query] Allow literal maps inside `dynamic/2`
|
12
|
+ * [Ecto.Query] Support macro expansion at the root level of `order_by`
|
13
|
+ * [Ecto.Query] Support preloading subquery sources in `from` and `join`
|
14
|
+ * [Ecto.Query] Allow map updates with dynamic values in `select`
|
15
|
+ * [Ecto.Query] Allow any data structure that implements the Enumerable protocol on the right side of `in`
|
16
|
+ * [Ecto.Repo] Support 2-arity preload functions that receive ids and the association metadata
|
17
|
+ * [Ecto.Repo] Allow Hot Updates on upsert queries in Postgres by removing duplicate fields during replace_all
|
18
|
+ * [Ecto.Repo] `insert_all` supports queries with only source
|
19
|
+ * [Ecto.Repo] `insert_all` supports queries with the update syntax
|
20
|
+ * [Ecto.Repo] Support `:allow_stale` on Repo struct/changeset operations
|
21
|
+ * [Ecto.Schema] Allow schema fields to be read-only via `:writable` option
|
22
|
+ * [Ecto.Schema] Add `:defaults_to_struct` option to `embeds_one`
|
23
|
+ * [Ecto.Schema] Support `:duration` type which maps to Elixir v1.17 duration
|
24
|
+ * [Ecto.Type] Bubble up custom cast errors of the inner type for `{:map, type}` and `{:array, type}`
|
25
|
+ * [Ecto.Type] Add `Ecto.Type.cast!/2`
|
26
|
+
|
27
|
+ ### Bug fixes
|
28
|
+
|
29
|
+ * [Ecto.Query] Ignore query prefix in CTE sources
|
30
|
+ * [Ecto.Query] Fix a bug of `preload` when a through association is used in a join and has a nested separate query preload. Now the association chain is no longer preloaded and we simply preload directly onto the loaded through association.
|
31
|
+ * [Ecto.Query] Validate `:prefix` is a string/binary, warn otherwise
|
32
|
+ * [Ecto.Query] Fix inspection when select has `map/struct` modifiers
|
33
|
+ * [Ecto.Query] Disable query cache for `values` lists
|
34
|
+ * [Ecto.Repo] Convert fields to their sources in `insert_all`
|
35
|
+ * [Ecto.Repo] Raise if empty list is given to `{:replace, fields}`
|
36
|
+ * [Ecto.Repo] Validate `:prefix` is a string/binary, warn otherwise
|
37
|
+ * [Ecto.Repo] Remove compile dependency on `:preload_order` MFA in `has_many`
|
38
|
+
|
39
|
+ ### Adapter changes
|
40
|
+
|
41
|
+ * `distinct`, `group_by`, `order_by` and `window` expressions use the new `Ecto.Query.ByExpr`
|
42
|
+ struct rather than the old `Ecto.Query.QueryExpr` struct
|
43
|
+
|
44
|
+ ### Potential incompatibilities
|
45
|
+
|
46
|
+ * [Ecto.ParameterizedType] Parameterized types are now represented internally as `{:parameterized, {mod, state}}`. While this representation is private, projects may have been relying on it, and therefore they need to adapt accordingly. Use `Ecto.ParameterizedType.init/2` to instantiate parameterized types.
|
47
|
+ * [Ecto.Query] Drop `:array_join` join type. It was added for Clickhouse support but it is no longer used
|
48
|
+
|
3
49
|
## v3.11.2 (2024-03-07)
|
4
50
|
|
5
51
|
### Bug fixes
|
changed
README.md
|
@@ -77,7 +77,7 @@ Ecto is commonly used to interact with databases, such as PostgreSQL and MySQL v
|
77
77
|
|
78
78
|
See the [getting started guide](https://hexdocs.pm/ecto/getting-started.html) and the [online documentation](https://hexdocs.pm/ecto) for more information. Other resources available are:
|
79
79
|
|
80
|
- * [Programming Ecto](https://pragprog.com/book/wmecto/programming-ecto), by Darin Wilson and Eric Meadows-Jönsson, which guides you from fundamentals up to advanced concepts
|
80
|
+ * [Programming Ecto](https://pragprog.com/titles/wmecto/programming-ecto/), by Darin Wilson and Eric Meadows-Jönsson, which guides you from fundamentals up to advanced concepts
|
81
81
|
|
82
82
|
* [The Little Ecto Cookbook](https://dashbit.co/ebooks/the-little-ecto-cookbook), a free ebook by Dashbit, which is a curation of the existing Ecto guides with some extra contents
|
83
83
|
|
|
@@ -129,12 +129,12 @@ defmodule MyApp.Repo do
|
129
129
|
|
130
130
|
| Branch | Support |
|
131
131
|
| ----------------- | ------------------------ |
|
132
|
- | v3.11 | Bug fixes |
|
132
|
+ | v3.12 | Bug fixes |
|
133
|
+ | v3.11 | Security patches only |
|
133
134
|
| v3.10 | Security patches only |
|
134
135
|
| v3.9 | Security patches only |
|
135
136
|
| v3.8 | Security patches only |
|
136
|
- | v3.7 | Security patches only |
|
137
|
- | v3.6 and earlier | Unsupported |
|
137
|
+ | v3.7 and earlier | Unsupported |
|
138
138
|
|
139
139
|
With version 3.0, Ecto API has become stable. Our main focus is on providing
|
140
140
|
bug fixes and incremental changes.
|
changed
hex_metadata.config
|
@@ -1,6 +1,6 @@
|
1
1
|
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/elixir-ecto/ecto">>}]}.
|
2
2
|
{<<"name">>,<<"ecto">>}.
|
3
|
- {<<"version">>,<<"3.11.2">>}.
|
3
|
+ {<<"version">>,<<"3.12.0">>}.
|
4
4
|
{<<"description">>,
|
5
5
|
<<"A toolkit for data mapping and language integrated query for Elixir">>}.
|
6
6
|
{<<"elixir">>,<<"~> 1.11">>}.
|
changed
integration_test/cases/joins.exs
|
@@ -589,4 +589,84 @@ defmodule Ecto.Integration.JoinsTest do
|
589
589
|
assert [post] = TestRepo.all(query)
|
590
590
|
assert post.post_user_composite_pk
|
591
591
|
end
|
592
|
+
|
593
|
+ test "joining a through association with a nested preloads" do
|
594
|
+ post = TestRepo.insert!(%Post{title: "1"})
|
595
|
+ user = TestRepo.insert!(%User{name: "1"})
|
596
|
+ TestRepo.insert!(%Comment{text: "1", post_id: post.id})
|
597
|
+ TestRepo.insert!(%Permalink{post_id: post.id, user_id: user.id})
|
598
|
+
|
599
|
+ query =
|
600
|
+ from c in Comment,
|
601
|
+ join: pp in assoc(c, :post_permalink),
|
602
|
+ join: u in assoc(pp, :user),
|
603
|
+ preload: [post_permalink: {pp, [:post, user: u]}]
|
604
|
+
|
605
|
+ [comment] = TestRepo.all(query)
|
606
|
+
|
607
|
+ assert not Ecto.assoc_loaded?(comment.post)
|
608
|
+ assert %Permalink{user: %User{}, post: %Post{}} = comment.post_permalink
|
609
|
+ end
|
610
|
+
|
611
|
+ test "joining multiple through associations with a nested preloads" do
|
612
|
+ post = TestRepo.insert!(%Post{title: "1"})
|
613
|
+ user = TestRepo.insert!(%User{name: "1"})
|
614
|
+ TestRepo.insert!(%Comment{text: "1", post_id: post.id, author_id: user.id})
|
615
|
+ TestRepo.insert!(%Permalink{post_id: post.id, user_id: user.id})
|
616
|
+
|
617
|
+ query =
|
618
|
+ from c in Comment,
|
619
|
+ join: pp in assoc(c, :post_permalink),
|
620
|
+ join: ap in assoc(c, :author_permalink),
|
621
|
+ join: u1 in assoc(pp, :user),
|
622
|
+ join: u2 in assoc(ap, :user),
|
623
|
+ preload: [post_permalink: {pp, [:post, user: u1]}, author_permalink: {ap, [:post, user: u2]}]
|
624
|
+
|
625
|
+ [comment] = TestRepo.all(query)
|
626
|
+
|
627
|
+ assert not Ecto.assoc_loaded?(comment.post)
|
628
|
+ assert not Ecto.assoc_loaded?(comment.author)
|
629
|
+ assert %Permalink{user: %User{}, post: %Post{}} = comment.post_permalink
|
630
|
+ assert %Permalink{user: %User{}, post: %Post{}} = comment.author_permalink
|
631
|
+ end
|
632
|
+
|
633
|
+ test "joining nested through associations with a nested preloads" do
|
634
|
+ user = TestRepo.insert!(%User{name: "1"})
|
635
|
+ post = TestRepo.insert!(%Post{title: "1", author_id: user.id})
|
636
|
+ TestRepo.insert!(%Comment{text: "1", post_id: post.id})
|
637
|
+ TestRepo.insert!(%Permalink{post_id: post.id, user_id: user.id})
|
638
|
+
|
639
|
+ query =
|
640
|
+ from c in Comment,
|
641
|
+ join: pp in assoc(c, :post_permalink),
|
642
|
+ join: up in assoc(pp, :user_posts),
|
643
|
+ preload: [post_permalink: {pp, [:post, user_posts: {up, :comments}]}]
|
644
|
+
|
645
|
+ [comment] = TestRepo.all(query)
|
646
|
+
|
647
|
+ assert not Ecto.assoc_loaded?(comment.post)
|
648
|
+ assert %Permalink{post: %Post{}, user_posts: [%Post{}]} = comment.post_permalink
|
649
|
+ assert not Ecto.assoc_loaded?(comment.post_permalink.user)
|
650
|
+ end
|
651
|
+
|
652
|
+ test "joining and preloading through a subquery" do
|
653
|
+ %{id: p_id} = TestRepo.insert!(%Post{})
|
654
|
+ %{id: c1_id} = TestRepo.insert!(%Comment{post_id: p_id})
|
655
|
+ %{id: c2_id} = TestRepo.insert!(%Comment{post_id: p_id})
|
656
|
+
|
657
|
+ q =
|
658
|
+ from p1 in Post,
|
659
|
+ left_join: u in User,
|
660
|
+ on: p1.author_id == u.id,
|
661
|
+ inner_join: c in subquery(from c in Comment),
|
662
|
+ on: p1.id == c.post_id,
|
663
|
+ join: p2 in Post,
|
664
|
+ on: c.post_id == p2.id,
|
665
|
+ preload: [author: u, force_comments: {c, post: p2}]
|
666
|
+
|
667
|
+ assert [%Post{id: ^p_id, force_comments: comments}] = TestRepo.all(q)
|
668
|
+ [comment1, comment2] = Enum.sort_by(comments, & &1.id)
|
669
|
+ assert %Comment{id: ^c1_id, post: %Post{id: ^p_id}} = comment1
|
670
|
+ assert %Comment{id: ^c2_id, post: %Post{id: ^p_id}} = comment2
|
671
|
+ end
|
592
672
|
end
|
changed
integration_test/cases/preload.exs
|
@@ -329,7 +329,7 @@ defmodule Ecto.Integration.PreloadTest do
|
329
329
|
|
330
330
|
## With queries
|
331
331
|
|
332
|
- test "preload with function" do
|
332
|
+ test "preload with 1-arity function" do
|
333
333
|
p1 = TestRepo.insert!(%Post{title: "1"})
|
334
334
|
p2 = TestRepo.insert!(%Post{title: "2"})
|
335
335
|
p3 = TestRepo.insert!(%Post{title: "3"})
|
|
@@ -347,6 +347,23 @@ defmodule Ecto.Integration.PreloadTest do
|
347
347
|
assert [] = pe3.comments
|
348
348
|
end
|
349
349
|
|
350
|
+ test "preload with 2-arity function" do
|
351
|
+ p = TestRepo.insert!(%Post{title: "1"})
|
352
|
+ c1 = TestRepo.insert!(%Comment{post_id: p.id})
|
353
|
+ c2 = TestRepo.insert!(%Comment{post_id: p.id})
|
354
|
+
|
355
|
+ # making a simple preloader so that it works across all adapters
|
356
|
+ preloader = fn parent_ids, assoc ->
|
357
|
+ %{related_key: related_key, queryable: queryable} = assoc
|
358
|
+
|
359
|
+ from(q in queryable, where: field(q, ^related_key) in ^parent_ids, order_by: q.id)
|
360
|
+ |> TestRepo.all()
|
361
|
+ end
|
362
|
+
|
363
|
+ assert p = TestRepo.preload(p, comments: preloader)
|
364
|
+ assert [^c1, ^c2] = p.comments
|
365
|
+ end
|
366
|
+
|
350
367
|
test "preload many_to_many with function" do
|
351
368
|
p1 = TestRepo.insert!(%Post{title: "1"})
|
352
369
|
p2 = TestRepo.insert!(%Post{title: "2"})
|
|
@@ -471,6 +488,19 @@ defmodule Ecto.Integration.PreloadTest do
|
471
488
|
[%{id: u1.id}, %{id: u2.id}, %{id: u3.id}, %{id: u4.id}]
|
472
489
|
end
|
473
490
|
|
491
|
+ test "preload into a subquery source" do
|
492
|
+ %{id: p_id} = TestRepo.insert!(%Post{})
|
493
|
+ %{id: c_id} = TestRepo.insert!(%Comment{post_id: p_id})
|
494
|
+
|
495
|
+ q =
|
496
|
+ from c in subquery(from c in Comment),
|
497
|
+ join: p in Post,
|
498
|
+ on: c.post_id == p.id,
|
499
|
+ preload: [post: p]
|
500
|
+
|
501
|
+ assert [%Comment{id: ^c_id, post: %Post{id: ^p_id}}] = TestRepo.all(q)
|
502
|
+ end
|
503
|
+
|
474
504
|
## With take
|
475
505
|
|
476
506
|
test "preload with take" do
|
changed
integration_test/cases/repo.exs
|
@@ -961,6 +961,14 @@ defmodule Ecto.Integration.RepoTest do
|
961
961
|
|
962
962
|
assert %Post{title: ^expected_title} = TestRepo.get(Post, expected_id)
|
963
963
|
end
|
964
|
+
|
965
|
+ test "insert_all with query and source field" do
|
966
|
+ %{id: post_id} = TestRepo.insert!(%Post{})
|
967
|
+ TestRepo.insert!(%Permalink{url: "url", title: "title"})
|
968
|
+
|
969
|
+ source = from p in Permalink, select: %{url: p.title, post_id: ^post_id}
|
970
|
+ assert {1, _} = TestRepo.insert_all(Permalink, source)
|
971
|
+ end
|
964
972
|
end
|
965
973
|
|
966
974
|
@tag :invalid_prefix
|
|
@@ -1347,6 +1355,12 @@ defmodule Ecto.Integration.RepoTest do
|
1347
1355
|
assert [%Post{title: "new title", visits: 13}] =
|
1348
1356
|
TestRepo.all(from p in Post, select: %Post{p | title: "new title"})
|
1349
1357
|
|
1358
|
+ assert [%Post{:title => "1", visits: -1}] =
|
1359
|
+ TestRepo.all(from p in Post, select: %{p | visits: ^"-1"})
|
1360
|
+
|
1361
|
+ assert [%Post{title: "1", visits: -1}] =
|
1362
|
+ TestRepo.all(from p in Post, select: %Post{p | visits: ^"-1"})
|
1363
|
+
|
1350
1364
|
assert_raise KeyError, fn ->
|
1351
1365
|
TestRepo.all(from p in Post, select: %{p | unknown: "new title"})
|
1352
1366
|
end
|
changed
integration_test/cases/type.exs
|
@@ -1,7 +1,7 @@
|
1
1
|
defmodule Ecto.Integration.TypeTest do
|
2
2
|
use Ecto.Integration.Case, async: Application.compile_env(:ecto, :async_integration_tests, true)
|
3
3
|
|
4
|
- alias Ecto.Integration.{Comment, Custom, Item, ItemColor, Order, Post, User, Tag, Usec}
|
4
|
+ alias Ecto.Integration.{Bitstring, Comment, Custom, Item, ItemColor, Order, Post, User, Tag, Usec}
|
5
5
|
alias Ecto.Integration.TestRepo
|
6
6
|
import Ecto.Query
|
7
7
|
|
|
@@ -67,6 +67,66 @@ defmodule Ecto.Integration.TypeTest do
|
67
67
|
assert [^datetime] = TestRepo.all(from u in Usec, where: u.utc_datetime_usec == ^datetime, select: u.utc_datetime_usec)
|
68
68
|
end
|
69
69
|
|
70
|
+ @tag :bitstring_type
|
71
|
+ test "bitstring type" do
|
72
|
+ bitstring = <<2::3>>
|
73
|
+
|
74
|
+ TestRepo.insert!(%Bitstring{bs: bitstring, bs_with_size: <<5::10>>})
|
75
|
+
|
76
|
+ # Bitstrings
|
77
|
+ assert [^bitstring] = TestRepo.all(from p in Bitstring, where: p.bs == ^bitstring, select: p.bs)
|
78
|
+ assert [^bitstring] = TestRepo.all(from p in Bitstring, where: p.bs == <<2::3>>, select: p.bs)
|
79
|
+
|
80
|
+ assert [<<42::6>>] = TestRepo.all(from p in Bitstring, limit: 1, select: p.bs_with_default)
|
81
|
+ end
|
82
|
+
|
83
|
+ if Code.ensure_loaded?(Duration) do
|
84
|
+ @tag :duration_type
|
85
|
+ test "duration type" do
|
86
|
+ duration = %Duration{year: 1, month: 1, second: 1, microsecond: {100, 6}}
|
87
|
+
|
88
|
+ struct = %Ecto.Integration.Duration{
|
89
|
+ dur: duration,
|
90
|
+ dur_with_fields: duration,
|
91
|
+ dur_with_precision: duration,
|
92
|
+ dur_with_fields_and_precision: duration
|
93
|
+ }
|
94
|
+
|
95
|
+ TestRepo.insert!(struct)
|
96
|
+
|
97
|
+ persisted_duration =
|
98
|
+ from(d in Ecto.Integration.Duration, where: d.dur == ^duration)
|
99
|
+ |> TestRepo.one()
|
100
|
+
|
101
|
+ assert persisted_duration.dur == duration
|
102
|
+
|
103
|
+ # `:field` option set to MONTH so it ignores all units lower than `:month`
|
104
|
+ assert persisted_duration.dur_with_fields == %Duration{
|
105
|
+ year: 1,
|
106
|
+ month: 1,
|
107
|
+ microsecond: {0, 6}
|
108
|
+ }
|
109
|
+
|
110
|
+ assert persisted_duration.dur_with_precision == %Duration{
|
111
|
+ year: 1,
|
112
|
+ month: 1,
|
113
|
+ second: 1,
|
114
|
+ microsecond: {100, 4}
|
115
|
+ }
|
116
|
+
|
117
|
+ # `:field` option is set to HOUR TO SECOND so it ignores all units lower than `:second`
|
118
|
+ assert persisted_duration.dur_with_fields_and_precision == %Duration{
|
119
|
+ year: 1,
|
120
|
+ month: 1,
|
121
|
+ second: 1,
|
122
|
+ microsecond: {0, 1}
|
123
|
+ }
|
124
|
+
|
125
|
+ # `:default set in migration`
|
126
|
+ assert persisted_duration.dur_with_default == %Duration{month: 10, microsecond: {0, 6}}
|
127
|
+ end
|
128
|
+ end
|
129
|
+
|
70
130
|
@tag :select_not
|
71
131
|
test "primitive types boolean negate" do
|
72
132
|
TestRepo.insert!(%Post{public: true})
|
changed
integration_test/support/schemas.exs
|
@@ -97,6 +97,7 @@ defmodule Ecto.Integration.Comment do
|
97
97
|
belongs_to :post, Ecto.Integration.Post
|
98
98
|
belongs_to :author, Ecto.Integration.User
|
99
99
|
has_one :post_permalink, through: [:post, :permalink]
|
100
|
+ has_one :author_permalink, through: [:author, :permalink]
|
100
101
|
end
|
101
102
|
|
102
103
|
def changeset(schema, params) do
|
|
@@ -123,6 +124,7 @@ defmodule Ecto.Integration.Permalink do
|
123
124
|
belongs_to :update_post, Ecto.Integration.Post, on_replace: :update, foreign_key: :post_id, define_field: false
|
124
125
|
belongs_to :user, Ecto.Integration.User
|
125
126
|
has_many :post_comments_authors, through: [:post, :comments_authors]
|
127
|
+ has_many :user_posts, through: [:user, :posts]
|
126
128
|
end
|
127
129
|
|
128
130
|
def changeset(schema, params) do
|
|
@@ -385,3 +387,37 @@ defmodule Ecto.Integration.ArrayLogging do
|
385
387
|
timestamps()
|
386
388
|
end
|
387
389
|
end
|
390
|
+
|
391
|
+ defmodule Ecto.Integration.Bitstring do
|
392
|
+ @moduledoc """
|
393
|
+ This module is used to test:
|
394
|
+
|
395
|
+ * Bitstring type
|
396
|
+
|
397
|
+ """
|
398
|
+ use Ecto.Integration.Schema
|
399
|
+
|
400
|
+ schema "bitstrings" do
|
401
|
+ field :bs, :bitstring
|
402
|
+ field :bs_with_default, :bitstring
|
403
|
+ field :bs_with_size, :bitstring
|
404
|
+ end
|
405
|
+ end
|
406
|
+
|
407
|
+ if Code.ensure_loaded?(Duration) do
|
408
|
+ defmodule Ecto.Integration.Duration do
|
409
|
+ @moduledoc """
|
410
|
+ This module is used to test:
|
411
|
+ * Duration type
|
412
|
+ """
|
413
|
+ use Ecto.Integration.Schema
|
414
|
+
|
415
|
+ schema "durations" do
|
416
|
+ field :dur, :duration
|
417
|
+ field :dur_with_fields, :duration
|
418
|
+ field :dur_with_precision, :duration
|
419
|
+ field :dur_with_fields_and_precision, :duration
|
420
|
+ field :dur_with_default, :duration
|
421
|
+ end
|
422
|
+ end
|
423
|
+ end
|
changed
lib/ecto.ex
|
@@ -697,7 +697,9 @@ defmodule Ecto do
|
697
697
|
|
698
698
|
The third argument `format` is the format the data has been dumped as. For
|
699
699
|
example, databases may dump embedded to `:json`, this function allows such
|
700
|
- dumped data to be put back into the schemas.
|
700
|
+ dumped data to be put back into the schemas. If custom types are used,
|
701
|
+ Ecto will invoke the `c:Ecto.Type.embed_as/1` callback to decide if the data
|
702
|
+ should be loaded using `cast` or `load`.
|
701
703
|
|
702
704
|
Fields that are not present in the schema (or `types` map) are ignored.
|
703
705
|
If any of the values has invalid type, an error is raised.
|
changed
lib/ecto/adapter.ex
|
@@ -45,7 +45,7 @@ defmodule Ecto.Adapter do
|
45
45
|
calls to the datastore to share cache information:
|
46
46
|
|
47
47
|
Repo.checkout(fn ->
|
48
|
- for _ <- 100 do
|
48
|
+ for _ <- 1..100 do
|
49
49
|
Repo.insert!(%Post{})
|
50
50
|
end
|
51
51
|
end)
|
changed
lib/ecto/adapter/queryable.ex
|
@@ -50,7 +50,7 @@ defmodule Ecto.Adapter.Queryable do
|
50
50
|
Commands invoked to prepare a query.
|
51
51
|
|
52
52
|
It is used on `c:Ecto.Repo.all/2`, `c:Ecto.Repo.update_all/3`,
|
53
|
- and `c:Ecto.Repo.delete_all/2`. If returns a tuple, saying if
|
53
|
+ and `c:Ecto.Repo.delete_all/2`. It returns a tuple, indicating if
|
54
54
|
this query can be cached or not, and the `prepared` query.
|
55
55
|
The `prepared` query is any term that will be passed to the
|
56
56
|
adapter's `c:execute/5`.
|
changed
lib/ecto/adapter/transaction.ex
|
@@ -9,7 +9,7 @@ defmodule Ecto.Adapter.Transaction do
|
9
9
|
Runs the given function inside a transaction.
|
10
10
|
|
11
11
|
Returns `{:ok, value}` if the transaction was successful where `value`
|
12
|
- is the value return by the function or `{:error, value}` if the transaction
|
12
|
+ is the value returned by the function or `{:error, value}` if the transaction
|
13
13
|
was rolled back where `value` is the value given to `rollback/1`.
|
14
14
|
"""
|
15
15
|
@callback transaction(adapter_meta, options :: Keyword.t(), function :: fun) ::
|
changed
lib/ecto/changeset.ex
|
@@ -125,7 +125,16 @@ defmodule Ecto.Changeset do
|
125
125
|
|
126
126
|
When applying changes using `cast/4`, an empty value will be automatically
|
127
127
|
converted to the field's default value. If the field is an array type, any
|
128
|
- empty value inside the array will be removed.
|
128
|
+ empty value inside the array will be removed. When a plain map is used in
|
129
|
+ the data portion of a schemaless changeset, every field's default value is
|
130
|
+ considered to be `nil`. For example:
|
131
|
+
|
132
|
+ iex> data = %{name: "Bob"}
|
133
|
+ iex> types = %{name: :string}
|
134
|
+ iex> params = %{name: ""}
|
135
|
+ iex> changeset = Ecto.Changeset.cast({data, types}, params, Map.keys(types))
|
136
|
+ iex> changeset.changes
|
137
|
+ %{name: nil}
|
129
138
|
|
130
139
|
Empty values are stored as a list in the changeset's `:empty_values` field.
|
131
140
|
The list contains elements of type `t:empty_value/0`. Those are either values,
|
|
@@ -351,7 +360,7 @@ defmodule Ecto.Changeset do
|
351
360
|
constraints: [],
|
352
361
|
filters: %{},
|
353
362
|
action: nil,
|
354
|
- types: nil,
|
363
|
+ types: %{},
|
355
364
|
empty_values: @empty_values,
|
356
365
|
repo: nil,
|
357
366
|
repo_opts: []
|
|
@@ -367,10 +376,10 @@ defmodule Ecto.Changeset do
|
367
376
|
prepare: [(t -> t)],
|
368
377
|
errors: [{atom, error}],
|
369
378
|
constraints: [constraint],
|
370
|
- validations: [{atom, term}],
|
379
|
+ validations: [validation],
|
371
380
|
filters: %{optional(atom) => term},
|
372
381
|
action: action,
|
373
|
- types: nil | %{atom => Ecto.Type.t() | {:assoc, term()} | {:embed, term()}}
|
382
|
+ types: types
|
374
383
|
}
|
375
384
|
|
376
385
|
@type t :: t(Ecto.Schema.t() | map | nil)
|
|
@@ -385,8 +394,9 @@ defmodule Ecto.Changeset do
|
385
394
|
error_type: atom
|
386
395
|
}
|
387
396
|
@type data :: map()
|
388
|
- @type types :: map()
|
397
|
+ @type types :: %{atom => Ecto.Type.t() | {:assoc, term()} | {:embed, term()}}
|
389
398
|
@type traverse_result :: %{atom => [term] | traverse_result}
|
399
|
+ @type validation :: {atom, term}
|
390
400
|
|
391
401
|
@typedoc """
|
392
402
|
A possible value that you can pass to the `:empty_values` option.
|
|
@@ -470,10 +480,6 @@ defmodule Ecto.Changeset do
|
470
480
|
change(%Changeset{data: data, types: Enum.into(types, %{}), valid?: true}, changes)
|
471
481
|
end
|
472
482
|
|
473
|
- def change(%Changeset{types: nil}, _changes) do
|
474
|
- raise ArgumentError, "changeset does not have types information"
|
475
|
- end
|
476
|
-
|
477
483
|
def change(%Changeset{changes: changes, types: types} = changeset, new_changes)
|
478
484
|
when is_map(new_changes) or is_list(new_changes) do
|
479
485
|
{changes, errors, valid?} =
|
|
@@ -729,10 +735,6 @@ defmodule Ecto.Changeset do
|
729
735
|
cast(data, types, %{}, params, permitted, opts)
|
730
736
|
end
|
731
737
|
|
732
|
- def cast(%Changeset{types: nil}, _params, _permitted, _opts) do
|
733
|
- raise ArgumentError, "changeset does not have types information"
|
734
|
- end
|
735
|
-
|
736
738
|
def cast(%Changeset{} = changeset, params, permitted, opts) do
|
737
739
|
%{changes: changes, data: data, types: types, empty_values: empty_values} = changeset
|
738
740
|
|
|
@@ -1025,7 +1027,8 @@ defmodule Ecto.Changeset do
|
1025
1027
|
|
1026
1028
|
* If there is an associated child with an ID and its ID is not given
|
1027
1029
|
as parameter, the `:on_replace` callback for that association will
|
1028
|
- be invoked (see the "On replace" section on the module documentation)
|
1030
|
+ be invoked (see the ["On replace" section](#module-the-on_replace-option)
|
1031
|
+ on the module documentation)
|
1029
1032
|
|
1030
1033
|
If two or more addresses have the same IDs, Ecto will consider that an
|
1031
1034
|
error and add an error to the changeset saying that there are duplicate
|
|
@@ -1076,7 +1079,7 @@ defmodule Ecto.Changeset do
|
1076
1079
|
of `cast_assoc/3`. This opens up the possibility to work on a subset of the data,
|
1077
1080
|
instead of all associations in the database.
|
1078
1081
|
|
1079
|
- Taking the initial example of users having addresses imagine those addresses
|
1082
|
+ Taking the initial example of users having addresses, imagine those addresses
|
1080
1083
|
are set up to belong to a country. If you want to allow users to bulk edit all
|
1081
1084
|
addresses that belong to a single country, you can do so by changing the preload
|
1082
1085
|
query:
|
|
@@ -1193,7 +1196,17 @@ defmodule Ecto.Changeset do
|
1193
1196
|
`cast_assoc/3` for more information
|
1194
1197
|
|
1195
1198
|
"""
|
1196
|
- def cast_assoc(changeset, name, opts \\ []) when is_atom(name) do
|
1199
|
+ @spec cast_assoc(t, atom, Keyword.t()) :: t
|
1200
|
+ def cast_assoc(changeset, name, opts \\ [])
|
1201
|
+
|
1202
|
+ def cast_assoc(%Changeset{data: %{} = data}, _name, _opts)
|
1203
|
+ when not is_map_key(data, :__meta__) do
|
1204
|
+ raise ArgumentError,
|
1205
|
+ "cast_assoc/3 cannot be used to cast associations into embedded schemas or schemaless changesets. " <>
|
1206
|
+ "Please modify the association independently."
|
1207
|
+ end
|
1208
|
+
|
1209
|
+ def cast_assoc(changeset, name, opts) when is_atom(name) do
|
1197
1210
|
cast_relation(:assoc, changeset, name, opts)
|
1198
1211
|
end
|
1199
1212
|
|
|
@@ -1240,6 +1253,7 @@ defmodule Ecto.Changeset do
|
1240
1253
|
`cast_assoc/3` for more information
|
1241
1254
|
|
1242
1255
|
"""
|
1256
|
+ @spec cast_embed(t, atom, Keyword.t()) :: t
|
1243
1257
|
def cast_embed(changeset, name, opts \\ []) when is_atom(name) do
|
1244
1258
|
cast_relation(:embed, changeset, name, opts)
|
1245
1259
|
end
|
|
@@ -1284,7 +1298,7 @@ defmodule Ecto.Changeset do
|
1284
1298
|
|
1285
1299
|
{:error, {message, meta}} ->
|
1286
1300
|
meta = [validation: type] ++ meta
|
1287
|
- error = {key, {message(opts, :invalid_message, message), meta}}
|
1301
|
+ error = {key, message(opts, :invalid_message, message, meta)}
|
1288
1302
|
%{changeset | errors: [error | changeset.errors], valid?: false}
|
1289
1303
|
|
1290
1304
|
# ignore or ok with change == original
|
|
@@ -1391,7 +1405,7 @@ defmodule Ecto.Changeset do
|
1391
1405
|
|
1392
1406
|
if required? and Relation.empty?(relation, current_changes) do
|
1393
1407
|
errors = [
|
1394
|
- {name, {message(opts, :required_message, "can't be blank"), [validation: :required]}}
|
1408
|
+ {name, message(opts, :required_message, "can't be blank", validation: :required)}
|
1395
1409
|
| errors
|
1396
1410
|
]
|
1397
1411
|
|
|
@@ -1502,10 +1516,10 @@ defmodule Ecto.Changeset do
|
1502
1516
|
|
1503
1517
|
defp cast_merge(cs1, cs2) do
|
1504
1518
|
new_params = (cs1.params || cs2.params) && Map.merge(cs1.params || %{}, cs2.params || %{})
|
1519
|
+ new_types = Map.merge(cs1.types, cs2.types)
|
1505
1520
|
new_changes = Map.merge(cs1.changes, cs2.changes)
|
1506
1521
|
new_errors = Enum.uniq(cs1.errors ++ cs2.errors)
|
1507
1522
|
new_required = Enum.uniq(cs1.required ++ cs2.required)
|
1508
|
- new_types = cs1.types || cs2.types
|
1509
1523
|
new_valid? = cs1.valid? and cs2.valid?
|
1510
1524
|
|
1511
1525
|
%{
|
|
@@ -1675,6 +1689,7 @@ defmodule Ecto.Changeset do
|
1675
1689
|
[%Post{id: 1, title: "world"}]
|
1676
1690
|
|
1677
1691
|
"""
|
1692
|
+ @spec get_assoc(t, atom, :changeset | :struct) :: [t | Ecto.Schema.t()]
|
1678
1693
|
def get_assoc(changeset, name, as \\ :changeset)
|
1679
1694
|
|
1680
1695
|
def get_assoc(%Changeset{} = changeset, name, :struct) do
|
|
@@ -1721,10 +1736,6 @@ defmodule Ecto.Changeset do
|
1721
1736
|
get_relation(:embed, changeset, name)
|
1722
1737
|
end
|
1723
1738
|
|
1724
|
- defp get_relation(_tag, %{types: nil}, _name) do
|
1725
|
- raise ArgumentError, "changeset does not have types information"
|
1726
|
- end
|
1727
|
-
|
1728
1739
|
defp get_relation(tag, %{changes: changes, data: data, types: types}, name) do
|
1729
1740
|
_ = relation!(:get, tag, name, Map.get(types, name))
|
1730
1741
|
|
|
@@ -1809,7 +1820,7 @@ defmodule Ecto.Changeset do
|
1809
1820
|
|
1810
1821
|
The given `function` is invoked with the change value only if there
|
1811
1822
|
is a change for `key`. Once the function is invoked, it behaves as
|
1812
|
- `put_changed/3`.
|
1823
|
+ `put_change/3`.
|
1813
1824
|
|
1814
1825
|
Note that the value of the change can still be `nil` (unless the field
|
1815
1826
|
was marked as required on `validate_required/3`).
|
|
@@ -1864,10 +1875,6 @@ defmodule Ecto.Changeset do
|
1864
1875
|
|
1865
1876
|
"""
|
1866
1877
|
@spec put_change(t, atom, term) :: t
|
1867
|
- def put_change(%Changeset{types: nil}, _key, _value) do
|
1868
|
- raise ArgumentError, "changeset does not have types information"
|
1869
|
- end
|
1870
|
-
|
1871
1878
|
def put_change(%Changeset{data: data, types: types} = changeset, key, value) do
|
1872
1879
|
type = Map.get(types, key)
|
1873
1880
|
|
|
@@ -1981,7 +1988,7 @@ defmodule Ecto.Changeset do
|
1981
1988
|
update or delete them). Different to passing changesets, structs are not
|
1982
1989
|
change tracked in any fashion. In other words, if you change a comment
|
1983
1990
|
struct and give it to `put_assoc/4`, the updates in the struct won't be
|
1984
|
- persisted. You must use changesets instead. `put_assoc/4` with structs
|
1991
|
+ persisted. You must use changesets, keyword lists, or maps instead. `put_assoc/4` with structs
|
1985
1992
|
only takes care of guaranteeing that the comments and the parent data
|
1986
1993
|
are associated. This is extremely useful when associating existing data,
|
1987
1994
|
as we will see in the "Example: Adding tags to a post" section.
|
|
@@ -2027,7 +2034,7 @@ defmodule Ecto.Changeset do
|
2027
2034
|
beginning, which is that there is only one new comment.
|
2028
2035
|
|
2029
2036
|
In cases like above, when you want to work only on a single entry, it is
|
2030
|
- much easier to simply work on the associated directly. For example, we
|
2037
|
+ much easier to simply work on the association directly. For example, we
|
2031
2038
|
could instead set the `post` association in the comment:
|
2032
2039
|
|
2033
2040
|
%Comment{body: "better example"}
|
|
@@ -2093,8 +2100,19 @@ defmodule Ecto.Changeset do
|
2093
2100
|
* The [associations cheatsheet](associations.html)
|
2094
2101
|
* The [Constraints and Upserts guide](constraints-and-upserts.html)
|
2095
2102
|
* The [Polymorphic associations with many to many guide](polymorphic-associations-with-many-to-many.html)
|
2103
|
+
|
2096
2104
|
"""
|
2097
|
- def put_assoc(%Changeset{} = changeset, name, value, opts \\ []) do
|
2105
|
+ @spec put_assoc(t, atom, term, Keyword.t()) :: t
|
2106
|
+ def put_assoc(changeset, name, value, opts \\ [])
|
2107
|
+
|
2108
|
+ def put_assoc(%Changeset{data: %{} = data}, _name, _value, _opts)
|
2109
|
+ when not is_map_key(data, :__meta__) do
|
2110
|
+ raise ArgumentError,
|
2111
|
+ "put_assoc/4 cannot be used to put associations into embedded schemas or schemaless changesets. " <>
|
2112
|
+ "Please modify the association independently."
|
2113
|
+ end
|
2114
|
+
|
2115
|
+ def put_assoc(%Changeset{} = changeset, name, value, opts) do
|
2098
2116
|
put_relation(:assoc, changeset, name, value, opts)
|
2099
2117
|
end
|
2100
2118
|
|
|
@@ -2115,14 +2133,11 @@ defmodule Ecto.Changeset do
|
2115
2133
|
Although this function accepts an `opts` argument, there are no options
|
2116
2134
|
currently supported by `put_embed/4`.
|
2117
2135
|
"""
|
2136
|
+ @spec put_embed(t, atom, term, Keyword.t()) :: t
|
2118
2137
|
def put_embed(%Changeset{} = changeset, name, value, opts \\ []) do
|
2119
2138
|
put_relation(:embed, changeset, name, value, opts)
|
2120
2139
|
end
|
2121
2140
|
|
2122
|
- defp put_relation(_tag, %{types: nil}, _name, _value, _opts) do
|
2123
|
- raise ArgumentError, "changeset does not have types information"
|
2124
|
- end
|
2125
|
-
|
2126
2141
|
defp put_relation(tag, changeset, name, value, _opts) do
|
2127
2142
|
%{data: data, types: types, changes: changes, errors: errors, valid?: valid?} = changeset
|
2128
2143
|
relation = relation!(:put, tag, name, Map.get(types, name))
|
|
@@ -2155,10 +2170,6 @@ defmodule Ecto.Changeset do
|
2155
2170
|
|
2156
2171
|
"""
|
2157
2172
|
@spec force_change(t, atom, term) :: t
|
2158
|
- def force_change(%Changeset{types: nil}, _key, _value) do
|
2159
|
- raise ArgumentError, "changeset does not have types information"
|
2160
|
- end
|
2161
|
-
|
2162
2173
|
def force_change(%Changeset{types: types} = changeset, key, value) do
|
2163
2174
|
case Map.get(types, key) do
|
2164
2175
|
{tag, _} when tag in @relations ->
|
|
@@ -2242,7 +2253,7 @@ defmodule Ecto.Changeset do
|
2242
2253
|
iex> {:error, changeset} = apply_action(changeset, :update)
|
2243
2254
|
%Ecto.Changeset{action: :update}
|
2244
2255
|
"""
|
2245
|
- @spec apply_action(t, atom) :: {:ok, Ecto.Schema.t() | data} | {:error, t}
|
2256
|
+ @spec apply_action(t, action) :: {:ok, Ecto.Schema.t() | data} | {:error, t}
|
2246
2257
|
def apply_action(%Changeset{} = changeset, action) when is_atom(action) do
|
2247
2258
|
if changeset.valid? do
|
2248
2259
|
{:ok, apply_changes(changeset)}
|
|
@@ -2270,7 +2281,7 @@ defmodule Ecto.Changeset do
|
2270
2281
|
|
2271
2282
|
See `apply_action/2` for more information.
|
2272
2283
|
"""
|
2273
|
- @spec apply_action!(t, atom) :: Ecto.Schema.t() | data
|
2284
|
+ @spec apply_action!(t, action) :: Ecto.Schema.t() | data
|
2274
2285
|
def apply_action!(%Changeset{} = changeset, action) do
|
2275
2286
|
case apply_action(changeset, action) do
|
2276
2287
|
{:ok, data} ->
|
|
@@ -2417,7 +2428,7 @@ defmodule Ecto.Changeset do
|
2417
2428
|
@spec validate_change(
|
2418
2429
|
t,
|
2419
2430
|
atom,
|
2420
|
- (atom, term -> [{atom, String.t()} | {atom, {String.t(), Keyword.t()}}])
|
2431
|
+ (atom, term -> [{atom, String.t()} | {atom, error}])
|
2421
2432
|
) :: t
|
2422
2433
|
def validate_change(%Changeset{} = changeset, field, validator) when is_atom(field) do
|
2423
2434
|
%{changes: changes, types: types, errors: errors} = changeset
|
|
@@ -2463,7 +2474,7 @@ defmodule Ecto.Changeset do
|
2463
2474
|
t,
|
2464
2475
|
atom,
|
2465
2476
|
term,
|
2466
|
- (atom, term -> [{atom, String.t()} | {atom, {String.t(), Keyword.t()}}])
|
2477
|
+ (atom, term -> [{atom, String.t()} | {atom, error}])
|
2467
2478
|
) :: t
|
2468
2479
|
def validate_change(
|
2469
2480
|
%Changeset{validations: validations} = changeset,
|
|
@@ -2489,7 +2500,7 @@ defmodule Ecto.Changeset do
|
2489
2500
|
If a field is given to `validate_required/3` but it has not been passed
|
2490
2501
|
as parameter during `cast/3` (i.e. it has not been changed), then
|
2491
2502
|
`validate_required/3` will check for its current value in the data.
|
2492
|
- If the data contains an non-empty value for the field, then no error is
|
2503
|
+ If the data contains a non-empty value for the field, then no error is
|
2493
2504
|
added. This allows developers to use `validate_required/3` to perform
|
2494
2505
|
partial updates. For example, on `insert` all fields would be required,
|
2495
2506
|
because their default values on the data are all `nil`, but on `update`,
|
|
@@ -2507,7 +2518,9 @@ defmodule Ecto.Changeset do
|
2507
2518
|
|
2508
2519
|
## Options
|
2509
2520
|
|
2510
|
- * `:message` - the message on failure, defaults to "can't be blank"
|
2521
|
+ * `:message` - the message on failure, defaults to "can't be blank".
|
2522
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
2523
|
+ when using `traverse_errors/2`.
|
2511
2524
|
|
2512
2525
|
## Examples
|
2513
2526
|
|
|
@@ -2531,8 +2544,12 @@ defmodule Ecto.Changeset do
|
2531
2544
|
%{changeset | required: fields ++ required}
|
2532
2545
|
|
2533
2546
|
_ ->
|
2534
|
- message = message(opts, "can't be blank")
|
2535
|
- new_errors = Enum.map(fields_with_errors, &{&1, {message, [validation: :required]}})
|
2547
|
+ new_errors =
|
2548
|
+ Enum.map(
|
2549
|
+ fields_with_errors,
|
2550
|
+ &{&1, message(opts, "can't be blank", validation: :required)}
|
2551
|
+ )
|
2552
|
+
|
2536
2553
|
changes = Map.drop(changes, fields_with_errors)
|
2537
2554
|
|
2538
2555
|
%{
|
|
@@ -2599,7 +2616,8 @@ defmodule Ecto.Changeset do
|
2599
2616
|
## Options
|
2600
2617
|
|
2601
2618
|
* `:message` - the message in case the constraint check fails,
|
2602
|
- defaults to "has already been taken".
|
2619
|
+ defaults to "has already been taken". Can also be a `{msg, opts}` tuple,
|
2620
|
+ to provide additional options when using `traverse_errors/2`.
|
2603
2621
|
|
2604
2622
|
* `:error_key` - the key to which changeset error will be added when
|
2605
2623
|
check fails, defaults to the first field name of the given list of
|
|
@@ -2700,10 +2718,10 @@ defmodule Ecto.Changeset do
|
2700
2718
|
if repo.exists?(query, repo_opts) do
|
2701
2719
|
error_key = Keyword.get(opts, :error_key, hd(fields))
|
2702
2720
|
|
2703
|
- add_error(changeset, error_key, message(opts, "has already been taken"),
|
2704
|
- validation: :unsafe_unique,
|
2705
|
- fields: fields
|
2706
|
- )
|
2721
|
+ {error_message, keys} =
|
2722
|
+ message(opts, "has already been taken", validation: :unsafe_unique, fields: fields)
|
2723
|
+
|
2724
|
+ add_error(changeset, error_key, error_message, keys)
|
2707
2725
|
else
|
2708
2726
|
changeset
|
2709
2727
|
end
|
|
@@ -2828,7 +2846,9 @@ defmodule Ecto.Changeset do
|
2828
2846
|
|
2829
2847
|
## Options
|
2830
2848
|
|
2831
|
- * `:message` - the message on failure, defaults to "has invalid format"
|
2849
|
+ * `:message` - the message on failure, defaults to "has invalid format".
|
2850
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
2851
|
+ when using `traverse_errors/2`.
|
2832
2852
|
|
2833
2853
|
## Examples
|
2834
2854
|
|
|
@@ -2845,7 +2865,7 @@ defmodule Ecto.Changeset do
|
2845
2865
|
|
2846
2866
|
if value =~ format,
|
2847
2867
|
do: [],
|
2848
|
- else: [{field, {message(opts, "has invalid format"), [validation: :format]}}]
|
2868
|
+ else: [{field, message(opts, "has invalid format", validation: :format)}]
|
2849
2869
|
end)
|
2850
2870
|
end
|
2851
2871
|
|
|
@@ -2854,7 +2874,9 @@ defmodule Ecto.Changeset do
|
2854
2874
|
|
2855
2875
|
## Options
|
2856
2876
|
|
2857
|
- * `:message` - the message on failure, defaults to "is invalid"
|
2877
|
+ * `:message` - the message on failure, defaults to "is invalid".
|
2878
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
2879
|
+ when using `traverse_errors/2`.
|
2858
2880
|
|
2859
2881
|
## Examples
|
2860
2882
|
|
|
@@ -2869,7 +2891,7 @@ defmodule Ecto.Changeset do
|
2869
2891
|
|
2870
2892
|
if Ecto.Type.include?(type, value, data),
|
2871
2893
|
do: [],
|
2872
|
- else: [{field, {message(opts, "is invalid"), [validation: :inclusion, enum: data]}}]
|
2894
|
+ else: [{field, message(opts, "is invalid", validation: :inclusion, enum: data)}]
|
2873
2895
|
end)
|
2874
2896
|
end
|
2875
2897
|
|
|
@@ -2884,7 +2906,9 @@ defmodule Ecto.Changeset do
|
2884
2906
|
|
2885
2907
|
## Options
|
2886
2908
|
|
2887
|
- * `:message` - the message on failure, defaults to "has an invalid entry"
|
2909
|
+ * `:message` - the message on failure, defaults to "has an invalid entry".
|
2910
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
2911
|
+ when using `traverse_errors/2`.
|
2888
2912
|
|
2889
2913
|
## Examples
|
2890
2914
|
|
|
@@ -2908,7 +2932,7 @@ defmodule Ecto.Changeset do
|
2908
2932
|
|
2909
2933
|
case Enum.any?(value, fn element -> not Ecto.Type.include?(element_type, element, data) end) do
|
2910
2934
|
true ->
|
2911
|
- [{field, {message(opts, "has an invalid entry"), [validation: :subset, enum: data]}}]
|
2935
|
+ [{field, message(opts, "has an invalid entry", validation: :subset, enum: data)}]
|
2912
2936
|
|
2913
2937
|
false ->
|
2914
2938
|
[]
|
|
@@ -2921,7 +2945,9 @@ defmodule Ecto.Changeset do
|
2921
2945
|
|
2922
2946
|
## Options
|
2923
2947
|
|
2924
|
- * `:message` - the message on failure, defaults to "is reserved"
|
2948
|
+ * `:message` - the message on failure, defaults to "is reserved".
|
2949
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
2950
|
+ when using `traverse_errors/2`.
|
2925
2951
|
|
2926
2952
|
## Examples
|
2927
2953
|
|
|
@@ -2934,7 +2960,7 @@ defmodule Ecto.Changeset do
|
2934
2960
|
type = Map.fetch!(changeset.types, field)
|
2935
2961
|
|
2936
2962
|
if Ecto.Type.include?(type, value, data),
|
2937
|
- do: [{field, {message(opts, "is reserved"), [validation: :exclusion, enum: data]}}],
|
2963
|
+ do: [{field, message(opts, "is reserved", validation: :exclusion, enum: data)}],
|
2938
2964
|
else: []
|
2939
2965
|
end)
|
2940
2966
|
end
|
|
@@ -2967,6 +2993,8 @@ defmodule Ecto.Changeset do
|
2967
2993
|
* "should have %{count} item(s)"
|
2968
2994
|
* "should have at least %{count} item(s)"
|
2969
2995
|
* "should have at most %{count} item(s)"
|
2996
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
2997
|
+ when using `traverse_errors/2`.
|
2970
2998
|
|
2971
2999
|
## Examples
|
2972
3000
|
|
|
@@ -3029,66 +3057,114 @@ defmodule Ecto.Changeset do
|
3029
3057
|
|
3030
3058
|
defp wrong_length(:string, _length, value, opts),
|
3031
3059
|
do:
|
3032
|
- {message(opts, "should be %{count} character(s)"),
|
3033
|
- count: value, validation: :length, kind: :is, type: :string}
|
3060
|
+ message(opts, "should be %{count} character(s)",
|
3061
|
+ count: value,
|
3062
|
+ validation: :length,
|
3063
|
+ kind: :is,
|
3064
|
+ type: :string
|
3065
|
+ )
|
3034
3066
|
|
3035
3067
|
defp wrong_length(:binary, _length, value, opts),
|
3036
3068
|
do:
|
3037
|
- {message(opts, "should be %{count} byte(s)"),
|
3038
|
- count: value, validation: :length, kind: :is, type: :binary}
|
3069
|
+ message(opts, "should be %{count} byte(s)",
|
3070
|
+ count: value,
|
3071
|
+ validation: :length,
|
3072
|
+ kind: :is,
|
3073
|
+ type: :binary
|
3074
|
+ )
|
3039
3075
|
|
3040
3076
|
defp wrong_length(:list, _length, value, opts),
|
3041
3077
|
do:
|
3042
|
- {message(opts, "should have %{count} item(s)"),
|
3043
|
- count: value, validation: :length, kind: :is, type: :list}
|
3078
|
+ message(opts, "should have %{count} item(s)",
|
3079
|
+ count: value,
|
3080
|
+ validation: :length,
|
3081
|
+ kind: :is,
|
3082
|
+ type: :list
|
3083
|
+ )
|
3044
3084
|
|
3045
3085
|
defp wrong_length(:map, _length, value, opts),
|
3046
3086
|
do:
|
3047
|
- {message(opts, "should have %{count} item(s)"),
|
3048
|
- count: value, validation: :length, kind: :is, type: :map}
|
3087
|
+ message(opts, "should have %{count} item(s)",
|
3088
|
+ count: value,
|
3089
|
+ validation: :length,
|
3090
|
+ kind: :is,
|
3091
|
+ type: :map
|
3092
|
+ )
|
3049
3093
|
|
3050
3094
|
defp too_short(_type, length, value, _opts) when length >= value, do: nil
|
3051
3095
|
|
3052
3096
|
defp too_short(:string, _length, value, opts) do
|
3053
|
- {message(opts, "should be at least %{count} character(s)"),
|
3054
|
- count: value, validation: :length, kind: :min, type: :string}
|
3097
|
+ message(opts, "should be at least %{count} character(s)",
|
3098
|
+ count: value,
|
3099
|
+ validation: :length,
|
3100
|
+ kind: :min,
|
3101
|
+ type: :string
|
3102
|
+ )
|
3055
3103
|
end
|
3056
3104
|
|
3057
3105
|
defp too_short(:binary, _length, value, opts) do
|
3058
|
- {message(opts, "should be at least %{count} byte(s)"),
|
3059
|
- count: value, validation: :length, kind: :min, type: :binary}
|
3106
|
+ message(opts, "should be at least %{count} byte(s)",
|
3107
|
+ count: value,
|
3108
|
+ validation: :length,
|
3109
|
+ kind: :min,
|
3110
|
+ type: :binary
|
3111
|
+ )
|
3060
3112
|
end
|
3061
3113
|
|
3062
3114
|
defp too_short(:list, _length, value, opts) do
|
3063
|
- {message(opts, "should have at least %{count} item(s)"),
|
3064
|
- count: value, validation: :length, kind: :min, type: :list}
|
3115
|
+ message(opts, "should have at least %{count} item(s)",
|
3116
|
+ count: value,
|
3117
|
+ validation: :length,
|
3118
|
+ kind: :min,
|
3119
|
+ type: :list
|
3120
|
+ )
|
3065
3121
|
end
|
3066
3122
|
|
3067
3123
|
defp too_short(:map, _length, value, opts) do
|
3068
|
- {message(opts, "should have at least %{count} item(s)"),
|
3069
|
- count: value, validation: :length, kind: :min, type: :map}
|
3124
|
+ message(opts, "should have at least %{count} item(s)",
|
3125
|
+ count: value,
|
3126
|
+ validation: :length,
|
3127
|
+ kind: :min,
|
3128
|
+ type: :map
|
3129
|
+ )
|
3070
3130
|
end
|
3071
3131
|
|
3072
3132
|
defp too_long(_type, length, value, _opts) when length <= value, do: nil
|
3073
3133
|
|
3074
3134
|
defp too_long(:string, _length, value, opts) do
|
3075
|
- {message(opts, "should be at most %{count} character(s)"),
|
3076
|
- count: value, validation: :length, kind: :max, type: :string}
|
3135
|
+ message(opts, "should be at most %{count} character(s)",
|
3136
|
+ count: value,
|
3137
|
+ validation: :length,
|
3138
|
+ kind: :max,
|
3139
|
+ type: :string
|
3140
|
+ )
|
3077
3141
|
end
|
3078
3142
|
|
3079
3143
|
defp too_long(:binary, _length, value, opts) do
|
3080
|
- {message(opts, "should be at most %{count} byte(s)"),
|
3081
|
- count: value, validation: :length, kind: :max, type: :binary}
|
3144
|
+ message(opts, "should be at most %{count} byte(s)",
|
3145
|
+ count: value,
|
3146
|
+ validation: :length,
|
3147
|
+ kind: :max,
|
3148
|
+ type: :binary
|
3149
|
+ )
|
3082
3150
|
end
|
3083
3151
|
|
3084
3152
|
defp too_long(:list, _length, value, opts) do
|
3085
|
- {message(opts, "should have at most %{count} item(s)"),
|
3086
|
- count: value, validation: :length, kind: :max, type: :list}
|
3153
|
+ message(opts, "should have at most %{count} item(s)",
|
3154
|
+ count: value,
|
3155
|
+ validation: :length,
|
3156
|
+ kind: :max,
|
3157
|
+ type: :list
|
3158
|
+ )
|
3087
3159
|
end
|
3088
3160
|
|
3089
3161
|
defp too_long(:map, _length, value, opts) do
|
3090
|
- {message(opts, "should have at most %{count} item(s)"),
|
3091
|
- count: value, validation: :length, kind: :max, type: :map}
|
3162
|
+ message(opts, "should have at most %{count} item(s)",
|
3163
|
+ count: value,
|
3164
|
+ validation: :length,
|
3165
|
+ kind: :max,
|
3166
|
+ type: :map
|
3167
|
+ )
|
3092
3168
|
end
|
3093
3169
|
|
3094
3170
|
@doc """
|
|
@@ -3109,6 +3185,8 @@ defmodule Ecto.Changeset do
|
3109
3185
|
* "must be greater than or equal to %{number}"
|
3110
3186
|
* "must be equal to %{number}"
|
3111
3187
|
* "must be not equal to %{number}"
|
3188
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
3189
|
+ when using `traverse_errors/2`.
|
3112
3190
|
|
3113
3191
|
## Examples
|
3114
3192
|
|
|
@@ -3121,14 +3199,14 @@ defmodule Ecto.Changeset do
|
3121
3199
|
def validate_number(changeset, field, opts) do
|
3122
3200
|
validate_change(changeset, field, {:number, opts}, fn
|
3123
3201
|
field, value ->
|
3124
|
- {message, opts} = Keyword.pop(opts, :message)
|
3125
|
-
|
3126
3202
|
unless valid_number?(value) do
|
3127
3203
|
raise ArgumentError,
|
3128
3204
|
"expected field `#{field}` to be a decimal, integer, or float, got: #{inspect(value)}"
|
3129
3205
|
end
|
3130
3206
|
|
3131
|
- Enum.find_value(opts, [], fn {spec_key, target_value} ->
|
3207
|
+ opts
|
3208
|
+ |> Keyword.drop([:message])
|
3209
|
+ |> Enum.find_value([], fn {spec_key, target_value} ->
|
3132
3210
|
case Map.fetch(@number_validators, spec_key) do
|
3133
3211
|
{:ok, {spec_function, default_message}} ->
|
3134
3212
|
unless valid_number?(target_value) do
|
|
@@ -3139,10 +3217,11 @@ defmodule Ecto.Changeset do
|
3139
3217
|
compare_numbers(
|
3140
3218
|
field,
|
3141
3219
|
value,
|
3142
|
- message || default_message,
|
3220
|
+ default_message,
|
3143
3221
|
spec_key,
|
3144
3222
|
spec_function,
|
3145
|
- target_value
|
3223
|
+ target_value,
|
3224
|
+ opts
|
3146
3225
|
)
|
3147
3226
|
|
3148
3227
|
:error ->
|
|
@@ -3167,31 +3246,32 @@ defmodule Ecto.Changeset do
|
3167
3246
|
defp compare_numbers(
|
3168
3247
|
field,
|
3169
3248
|
%Decimal{} = value,
|
3170
|
- message,
|
3249
|
+ default_message,
|
3171
3250
|
spec_key,
|
3172
3251
|
_spec_function,
|
3173
|
- %Decimal{} = target_value
|
3252
|
+ %Decimal{} = target_value,
|
3253
|
+ opts
|
3174
3254
|
) do
|
3175
3255
|
result = Decimal.compare(value, target_value)
|
3176
3256
|
|
3177
3257
|
case decimal_compare(result, spec_key) do
|
3178
3258
|
true -> nil
|
3179
|
- false -> [{field, {message, validation: :number, kind: spec_key, number: target_value}}]
|
3259
|
+ false -> [{field, message(opts, default_message, validation: :number, kind: spec_key, number: target_value)}]
|
3180
3260
|
end
|
3181
3261
|
end
|
3182
3262
|
|
3183
|
- defp compare_numbers(field, value, message, spec_key, spec_function, %Decimal{} = target_value) do
|
3184
|
- compare_numbers(field, decimal_new(value), message, spec_key, spec_function, target_value)
|
3263
|
+ defp compare_numbers(field, value, default_message, spec_key, spec_function, %Decimal{} = target_value, opts) do
|
3264
|
+ compare_numbers(field, decimal_new(value), default_message, spec_key, spec_function, target_value, opts)
|
3185
3265
|
end
|
3186
3266
|
|
3187
|
- defp compare_numbers(field, %Decimal{} = value, message, spec_key, spec_function, target_value) do
|
3188
|
- compare_numbers(field, value, message, spec_key, spec_function, decimal_new(target_value))
|
3267
|
+ defp compare_numbers(field, %Decimal{} = value, default_message, spec_key, spec_function, target_value, opts) do
|
3268
|
+ compare_numbers(field, value, default_message, spec_key, spec_function, decimal_new(target_value), opts)
|
3189
3269
|
end
|
3190
3270
|
|
3191
|
- defp compare_numbers(field, value, message, spec_key, spec_function, target_value) do
|
3271
|
+ defp compare_numbers(field, value, default_message, spec_key, spec_function, target_value, opts) do
|
3192
3272
|
case apply(spec_function, [value, target_value]) do
|
3193
3273
|
true -> nil
|
3194
|
- false -> [{field, {message, validation: :number, kind: spec_key, number: target_value}}]
|
3274
|
+ false -> [{field, message(opts, default_message, validation: :number, kind: spec_key, number: target_value)}]
|
3195
3275
|
end
|
3196
3276
|
end
|
3197
3277
|
|
|
@@ -3224,7 +3304,9 @@ defmodule Ecto.Changeset do
|
3224
3304
|
|
3225
3305
|
## Options
|
3226
3306
|
|
3227
|
- * `:message` - the message on failure, defaults to "does not match confirmation"
|
3307
|
+ * `:message` - the message on failure, defaults to "does not match confirmation".
|
3308
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
3309
|
+ when using `traverse_errors/2`.
|
3228
3310
|
* `:required` - boolean, sets whether existence of confirmation parameter
|
3229
3311
|
is required for addition of error. Defaults to false
|
3230
3312
|
|
|
@@ -3253,8 +3335,7 @@ defmodule Ecto.Changeset do
|
3253
3335
|
|
3254
3336
|
%{^error_param => _} ->
|
3255
3337
|
[
|
3256
|
- {error_field,
|
3257
|
- {message(opts, "does not match confirmation"), [validation: :confirmation]}}
|
3338
|
+ {error_field, message(opts, "does not match confirmation", validation: :confirmation)}
|
3258
3339
|
]
|
3259
3340
|
|
3260
3341
|
%{} ->
|
|
@@ -3277,11 +3358,21 @@ defmodule Ecto.Changeset do
|
3277
3358
|
required = Keyword.get(opts, :required, false)
|
3278
3359
|
|
3279
3360
|
if required,
|
3280
|
- do: [{error_field, {message(opts, "can't be blank"), [validation: :required]}}],
|
3361
|
+ do: [{error_field, message(opts, "can't be blank", validation: :required)}],
|
3281
3362
|
else: []
|
3282
3363
|
end
|
3283
3364
|
|
3284
|
- defp message(opts, key \\ :message, default) do
|
3365
|
+ defp message(opts, key \\ :message, default, message_opts) do
|
3366
|
+ case Keyword.get(opts, key, default) do
|
3367
|
+ {message, extra_opts} when is_binary(message) and is_list(extra_opts) ->
|
3368
|
+ {message, Keyword.merge(message_opts, extra_opts)}
|
3369
|
+
|
3370
|
+ message when is_binary(message) ->
|
3371
|
+ {message, message_opts}
|
3372
|
+ end
|
3373
|
+ end
|
3374
|
+
|
3375
|
+ defp constraint_message(opts, key \\ :message, default) do
|
3285
3376
|
Keyword.get(opts, key, default)
|
3286
3377
|
end
|
3287
3378
|
|
|
@@ -3294,7 +3385,9 @@ defmodule Ecto.Changeset do
|
3294
3385
|
|
3295
3386
|
## Options
|
3296
3387
|
|
3297
|
- * `:message` - the message on failure, defaults to "must be accepted"
|
3388
|
+ * `:message` - the message on failure, defaults to "must be accepted".
|
3389
|
+ Can also be a `{msg, opts}` tuple, to provide additional options
|
3390
|
+ when using `traverse_errors/2`.
|
3298
3391
|
|
3299
3392
|
## Examples
|
3300
3393
|
|
|
@@ -3324,7 +3417,7 @@ defmodule Ecto.Changeset do
|
3324
3417
|
|
3325
3418
|
case Ecto.Type.cast(:boolean, value) do
|
3326
3419
|
{:ok, true} -> []
|
3327
|
- _ -> [{field, {message(opts, "must be accepted"), validation: :acceptance}}]
|
3420
|
+ _ -> [{field, message(opts, "must be accepted", validation: :acceptance)}]
|
3328
3421
|
end
|
3329
3422
|
end
|
3330
3423
|
|
|
@@ -3552,9 +3645,10 @@ defmodule Ecto.Changeset do
|
3552
3645
|
`starts_with?` `:name` to this changeset constraint.
|
3553
3646
|
|
3554
3647
|
"""
|
3648
|
+ @spec check_constraint(t, atom, Keyword.t()) :: t
|
3555
3649
|
def check_constraint(changeset, field, opts \\ []) do
|
3556
3650
|
name = opts[:name] || raise ArgumentError, "must supply the name of the constraint"
|
3557
|
- message = message(opts, "is invalid")
|
3651
|
+ message = constraint_message(opts, "is invalid")
|
3558
3652
|
match_type = Keyword.get(opts, :match, :exact)
|
3559
3653
|
|
3560
3654
|
add_constraint(changeset, :check, name, match_type, field, message, :check)
|
|
@@ -3699,7 +3793,7 @@ defmodule Ecto.Changeset do
|
3699
3793
|
|
3700
3794
|
def unique_constraint(changeset, [first_field | _] = fields, opts) do
|
3701
3795
|
name = opts[:name] || unique_index_name(changeset, fields)
|
3702
|
- message = message(opts, "has already been taken")
|
3796
|
+ message = constraint_message(opts, "has already been taken")
|
3703
3797
|
match_type = Keyword.get(opts, :match, :exact)
|
3704
3798
|
error_key = Keyword.get(opts, :error_key, first_field)
|
3705
3799
|
|
|
@@ -3765,7 +3859,7 @@ defmodule Ecto.Changeset do
|
3765
3859
|
def foreign_key_constraint(changeset, field, opts \\ []) do
|
3766
3860
|
name = opts[:name] || "#{get_source(changeset)}_#{get_field_source(changeset, field)}_fkey"
|
3767
3861
|
match_type = Keyword.get(opts, :match, :exact)
|
3768
|
- message = message(opts, "does not exist")
|
3862
|
+ message = constraint_message(opts, "does not exist")
|
3769
3863
|
|
3770
3864
|
add_constraint(changeset, :foreign_key, name, match_type, field, message, :foreign)
|
3771
3865
|
end
|
|
@@ -3823,7 +3917,7 @@ defmodule Ecto.Changeset do
|
3823
3917
|
end
|
3824
3918
|
|
3825
3919
|
match_type = Keyword.get(opts, :match, :exact)
|
3826
|
- message = message(opts, "does not exist")
|
3920
|
+ message = constraint_message(opts, "does not exist")
|
3827
3921
|
|
3828
3922
|
add_constraint(changeset, :foreign_key, name, match_type, assoc, message, :assoc)
|
3829
3923
|
end
|
|
@@ -3879,7 +3973,7 @@ defmodule Ecto.Changeset do
|
3879
3973
|
related: related
|
3880
3974
|
} ->
|
3881
3975
|
{opts[:name] || "#{related.__schema__(:source)}_#{related_key}_fkey",
|
3882
|
- message(opts, no_assoc_message(cardinality))}
|
3976
|
+ constraint_message(opts, no_assoc_message(cardinality))}
|
3883
3977
|
|
3884
3978
|
other ->
|
3885
3979
|
raise ArgumentError,
|
|
@@ -3917,7 +4011,7 @@ defmodule Ecto.Changeset do
|
3917
4011
|
name =
|
3918
4012
|
opts[:name] || "#{get_source(changeset)}_#{get_field_source(changeset, field)}_exclusion"
|
3919
4013
|
|
3920
|
- message = message(opts, "violates an exclusion constraint")
|
4014
|
+ message = constraint_message(opts, "violates an exclusion constraint")
|
3921
4015
|
match_type = Keyword.get(opts, :match, :exact)
|
3922
4016
|
|
3923
4017
|
add_constraint(changeset, :exclusion, name, match_type, field, message, :exclusion)
|
|
@@ -4129,7 +4223,7 @@ defmodule Ecto.Changeset do
|
4129
4223
|
"""
|
4130
4224
|
@spec traverse_validations(
|
4131
4225
|
t,
|
4132
|
- (error -> String.t()) | (Changeset.t(), atom, error -> String.t())
|
4226
|
+ (validation -> String.t()) | (Changeset.t(), atom, validation -> String.t())
|
4133
4227
|
) :: traverse_result
|
4134
4228
|
def traverse_validations(
|
4135
4229
|
%Changeset{validations: validations, changes: changes, types: types} = changeset,
|
|
@@ -4147,8 +4241,9 @@ defimpl Inspect, for: Ecto.Changeset do
|
4147
4241
|
import Inspect.Algebra
|
4148
4242
|
|
4149
4243
|
def inspect(%Ecto.Changeset{data: data} = changeset, opts) do
|
4244
|
+ # The trailing element is skipped later on
|
4150
4245
|
list =
|
4151
|
- for attr <- [:action, :changes, :errors, :data, :valid?] do
|
4246
|
+ for attr <- [:action, :changes, :errors, :data, :valid?, :action] do
|
4152
4247
|
{attr, Map.get(changeset, attr)}
|
4153
4248
|
end
|
4154
4249
|
|
|
@@ -4165,20 +4260,20 @@ defimpl Inspect, for: Ecto.Changeset do
|
4165
4260
|
[]
|
4166
4261
|
end
|
4167
4262
|
|
4168
|
- container_doc("#Ecto.Changeset<", list, ">", opts, fn
|
4169
|
- {:action, action}, opts ->
|
4263
|
+ container_doc("#Ecto.Changeset<", list, ">", %{limit: 5}, fn
|
4264
|
+ {:action, action}, _opts ->
|
4170
4265
|
concat("action: ", to_doc(action, opts))
|
4171
4266
|
|
4172
|
- {:changes, changes}, opts ->
|
4267
|
+ {:changes, changes}, _opts ->
|
4173
4268
|
concat("changes: ", changes |> filter(redacted_fields) |> to_doc(opts))
|
4174
4269
|
|
4175
4270
|
{:data, data}, _opts ->
|
4176
4271
|
concat("data: ", to_struct(data, opts))
|
4177
4272
|
|
4178
|
- {:errors, errors}, opts ->
|
4273
|
+ {:errors, errors}, _opts ->
|
4179
4274
|
concat("errors: ", to_doc(errors, opts))
|
4180
4275
|
|
4181
|
- {:valid?, valid?}, opts ->
|
4276
|
+ {:valid?, valid?}, _opts ->
|
4182
4277
|
concat("valid?: ", to_doc(valid?, opts))
|
4183
4278
|
end)
|
4184
4279
|
end
|
changed
lib/ecto/changeset/relation.ex
|
@@ -469,7 +469,7 @@ defmodule Ecto.Changeset.Relation do
|
469
469
|
Enum.map_reduce(current, {%{}, 0}, fn struct, {acc, counter} ->
|
470
470
|
pks = get_pks.(struct)
|
471
471
|
key = if pks == [], do: map_size(acc), else: pks
|
472
|
- {pks, {Map.put(acc, key, struct), counter+ 1}}
|
472
|
+ {pks, {Map.put(acc, key, struct), counter + 1}}
|
473
473
|
end)
|
474
474
|
|
475
475
|
if map_size(map) != counter do
|
changed
lib/ecto/enum.ex
|
@@ -4,7 +4,7 @@ defmodule Ecto.Enum do
|
4
4
|
|
5
5
|
`Ecto.Enum` must be used whenever you want to keep atom values in a field.
|
6
6
|
Since atoms cannot be persisted to the database, `Ecto.Enum` converts them
|
7
|
- to a string or an integer when writing to the database and converts them back
|
7
|
+ to strings or integers when writing to the database and converts them back
|
8
8
|
to atoms when loading data. It can be used in your schemas as follows:
|
9
9
|
|
10
10
|
# Stored as strings
|
|
@@ -15,7 +15,7 @@ defmodule Ecto.Enum do
|
15
15
|
# Stored as integers
|
16
16
|
field :status, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
|
17
17
|
|
18
|
- Therefore, the type to be used in your migrations for enum fields depend
|
18
|
+ Therefore, the type to be used in your migrations for enum fields depends
|
19
19
|
on the choice above. For the cases above, one would do, respectively:
|
20
20
|
|
21
21
|
add :status, :string
|
|
@@ -38,24 +38,11 @@ defmodule Ecto.Enum do
|
38
38
|
by an atom in the list will be invalid.
|
39
39
|
|
40
40
|
The helper function `mappings/2` returns the mappings for a given schema and
|
41
|
- field, which can be used in places like form drop-downs. For example, given
|
42
|
- the following schema:
|
41
|
+ field, which can be used in places like form drop-downs. See `mappings/2` for
|
42
|
+ examples.
|
43
43
|
|
44
|
- defmodule EnumSchema do
|
45
|
- use Ecto.Schema
|
46
|
-
|
47
|
- schema "my_schema" do
|
48
|
- field :my_enum, Ecto.Enum, values: [:foo, :bar, :baz]
|
49
|
- end
|
50
|
- end
|
51
|
-
|
52
|
- You can call `mappings/2` like this:
|
53
|
-
|
54
|
- Ecto.Enum.mappings(EnumSchema, :my_enum)
|
55
|
- #=> [foo: "foo", bar: "bar", baz: "baz"]
|
56
|
-
|
57
|
- If you want the values only, you can use `Ecto.Enum.values/2`, and if you want
|
58
|
- the dump values only, you can use `Ecto.Enum.dump_values/2`.
|
44
|
+ If you want the values only, you can use `values/2`, and if you want
|
45
|
+ the "dump-able" values only, you can use `dump_values/2`.
|
59
46
|
|
60
47
|
## Embeds
|
61
48
|
|
|
@@ -175,10 +162,17 @@ defmodule Ecto.Enum do
|
175
162
|
|
176
163
|
def cast(data, params) do
|
177
164
|
case params do
|
178
|
- %{on_load: %{^data => as_atom}} -> {:ok, as_atom}
|
179
|
- %{on_dump: %{^data => _}} -> {:ok, data}
|
180
|
- %{on_cast: %{^data => as_atom}} -> {:ok, as_atom}
|
181
|
- _ -> :error
|
165
|
+ %{on_load: %{^data => as_atom}} ->
|
166
|
+ {:ok, as_atom}
|
167
|
+
|
168
|
+ %{on_dump: %{^data => _}} ->
|
169
|
+ {:ok, data}
|
170
|
+
|
171
|
+ %{on_cast: %{^data => as_atom}} ->
|
172
|
+ {:ok, as_atom}
|
173
|
+
|
174
|
+ params ->
|
175
|
+ {:error, validation: :inclusion, enum: Map.keys(params.on_cast)}
|
182
176
|
end
|
183
177
|
end
|
184
178
|
|
|
@@ -213,35 +207,128 @@ defmodule Ecto.Enum do
|
213
207
|
"#Ecto.Enum<values: #{inspect(Keyword.keys(mappings))}>"
|
214
208
|
end
|
215
209
|
|
216
|
- @doc "Returns the possible values for a given schema or types map and field"
|
217
|
- @spec values(map | module, atom) :: [atom()]
|
210
|
+ @doc """
|
211
|
+ Returns the possible values for a given schema or types map and field.
|
212
|
+
|
213
|
+ These values are the atoms that represent the different possible values
|
214
|
+ of the field.
|
215
|
+
|
216
|
+ ## Examples
|
217
|
+
|
218
|
+ Assuming this schema:
|
219
|
+
|
220
|
+ defmodule MySchema do
|
221
|
+ use Ecto.Schema
|
222
|
+
|
223
|
+ schema "my_schema" do
|
224
|
+ field :my_string_enum, Ecto.Enum, values: [:foo, :bar, :baz]
|
225
|
+ field :my_integer_enum, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
|
226
|
+ end
|
227
|
+ end
|
228
|
+
|
229
|
+ Then:
|
230
|
+
|
231
|
+ Ecto.Enum.values(MySchema, :my_string_enum)
|
232
|
+ #=> [:foo, :bar, :baz]
|
233
|
+
|
234
|
+ Ecto.Enum.values(MySchema, :my_integer_enum)
|
235
|
+ #=> [:foo, :bar, :baz]
|
236
|
+
|
237
|
+ """
|
238
|
+ @spec values(map | Ecto.Schema.t(), atom) :: [atom()]
|
218
239
|
def values(schema_or_types, field) do
|
219
240
|
schema_or_types
|
220
241
|
|> mappings(field)
|
221
242
|
|> Keyword.keys()
|
222
243
|
end
|
223
244
|
|
224
|
- @doc "Returns the possible dump values for a given schema or types map and field"
|
225
|
- @spec dump_values(map | module, atom) :: [String.t()] | [integer()]
|
245
|
+ @doc """
|
246
|
+ Returns the possible dump values for a given schema or types map and field
|
247
|
+
|
248
|
+ "Dump values" are the values that can be dumped in the database. For enums stored
|
249
|
+ as strings, these are the strings that will be dumped in the database. For enums
|
250
|
+ stored as integers, these are the integers that will be dumped in the database.
|
251
|
+
|
252
|
+ ## Examples
|
253
|
+
|
254
|
+ Assuming this schema:
|
255
|
+
|
256
|
+ defmodule MySchema do
|
257
|
+ use Ecto.Schema
|
258
|
+
|
259
|
+ schema "my_schema" do
|
260
|
+ field :my_string_enum, Ecto.Enum, values: [:foo, :bar, :baz]
|
261
|
+ field :my_integer_enum, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
|
262
|
+ end
|
263
|
+ end
|
264
|
+
|
265
|
+ Then:
|
266
|
+
|
267
|
+ Ecto.Enum.dump_values(MySchema, :my_string_enum)
|
268
|
+ #=> ["foo", "bar", "baz"]
|
269
|
+
|
270
|
+ Ecto.Enum.dump_values(MySchema, :my_integer_enum)
|
271
|
+ #=> [1, 2, 5]
|
272
|
+
|
273
|
+ `schema_or_types` can also be a types map. See `mappings/2` for more information.
|
274
|
+ """
|
275
|
+ @spec dump_values(map | Ecto.Schema.t(), atom) :: [String.t()] | [integer()]
|
226
276
|
def dump_values(schema_or_types, field) do
|
227
277
|
schema_or_types
|
228
278
|
|> mappings(field)
|
229
279
|
|> Keyword.values()
|
230
280
|
end
|
231
281
|
|
232
|
- @doc "Returns the mappings between values and dumped values"
|
233
|
- @spec mappings(map, atom) :: Keyword.t()
|
282
|
+ @doc """
|
283
|
+ Returns the mappings between values and dumped values.
|
284
|
+
|
285
|
+ ## Examples
|
286
|
+
|
287
|
+ Assuming this schema:
|
288
|
+
|
289
|
+ defmodule MySchema do
|
290
|
+ use Ecto.Schema
|
291
|
+
|
292
|
+ schema "my_schema" do
|
293
|
+ field :my_string_enum, Ecto.Enum, values: [:foo, :bar, :baz]
|
294
|
+ field :my_integer_enum, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
|
295
|
+ end
|
296
|
+ end
|
297
|
+
|
298
|
+ Here are some examples of using `mappings/2` with it:
|
299
|
+
|
300
|
+ Ecto.Enum.mappings(MySchema, :my_string_enum)
|
301
|
+ #=> [foo: "foo", bar: "bar", baz: "baz"]
|
302
|
+
|
303
|
+ Ecto.Enum.mappings(MySchema, :my_integer_enum)
|
304
|
+ #=> [foo: 1, bar: 2, baz: 5]
|
305
|
+
|
306
|
+ Examples of calling `mappings/2` with a types map:
|
307
|
+
|
308
|
+ schemaless_types = %{
|
309
|
+ my_enum: Ecto.ParameterizedType.init(Ecto.Enum, values: [:foo, :bar, :baz]),
|
310
|
+ my_integer_enum: Ecto.ParameterizedType.init(Ecto.Enum, values: [foo: 1, bar: 2, baz: 5])
|
311
|
+ }
|
312
|
+
|
313
|
+ Ecto.Enum.mappings(schemaless_types, :my_enum)
|
314
|
+ #=> [foo: "foo", bar: "bar", baz: "baz"]
|
315
|
+ Ecto.Enum.mappings(schemaless_types, :my_integer_enum)
|
316
|
+ #=> [foo: 1, bar: 2, baz: 5]
|
317
|
+
|
318
|
+ """
|
319
|
+ @spec mappings(map | Ecto.Schema.t(), atom) :: keyword(String.t() | integer())
|
320
|
+ def mappings(schema_or_types, field)
|
321
|
+
|
234
322
|
def mappings(types, field) when is_map(types) do
|
235
323
|
case types do
|
236
|
- %{^field => {:parameterized, Ecto.Enum, %{mappings: mappings}}} -> mappings
|
237
|
- %{^field => {_, {:parameterized, Ecto.Enum, %{mappings: mappings}}}} -> mappings
|
324
|
+ %{^field => {:parameterized, {Ecto.Enum, %{mappings: mappings}}}} -> mappings
|
325
|
+ %{^field => {_, {:parameterized, {Ecto.Enum, %{mappings: mappings}}}}} -> mappings
|
238
326
|
%{^field => _} -> raise ArgumentError, "#{field} is not an Ecto.Enum field"
|
239
327
|
%{} -> raise ArgumentError, "#{field} does not exist"
|
240
328
|
end
|
241
329
|
end
|
242
330
|
|
243
|
- @spec mappings(module, atom) :: Keyword.t()
|
244
|
- def mappings(schema, field) do
|
331
|
+ def mappings(schema, field) when is_atom(schema) do
|
245
332
|
try do
|
246
333
|
schema.__changeset__()
|
247
334
|
rescue
|
changed
lib/ecto/exceptions.ex
|
@@ -159,7 +159,7 @@ defmodule Ecto.InvalidURLError do
|
159
159
|
def exception(opts) do
|
160
160
|
url = Keyword.fetch!(opts, :url)
|
161
161
|
msg = Keyword.fetch!(opts, :message)
|
162
|
- msg = "invalid url #{url}, #{msg}. The parsed URL is: #{inspect(URI.parse(url))}"
|
162
|
+ msg = "invalid URL #{url}, #{msg}. The parsed URL is: #{inspect(URI.parse(url))}"
|
163
163
|
%__MODULE__{message: msg, url: url}
|
164
164
|
end
|
165
165
|
end
|
|
@@ -268,6 +268,12 @@ defmodule Ecto.StaleEntryError do
|
268
268
|
attempted to #{action} a stale struct:
|
269
269
|
|
270
270
|
#{inspect(changeset.data)}
|
271
|
+
|
272
|
+ This typically happens when the struct no longer exists in the database \
|
273
|
+ or a database trigger/rule has forbidden the action. If stale entries are \
|
274
|
+ expected, you may use `:stale_error_field` to convert this into a changeset \
|
275
|
+ error, or set `:allow_stale` to true if you would like stale operations to \
|
276
|
+ be considered a success (such as a stale deletion)
|
271
277
|
"""
|
272
278
|
|
273
279
|
%__MODULE__{message: msg, changeset: changeset}
|
changed
lib/ecto/json.ex
|
@@ -29,7 +29,7 @@ if Code.ensure_loaded?(Jason.Encoder) do
|
29
29
|
exposed externally.
|
30
30
|
|
31
31
|
You can either map the schemas to remove the :__meta__ field before \
|
32
|
- encoding to JSON, or explicit list the JSON fields in your schema:
|
32
|
+ encoding to JSON, or explicitly list the JSON fields in your schema:
|
33
33
|
|
34
34
|
defmodule #{inspect(schema)} do
|
35
35
|
# ...
|
changed
lib/ecto/multi.ex
|
@@ -278,7 +278,9 @@ defmodule Ecto.Multi do
|
278
278
|
@doc """
|
279
279
|
Adds an insert operation to the multi.
|
280
280
|
|
281
|
- Accepts the same arguments and options as `c:Ecto.Repo.insert/2` does.
|
281
|
+ The `name` must be unique from other statements in the multi.
|
282
|
+
|
283
|
+ The remaining arguments and options are the same as in `c:Ecto.Repo.insert/2`.
|
282
284
|
|
283
285
|
## Example
|
284
286
|
|
|
@@ -317,7 +319,9 @@ defmodule Ecto.Multi do
|
317
319
|
@doc """
|
318
320
|
Adds an update operation to the multi.
|
319
321
|
|
320
|
- Accepts the same arguments and options as `c:Ecto.Repo.update/2` does.
|
322
|
+ The `name` must be unique from other statements in the multi.
|
323
|
+
|
324
|
+ The remaining arguments and options are the same as in `c:Ecto.Repo.update/2`.
|
321
325
|
|
322
326
|
## Example
|
323
327
|
|
|
@@ -349,7 +353,9 @@ defmodule Ecto.Multi do
|
349
353
|
@doc """
|
350
354
|
Inserts or updates a changeset depending on whether the changeset was persisted or not.
|
351
355
|
|
352
|
- Accepts the same arguments and options as `c:Ecto.Repo.insert_or_update/2` does.
|
356
|
+ The `name` must be unique from other statements in the multi.
|
357
|
+
|
358
|
+ The remaining arguments and options are the same as in `c:Ecto.Repo.insert_or_update/2`.
|
353
359
|
|
354
360
|
## Example
|
355
361
|
|
|
@@ -391,7 +397,9 @@ defmodule Ecto.Multi do
|
391
397
|
@doc """
|
392
398
|
Adds a delete operation to the multi.
|
393
399
|
|
394
|
- Accepts the same arguments and options as `c:Ecto.Repo.delete/2` does.
|
400
|
+ The `name` must be unique from other statements in the multi.
|
401
|
+
|
402
|
+ The remaining arguments and options are the same as in `c:Ecto.Repo.delete/2`.
|
395
403
|
|
396
404
|
## Example
|
397
405
|
|
|
@@ -437,7 +445,9 @@ defmodule Ecto.Multi do
|
437
445
|
@doc """
|
438
446
|
Runs a query expecting one result and stores it in the multi.
|
439
447
|
|
440
|
- Accepts the same arguments and options as `c:Ecto.Repo.one/2`.
|
448
|
+ The `name` must be unique from other statements in the multi.
|
449
|
+
|
450
|
+ The remaining arguments and options are the same as in `c:Ecto.Repo.one/2`.
|
441
451
|
|
442
452
|
## Example
|
443
453
|
|
|
@@ -467,7 +477,9 @@ defmodule Ecto.Multi do
|
467
477
|
@doc """
|
468
478
|
Runs a query and stores all entries in the multi.
|
469
479
|
|
470
|
- Accepts the same arguments and options as `c:Ecto.Repo.all/2` does.
|
480
|
+ The `name` must be unique from other statements in the multi.
|
481
|
+
|
482
|
+ The remaining arguments and options are the same as in `c:Ecto.Repo.all/2` does.
|
471
483
|
|
472
484
|
## Example
|
473
485
|
|
|
@@ -498,7 +510,9 @@ defmodule Ecto.Multi do
|
498
510
|
@doc """
|
499
511
|
Checks if there exists an entry matching the given query and stores a boolean in the multi.
|
500
512
|
|
501
|
- Accepts the same arguments and options as `c:Ecto.Repo.exists?/2`.
|
513
|
+ The `name` must be unique from other statements in the multi.
|
514
|
+
|
515
|
+ The remaining arguments and options are the same as in `c:Ecto.Repo.exists?/2`.
|
502
516
|
|
503
517
|
## Example
|
changed
lib/ecto/parameterized_type.ex
|
@@ -88,7 +88,7 @@ defmodule Ecto.ParameterizedType do
|
88
88
|
@type params :: term()
|
89
89
|
|
90
90
|
@doc """
|
91
|
- Callback to convert the options specified in the field macro into parameters
|
91
|
+ Callback to convert the options specified in the field macro into parameters
|
92
92
|
to be used in other callbacks.
|
93
93
|
|
94
94
|
This function is called at compile time, and should raise if invalid values are
|
|
@@ -193,7 +193,7 @@ defmodule Ecto.ParameterizedType do
|
193
193
|
Useful when manually initializing a type for schemaless changesets.
|
194
194
|
"""
|
195
195
|
def init(type, opts) do
|
196
|
- {:parameterized, type, type.init(opts)}
|
196
|
+ {:parameterized, {type, type.init(opts)}}
|
197
197
|
end
|
198
198
|
|
199
199
|
@doc false
|
changed
lib/ecto/query.ex
|
@@ -353,7 +353,7 @@ defmodule Ecto.Query do
|
353
353
|
or `join` is used. The query prefix is used only if none of the above
|
354
354
|
are declared.
|
355
355
|
|
356
|
- Let's see some examples. To see the query prefix globally, the simplest
|
356
|
+ Let's see some examples. To set the query prefix globally, the simplest
|
357
357
|
mechanism is to pass an option to the repository operation:
|
358
358
|
|
359
359
|
results = Repo.all(query, prefix: "accounts")
|
|
@@ -431,6 +431,11 @@ defmodule Ecto.Query do
|
431
431
|
defstruct [:expr, :file, :line, params: []]
|
432
432
|
end
|
433
433
|
|
434
|
+ defmodule ByExpr do
|
435
|
+ @moduledoc false
|
436
|
+ defstruct [:expr, :file, :line, params: [], subqueries: []]
|
437
|
+ end
|
438
|
+
|
434
439
|
defmodule BooleanExpr do
|
435
440
|
@moduledoc false
|
436
441
|
defstruct [:op, :expr, :file, :line, params: [], subqueries: []]
|
|
@@ -865,7 +870,8 @@ defmodule Ecto.Query do
|
865
870
|
)
|
866
871
|
|
867
872
|
If you need to refer to a parent binding which is not known when writing the subquery,
|
868
|
- you can use `parent_as` as shown in the examples under "Named bindings" in this module doc.
|
873
|
+ you can use `parent_as` as shown in the examples under ["Named bindings"](#module-named-bindings)
|
874
|
+ in this module doc.
|
869
875
|
|
870
876
|
You can also use subquery directly in `select` and `select_merge`:
|
871
877
|
|
|
@@ -897,9 +903,7 @@ defmodule Ecto.Query do
|
897
903
|
:right_join,
|
898
904
|
:full_join,
|
899
905
|
:inner_lateral_join,
|
900
|
- :left_lateral_join,
|
901
|
- :array_join,
|
902
|
- :left_array_join
|
906
|
+ :left_lateral_join
|
903
907
|
]
|
904
908
|
|
905
909
|
@doc """
|
|
@@ -948,8 +952,6 @@ defmodule Ecto.Query do
|
948
952
|
Ecto.Query.exclude(query, :full_join)
|
949
953
|
Ecto.Query.exclude(query, :inner_lateral_join)
|
950
954
|
Ecto.Query.exclude(query, :left_lateral_join)
|
951
|
- Ecto.Query.exclude(query, :array_join)
|
952
|
- Ecto.Query.exclude(query, :left_array_join)
|
953
955
|
|
954
956
|
However, keep in mind that if a join is removed and its bindings
|
955
957
|
were referenced elsewhere, the bindings won't be removed, leading
|
|
@@ -1196,8 +1198,6 @@ defmodule Ecto.Query do
|
1196
1198
|
defp join_qual(:cross_lateral_join), do: :cross_lateral
|
1197
1199
|
defp join_qual(:left_lateral_join), do: :left_lateral
|
1198
1200
|
defp join_qual(:inner_lateral_join), do: :inner_lateral
|
1199
|
- defp join_qual(:array_join), do: :array
|
1200
|
- defp join_qual(:left_array_join), do: :left_array
|
1201
1201
|
|
1202
1202
|
defp collect_with_ties([{:with_ties, with_ties} | t], nil),
|
1203
1203
|
do: collect_with_ties(t, with_ties)
|
|
@@ -1249,12 +1249,11 @@ defmodule Ecto.Query do
|
1249
1249
|
Receives a source that is to be joined to the query and a condition for
|
1250
1250
|
the join. The join condition can be any expression that evaluates
|
1251
1251
|
to a boolean value. The qualifier must be one of `:inner`, `:left`,
|
1252
|
- `:right`, `:cross`, `:cross_lateral`, `:full`, `:inner_lateral`, `:left_lateral`,
|
1253
|
- `:array` or `:left_array`.
|
1252
|
+ `:right`, `:cross`, `:cross_lateral`, `:full`, `:inner_lateral` or `:left_lateral`.
|
1254
1253
|
|
1255
1254
|
For a keyword query the `:join` keyword can be changed to `:inner_join`,
|
1256
|
- `:left_join`, `:right_join`, `:cross_join`, `:cross_lateral_join`, `:full_join`, `:inner_lateral_join`,
|
1257
|
- `:left_lateral_join`, `:array_join` or `:left_array_join`. `:join` is equivalent to `:inner_join`.
|
1255
|
+ `:left_join`, `:right_join`, `:cross_join`, `:cross_lateral_join`, `:full_join`, `:inner_lateral_join`
|
1256
|
+ or `:left_lateral_join`. `:join` is equivalent to `:inner_join`.
|
1258
1257
|
|
1259
1258
|
Currently it is possible to join on:
|
1260
1259
|
|
|
@@ -1403,18 +1402,6 @@ defmodule Ecto.Query do
|
1403
1402
|
disclaimers about such functionality.
|
1404
1403
|
|
1405
1404
|
Join hints must be static compile-time strings when they are specified as (list of) strings.
|
1406
|
-
|
1407
|
- ## Array joins
|
1408
|
-
|
1409
|
- The `:array` and `:left_array` qualifiers can be used to join with array
|
1410
|
- columns in [Clickhouse:](https://clickhouse.com/docs/en/sql-reference/statements/select/array-join)
|
1411
|
-
|
1412
|
- from at in "arrays_test",
|
1413
|
- array_join: a in "arr",
|
1414
|
- select: %{s: at.s, arr: a}
|
1415
|
-
|
1416
|
- Note that only the columns in the base table (i.e. the table referenced in `FROM`) can be used in the array join.
|
1417
|
-
|
1418
1405
|
"""
|
1419
1406
|
@join_opts [:on | @from_join_opts]
|
1420
1407
|
|
|
@@ -1943,7 +1930,22 @@ defmodule Ecto.Query do
|
1943
1930
|
end
|
1944
1931
|
|
1945
1932
|
@doc """
|
1946
|
- Same as `order_by/3` except new expressions will be prepended to existing ones.
|
1933
|
+ An order by query expression that is prepended to existing ones.
|
1934
|
+
|
1935
|
+ Accepts the same input as `order_by/3` except the expression will
|
1936
|
+ come before any previously defined order by expression. This only
|
1937
|
+ works with the macro-based query syntax and not the keyword-based
|
1938
|
+ query syntax.
|
1939
|
+
|
1940
|
+ For example, the following will generate a query that orders by `human_popluation`
|
1941
|
+ and then `name`:
|
1942
|
+
|
1943
|
+ City |> order_by([c], c.name) |> prepend_order_by([c], c.human_population)
|
1944
|
+
|
1945
|
+ The corresponding keyword-based syntax will raise an error:
|
1946
|
+
|
1947
|
+ from c in City, order_by: c.name, prepend_order_by: c.human_population
|
1948
|
+
|
1947
1949
|
"""
|
1948
1950
|
defmacro prepend_order_by(query, binding \\ [], expr) do
|
1949
1951
|
Builder.OrderBy.build(query, binding, expr, :prepend, __CALLER__)
|
|
@@ -1955,25 +1957,46 @@ defmodule Ecto.Query do
|
1955
1957
|
Combines result sets of multiple queries. The `select` of each query
|
1956
1958
|
must be exactly the same, with the same types in the same order.
|
1957
1959
|
|
1958
|
- > ### Selecting literal atoms {: .warning}
|
1959
|
- >
|
1960
|
- > When selecting a literal atom, its value must be the same across
|
1961
|
- > all queries. Otherwise, the value from the parent query will be
|
1962
|
- > applied to all other queries. This also holds true for selecting
|
1963
|
- > maps with atom keys.
|
1964
|
-
|
1965
1960
|
Union expression returns only unique rows as if each query returned
|
1966
1961
|
distinct results. This may cause a performance penalty. If you need
|
1967
1962
|
to combine multiple result sets without removing duplicate rows
|
1968
1963
|
consider using `union_all/2`.
|
1969
1964
|
|
1970
|
- Note that the operations `order_by`, `limit` and `offset` of the
|
1971
|
- current `query` apply to the result of the union. `order_by` must
|
1972
|
- be specified in one of the following ways, since the union of two
|
1973
|
- or more queries is not automatically aliased:
|
1965
|
+ ## Combination behaviour
|
1974
1966
|
|
1975
|
- - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement that directly access the union fields.
|
1976
|
- - Wrap the union in a subquery and refer to the binding of the subquery.
|
1967
|
+ There are several behaviours of combination queries that must be taken
|
1968
|
+ into account, otherwise you may unexpectedly return the wrong query result.
|
1969
|
+
|
1970
|
+ ### Order by, limit and offset
|
1971
|
+
|
1972
|
+ The `order_by`, `limit` and `offset` expressions of the parent query apply
|
1973
|
+ to the result of the entire combination. `order_by` must be specified in one
|
1974
|
+ of the following ways, since the combination of two or more queries is not
|
1975
|
+ automatically aliased:
|
1976
|
+
|
1977
|
+ - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement
|
1978
|
+ that directly access the combination fields.
|
1979
|
+ - Wrap the combination in a subquery and refer to the binding of the subquery.
|
1980
|
+
|
1981
|
+ ### Column selection ordering
|
1982
|
+
|
1983
|
+ The columns of each of the queries in the combination must be specified in
|
1984
|
+ the exact same order. Otherwise, you may see the values of one column appearing
|
1985
|
+ in another. This holds for all types of select expressions, including maps.
|
1986
|
+
|
1987
|
+ For example, the following query will interchange the values of the supplier's
|
1988
|
+ name and city because that is the order the fields are specified in the customer
|
1989
|
+ query.
|
1990
|
+
|
1991
|
+ supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
|
1992
|
+ customer_query = from c in Customer, select: %{name: c.name, city: c.city}
|
1993
|
+ union(supplier_query, ^customer_query)
|
1994
|
+
|
1995
|
+ ### Selecting literal atoms
|
1996
|
+
|
1997
|
+ When selecting a literal atom, its value must be the same across all queries.
|
1998
|
+ Otherwise, the value from the parent query will be applied to all other queries.
|
1999
|
+ This also holds true for selecting maps with atom keys.
|
1977
2000
|
|
1978
2001
|
## Keywords examples
|
1979
2002
|
|
|
@@ -2008,20 +2031,41 @@ defmodule Ecto.Query do
|
2008
2031
|
Combines result sets of multiple queries. The `select` of each query
|
2009
2032
|
must be exactly the same, with the same types in the same order.
|
2010
2033
|
|
2011
|
- > ### Selecting literal atoms {: .warning}
|
2012
|
- >
|
2013
|
- > When selecting a literal atom, its value must be the same across
|
2014
|
- > all queries. Otherwise, the value from the parent query will be
|
2015
|
- > applied to all other queries. This also holds true for selecting
|
2016
|
- > maps with atom keys.
|
2034
|
+ ## Combination behaviour
|
2017
2035
|
|
2018
|
- Note that the operations `order_by`, `limit` and `offset` of the
|
2019
|
- current `query` apply to the result of the union. `order_by` must
|
2020
|
- be specified in one of the following ways, since the union of two
|
2021
|
- or more queries is not automatically aliased:
|
2036
|
+ There are several behaviours of combination queries that must be taken
|
2037
|
+ into account, otherwise you may unexpectedly return the wrong query result.
|
2022
2038
|
|
2023
|
- - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement that directly access the union fields.
|
2024
|
- - Wrap the union in a subquery and refer to the binding of the subquery.
|
2039
|
+ ### Order by, limit and offset
|
2040
|
+
|
2041
|
+ The `order_by`, `limit` and `offset` expressions of the parent query apply
|
2042
|
+ to the result of the entire combination. `order_by` must be specified in one
|
2043
|
+ of the following ways, since the combination of two or more queries is not
|
2044
|
+ automatically aliased:
|
2045
|
+
|
2046
|
+ - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement
|
2047
|
+ that directly access the combination fields.
|
2048
|
+ - Wrap the combination in a subquery and refer to the binding of the subquery.
|
2049
|
+
|
2050
|
+ ### Column selection ordering
|
2051
|
+
|
2052
|
+ The columns of each of the queries in the combination must be specified in
|
2053
|
+ the exact same order. Otherwise, you may see the values of one column appearing
|
2054
|
+ in another. This holds for all types of select expressions, including maps.
|
2055
|
+
|
2056
|
+ For example, the following query will interchange the values of the supplier's
|
2057
|
+ name and city because that is the order the fields are specified in the customer
|
2058
|
+ query.
|
2059
|
+
|
2060
|
+ supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
|
2061
|
+ customer_query = from c in Customer, select: %{name: c.name, city: c.city}
|
2062
|
+ union_all(supplier_query, ^customer_query)
|
2063
|
+
|
2064
|
+ ### Selecting literal atoms
|
2065
|
+
|
2066
|
+ When selecting a literal atom, its value must be the same across all queries.
|
2067
|
+ Otherwise, the value from the parent query will be applied to all other queries.
|
2068
|
+ This also holds true for selecting maps with atom keys.
|
2025
2069
|
|
2026
2070
|
## Keywords examples
|
2027
2071
|
|
|
@@ -2056,25 +2100,46 @@ defmodule Ecto.Query do
|
2056
2100
|
`select` of each query must be exactly the same, with the same
|
2057
2101
|
types in the same order.
|
2058
2102
|
|
2059
|
- > ### Selecting literal atoms {: .warning}
|
2060
|
- >
|
2061
|
- > When selecting a literal atom, its value must be the same across
|
2062
|
- > all queries. Otherwise, the value from the parent query will be
|
2063
|
- > applied to all other queries. This also holds true for selecting
|
2064
|
- > maps with atom keys.
|
2065
|
-
|
2066
2103
|
Except expression returns only unique rows as if each query returned
|
2067
2104
|
distinct results. This may cause a performance penalty. If you need
|
2068
2105
|
to take the difference of multiple result sets without
|
2069
2106
|
removing duplicate rows consider using `except_all/2`.
|
2070
2107
|
|
2071
|
- Note that the operations `order_by`, `limit` and `offset` of the
|
2072
|
- current `query` apply to the result of the set difference. `order_by`
|
2073
|
- must be specified in one of the following ways, since the set difference
|
2074
|
- of two or more queries is not automatically aliased:
|
2108
|
+ ## Combination behaviour
|
2075
2109
|
|
2076
|
- - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement that directly access the set difference fields.
|
2077
|
- - Wrap the set difference in a subquery and refer to the binding of the subquery.
|
2110
|
+ There are several behaviours of combination queries that must be taken
|
2111
|
+ into account, otherwise you may unexpectedly return the wrong query result.
|
2112
|
+
|
2113
|
+ ### Order by, limit and offset
|
2114
|
+
|
2115
|
+ The `order_by`, `limit` and `offset` expressions of the parent query apply
|
2116
|
+ to the result of the entire combination. `order_by` must be specified in one
|
2117
|
+ of the following ways, since the combination of two or more queries is not
|
2118
|
+ automatically aliased:
|
2119
|
+
|
2120
|
+ - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement
|
2121
|
+ that directly access the combination fields.
|
2122
|
+ - Wrap the combination in a subquery and refer to the binding of the subquery.
|
2123
|
+
|
2124
|
+ ### Column selection ordering
|
2125
|
+
|
2126
|
+ The columns of each of the queries in the combination must be specified in
|
2127
|
+ the exact same order. Otherwise, you may see the values of one column appearing
|
2128
|
+ in another. This holds for all types of select expressions, including maps.
|
2129
|
+
|
2130
|
+ For example, the following query will interchange the values of the supplier's
|
2131
|
+ name and city because that is the order the fields are specified in the customer
|
2132
|
+ query.
|
2133
|
+
|
2134
|
+ supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
|
2135
|
+ customer_query = from c in Customer, select: %{name: c.name, city: c.city}
|
2136
|
+ except(supplier_query, ^customer_query)
|
2137
|
+
|
2138
|
+ ### Selecting literal atoms
|
2139
|
+
|
2140
|
+ When selecting a literal atom, its value must be the same across all queries.
|
2141
|
+ Otherwise, the value from the parent query will be applied to all other queries.
|
2142
|
+ This also holds true for selecting maps with atom keys.
|
2078
2143
|
|
2079
2144
|
## Keywords examples
|
2080
2145
|
|
|
@@ -2109,20 +2174,41 @@ defmodule Ecto.Query do
|
2109
2174
|
`select` of each query must be exactly the same, with the same
|
2110
2175
|
types in the same order.
|
2111
2176
|
|
2112
|
- > ### Selecting literal atoms {: .warning}
|
2113
|
- >
|
2114
|
- > When selecting a literal atom, its value must be the same across
|
2115
|
- > all queries. Otherwise, the value from the parent query will be
|
2116
|
- > applied to all other queries. This also holds true for selecting
|
2117
|
- > maps with atom keys.
|
2177
|
+ ## Combination behaviour
|
2118
2178
|
|
2119
|
- Note that the operations `order_by`, `limit` and `offset` of the
|
2120
|
- current `query` apply to the result of the set difference. `order_by`
|
2121
|
- must be specified in one of the following ways, since the set difference
|
2122
|
- of two or more queries is not automatically aliased:
|
2179
|
+ There are several behaviours of combination queries that must be taken
|
2180
|
+ into account, otherwise you may unexpectedly return the wrong query result.
|
2123
2181
|
|
2124
|
- - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement that directly access the set difference fields.
|
2125
|
- - Wrap the set difference in a subquery and refer to the binding of the subquery.
|
2182
|
+ ### Order by, limit and offset
|
2183
|
+
|
2184
|
+ The `order_by`, `limit` and `offset` expressions of the parent query apply
|
2185
|
+ to the result of the entire combination. `order_by` must be specified in one
|
2186
|
+ of the following ways, since the combination of two or more queries is not
|
2187
|
+ automatically aliased:
|
2188
|
+
|
2189
|
+ - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement
|
2190
|
+ that directly access the combination fields.
|
2191
|
+ - Wrap the combination in a subquery and refer to the binding of the subquery.
|
2192
|
+
|
2193
|
+ ### Column selection ordering
|
2194
|
+
|
2195
|
+ The columns of each of the queries in the combination must be specified in
|
2196
|
+ the exact same order. Otherwise, you may see the values of one column appearing
|
2197
|
+ in another. This holds for all types of select expressions, including maps.
|
2198
|
+
|
2199
|
+ For example, the following query will interchange the values of the supplier's
|
2200
|
+ name and city because that is the order the fields are specified in the customer
|
2201
|
+ query.
|
2202
|
+
|
2203
|
+ supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
|
2204
|
+ customer_query = from c in Customer, select: %{name: c.name, city: c.city}
|
2205
|
+ except_all(supplier_query, ^customer_query)
|
2206
|
+
|
2207
|
+ ### Selecting literal atoms
|
2208
|
+
|
2209
|
+ When selecting a literal atom, its value must be the same across all queries.
|
2210
|
+ Otherwise, the value from the parent query will be applied to all other queries.
|
2211
|
+ This also holds true for selecting maps with atom keys.
|
2126
2212
|
|
2127
2213
|
## Keywords examples
|
2128
2214
|
|
|
@@ -2157,25 +2243,46 @@ defmodule Ecto.Query do
|
2157
2243
|
`select` of each query must be exactly the same, with the same
|
2158
2244
|
types in the same order.
|
2159
2245
|
|
2160
|
- > ### Selecting literal atoms {: .warning}
|
2161
|
- >
|
2162
|
- > When selecting a literal atom, its value must be the same across
|
2163
|
- > all queries. Otherwise, the value from the parent query will be
|
2164
|
- > applied to all other queries. This also holds true for selecting
|
2165
|
- > maps with atom keys.
|
2166
|
-
|
2167
2246
|
Intersect expression returns only unique rows as if each query returned
|
2168
2247
|
distinct results. This may cause a performance penalty. If you need
|
2169
2248
|
to take the intersection of multiple result sets without
|
2170
2249
|
removing duplicate rows consider using `intersect_all/2`.
|
2171
2250
|
|
2172
|
- Note that the operations `order_by`, `limit` and `offset` of the
|
2173
|
- current `query` apply to the result of the set difference. `order_by`
|
2174
|
- must be specified in one of the following ways, since the intersection
|
2175
|
- of two or more queries is not automatically aliased:
|
2251
|
+ ## Combination behaviour
|
2176
2252
|
|
2177
|
- - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement that directly access the intersection fields.
|
2178
|
- - Wrap the intersection in a subquery and refer to the binding of the subquery.
|
2253
|
+ There are several behaviours of combination queries that must be taken
|
2254
|
+ into account, otherwise you may unexpectedly return the wrong query result.
|
2255
|
+
|
2256
|
+ ### Order by, limit and offset
|
2257
|
+
|
2258
|
+ The `order_by`, `limit` and `offset` expressions of the parent query apply
|
2259
|
+ to the result of the entire combination. `order_by` must be specified in one
|
2260
|
+ of the following ways, since the combination of two or more queries is not
|
2261
|
+ automatically aliased:
|
2262
|
+
|
2263
|
+ - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement
|
2264
|
+ that directly access the combination fields.
|
2265
|
+ - Wrap the combination in a subquery and refer to the binding of the subquery.
|
2266
|
+
|
2267
|
+ ### Column selection ordering
|
2268
|
+
|
2269
|
+ The columns of each of the queries in the combination must be specified in
|
2270
|
+ the exact same order. Otherwise, you may see the values of one column appearing
|
2271
|
+ in another. This holds for all types of select expressions, including maps.
|
2272
|
+
|
2273
|
+ For example, the following query will interchange the values of the supplier's
|
2274
|
+ name and city because that is the order the fields are specified in the customer
|
2275
|
+ query.
|
2276
|
+
|
2277
|
+ supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
|
2278
|
+ customer_query = from c in Customer, select: %{name: c.name, city: c.city}
|
2279
|
+ intersect(supplier_query, ^customer_query)
|
2280
|
+
|
2281
|
+ ### Selecting literal atoms
|
2282
|
+
|
2283
|
+ When selecting a literal atom, its value must be the same across all queries.
|
2284
|
+ Otherwise, the value from the parent query will be applied to all other queries.
|
2285
|
+ This also holds true for selecting maps with atom keys.
|
2179
2286
|
|
2180
2287
|
## Keywords examples
|
2181
2288
|
|
|
@@ -2210,20 +2317,41 @@ defmodule Ecto.Query do
|
2210
2317
|
`select` of each query must be exactly the same, with the same
|
2211
2318
|
types in the same order.
|
2212
2319
|
|
2213
|
- > ### Selecting literal atoms {: .warning}
|
2214
|
- >
|
2215
|
- > When selecting a literal atom, its value must be the same across
|
2216
|
- > all queries. Otherwise, the value from the parent query will be
|
2217
|
- > applied to all other queries. This also holds true for selecting
|
2218
|
- > maps with atom keys.
|
2320
|
+ ## Combination behaviour
|
2219
2321
|
|
2220
|
- Note that the operations `order_by`, `limit` and `offset` of the
|
2221
|
- current `query` apply to the result of the set difference. `order_by`
|
2222
|
- must be specified in one of the following ways, since the intersection
|
2223
|
- of two or more queries is not automatically aliased:
|
2322
|
+ There are several behaviours of combination queries that must be taken
|
2323
|
+ into account, otherwise you may unexpectedly return the wrong query result.
|
2224
2324
|
|
2225
|
- - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement that directly access the intersection fields.
|
2226
|
- - Wrap the intersection in a subquery and refer to the binding of the subquery.
|
2325
|
+ ### Order by, limit and offset
|
2326
|
+
|
2327
|
+ The `order_by`, `limit` and `offset` expressions of the parent query apply
|
2328
|
+ to the result of the entire combination. `order_by` must be specified in one
|
2329
|
+ of the following ways, since the combination of two or more queries is not
|
2330
|
+ automatically aliased:
|
2331
|
+
|
2332
|
+ - Use `Ecto.Query.API.fragment/1` to pass an `order_by` statement
|
2333
|
+ that directly access the combination fields.
|
2334
|
+ - Wrap the combination in a subquery and refer to the binding of the subquery.
|
2335
|
+
|
2336
|
+ ### Column selection ordering
|
2337
|
+
|
2338
|
+ The columns of each of the queries in the combination must be specified in
|
2339
|
+ the exact same order. Otherwise, you may see the values of one column appearing
|
2340
|
+ in another. This holds for all types of select expressions, including maps.
|
2341
|
+
|
2342
|
+ For example, the following query will interchange the values of the supplier's
|
2343
|
+ name and city because that is the order the fields are specified in the customer
|
2344
|
+ query.
|
2345
|
+
|
2346
|
+ supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
|
2347
|
+ customer_query = from c in Customer, select: %{name: c.name, city: c.city}
|
2348
|
+ intersect_all(supplier_query, ^customer_query)
|
2349
|
+
|
2350
|
+ ### Selecting literal atoms
|
2351
|
+
|
2352
|
+ When selecting a literal atom, its value must be the same across all queries.
|
2353
|
+ Otherwise, the value from the parent query will be applied to all other queries.
|
2354
|
+ This also holds true for selecting maps with atom keys.
|
2227
2355
|
|
2228
2356
|
## Keywords examples
|
2229
2357
|
|
|
@@ -2393,6 +2521,19 @@ defmodule Ecto.Query do
|
2393
2521
|
|
2394
2522
|
from(u in User, update: [pull: [tags: "not cool"]])
|
2395
2523
|
|
2524
|
+ ## Composable
|
2525
|
+
|
2526
|
+ Remember that all query expressions are composable, so you can use `update`
|
2527
|
+ multiple times in the same query to merge the update expressions:
|
2528
|
+
|
2529
|
+ new_name = "new name"
|
2530
|
+ User
|
2531
|
+ |> update([u], set: [name: fragment("upper(?)", ^new_name)])
|
2532
|
+ |> update([u], set: [age: 42])
|
2533
|
+
|
2534
|
+ This can be useful to compose updates from different functions
|
2535
|
+ or when mixing interpolation, such as `set: ^updates`, with regular
|
2536
|
+ query expressions, such as `set: [age: u.age + 1]`.
|
2396
2537
|
"""
|
2397
2538
|
defmacro update(query, binding \\ [], expr) do
|
2398
2539
|
Builder.Update.build(query, binding, expr, __CALLER__)
|
|
@@ -2517,39 +2658,13 @@ defmodule Ecto.Query do
|
2517
2658
|
Nested associations can also be preloaded in both formats:
|
2518
2659
|
|
2519
2660
|
Repo.all from p in Post,
|
2520
|
- preload: [comments: :likes]
|
2661
|
+ preload: [:author, comments: :likes]
|
2521
2662
|
|
2522
2663
|
Repo.all from p in Post,
|
2523
2664
|
join: c in assoc(p, :comments),
|
2524
2665
|
join: l in assoc(c, :likes),
|
2525
2666
|
where: l.inserted_at > c.updated_at,
|
2526
|
- preload: [comments: {c, likes: l}]
|
2527
|
-
|
2528
|
- Applying a limit to the association can be achieved with `inner_lateral_join`:
|
2529
|
-
|
2530
|
- Repo.all from p in Post, as: :post,
|
2531
|
- join: c in assoc(p, :comments),
|
2532
|
- inner_lateral_join: top_five in subquery(
|
2533
|
- from Comment,
|
2534
|
- where: [post_id: parent_as(:post).id],
|
2535
|
- order_by: :popularity,
|
2536
|
- limit: 5,
|
2537
|
- select: [:id]
|
2538
|
- ), on: top_five.id == c.id,
|
2539
|
- preload: [comments: c]
|
2540
|
-
|
2541
|
- Preloaded joins can also be specified dynamically using `dynamic`:
|
2542
|
-
|
2543
|
- preloads = [comments: dynamic([comments: c], c)]
|
2544
|
-
|
2545
|
- Repo.all from p in Post,
|
2546
|
- join: c in assoc(p, :comments),
|
2547
|
- as: :comments,
|
2548
|
- where: c.published_at > p.updated_at,
|
2549
|
- preload: ^preloads
|
2550
|
-
|
2551
|
- See "`preload`" in the documentation for `dynamic/2` for more
|
2552
|
- details.
|
2667
|
+ preload: [:author, comments: {c, likes: l}]
|
2553
2668
|
|
2554
2669
|
## Preload queries
|
2555
2670
|
|
|
@@ -2563,9 +2678,9 @@ defmodule Ecto.Query do
|
2563
2678
|
then another for loading the comments associated with the posts.
|
2564
2679
|
Comments will be ordered by `published_at`.
|
2565
2680
|
|
2566
|
- When specifying a preload query, you can still preload the associations of
|
2567
|
- those records. For instance, you could preload an author's published posts and
|
2568
|
- the comments on those posts:
|
2681
|
+ When specifying a preload query, you can still nest preloads.
|
2682
|
+ For instance, you could preload an author's published posts and
|
2683
|
+ their comments as follows:
|
2569
2684
|
|
2570
2685
|
posts_query = from p in Post, where: p.state == :published
|
2571
2686
|
Repo.all from a in Author, preload: [posts: ^{posts_query, [:comments]}]
|
|
@@ -2576,11 +2691,6 @@ defmodule Ecto.Query do
|
2576
2691
|
posts_query =
|
2577
2692
|
from p in Post, where: p.state == :published, preload: :related_posts
|
2578
2693
|
|
2579
|
- The same can be written as pipe based query:
|
2580
|
-
|
2581
|
- posts_query =
|
2582
|
- Post |> where([p], p.state == :published) |> preload(:related_posts)
|
2583
|
-
|
2584
2694
|
Note: keep in mind operations like limit and offset in the preload
|
2585
2695
|
query will affect the whole result set and not each association. For
|
2586
2696
|
example, the query below:
|
|
@@ -2589,7 +2699,7 @@ defmodule Ecto.Query do
|
2589
2699
|
Repo.all from p in Post, preload: [comments: ^comments_query]
|
2590
2700
|
|
2591
2701
|
won't bring the top of comments per post. Rather, it will only bring
|
2592
|
- the 5 top comments across all posts. Instead, use a window:
|
2702
|
+ the 5 top comments across all posts. Instead, you must use a window:
|
2593
2703
|
|
2594
2704
|
ranking_query =
|
2595
2705
|
from c in Comment,
|
|
@@ -2603,19 +2713,22 @@ defmodule Ecto.Query do
|
2603
2713
|
|
2604
2714
|
Repo.all from p in Post, preload: [comments: ^comments_query]
|
2605
2715
|
|
2606
|
- Similarly, if you have a `:through` association, such as posts has many
|
2607
|
- `comments_authors` through comments (`posts->comments->comments_authors`),
|
2608
|
- the query will only customize the relationship between comments and
|
2609
|
- comments_authors, even if preloaded through posts. This means `order_by`
|
2610
|
- clauses on `:through` associations affect only the direct relationship
|
2611
|
- between `comments` and `comments_authors`, not between `posts` and
|
2612
|
- `comments_authors`.
|
2716
|
+ For `:through` associations, such as a post may have many comments_authors,
|
2717
|
+ written as `has_many :comments_authors, through: [:comments, :author]`
|
2718
|
+ the query given to preload customizes the relationship between comments and
|
2719
|
+ authors, even if preloaded through posts. Another way to put it, in case of
|
2720
|
+ `:through` associations, the query given to preload customizes the last join
|
2721
|
+ of the association chain. This means `order_by` clauses on `:through`
|
2722
|
+ associations affect only the direct relationship between `comments` and
|
2723
|
+ `authors`, not between posts and comments.
|
2613
2724
|
|
2614
2725
|
## Preload functions
|
2615
2726
|
|
2616
|
- Preload also allows functions to be given. In such cases, the function
|
2617
|
- receives the IDs of the parent association and it must return the associated
|
2618
|
- data. Ecto then will map this data and sort it by the relationship key:
|
2727
|
+ Preload also allows functions to be given. If the function has an arity of 1,
|
2728
|
+ it receives only the IDs of the parent association. If it has an arity of 2, it
|
2729
|
+ receives the IDS of the parent association as the first argument and the association
|
2730
|
+ metadata as the second argument. Both functions must return the associated data.
|
2731
|
+ Ecto then will map this data and sort it by the relationship key:
|
2619
2732
|
|
2620
2733
|
comment_preloader = fn post_ids -> fetch_comments_by_post_ids(post_ids) end
|
2621
2734
|
Repo.all from p in Post, preload: [comments: ^comment_preloader]
|
|
@@ -2648,7 +2761,46 @@ defmodule Ecto.Query do
|
2648
2761
|
function, the function will receive a list of "post_ids" as the argument
|
2649
2762
|
and it must return a tuple in the format of `{post_id, tag}`
|
2650
2763
|
|
2651
|
- If you want to reset the loaded fields, see `Ecto.reset_fields/2`.
|
2764
|
+ The 2-arity version of the function is especially useful if you would like to
|
2765
|
+ build a general preloader that works across all associations. For example, if
|
2766
|
+ you would like to build a preloader for lateral joins that finds the newest
|
2767
|
+ associations you may do the following:
|
2768
|
+
|
2769
|
+ lateral_preloader = fn ids, assoc -> newest_records(ids, assoc, 5) end
|
2770
|
+
|
2771
|
+ def newest_records(parent_ids, assoc, n) do
|
2772
|
+ %{related_key: related_key, queryable: queryable} = assoc
|
2773
|
+
|
2774
|
+ squery =
|
2775
|
+ from q in queryable,
|
2776
|
+ where: field(q, ^related_key) == parent_as(:parent_ids).id,
|
2777
|
+ order_by: {:desc, :created_at},
|
2778
|
+ limit: ^n
|
2779
|
+
|
2780
|
+ query =
|
2781
|
+ from f in fragment("SELECT id from UNNEST(?::int[]) AS id", ^parent_ids), as: :parent_ids,
|
2782
|
+ inner_lateral_join: s in subquery(squery), on: true,
|
2783
|
+ select: s
|
2784
|
+
|
2785
|
+ Repo.all(query)
|
2786
|
+ end
|
2787
|
+
|
2788
|
+ For the list of available metadata, see the module documentation of the association types.
|
2789
|
+ For example, see `Ecto.Association.BelongsTo`.
|
2790
|
+
|
2791
|
+ ## Dynamic preloads
|
2792
|
+
|
2793
|
+ Preloads can also be specified dynamically using the `dynamic` macro:
|
2794
|
+
|
2795
|
+ preloads = [comments: dynamic([comments: c], c)]
|
2796
|
+
|
2797
|
+ Repo.all from p in Post,
|
2798
|
+ join: c in assoc(p, :comments),
|
2799
|
+ as: :comments,
|
2800
|
+ where: c.published_at > p.updated_at,
|
2801
|
+ preload: ^preloads
|
2802
|
+
|
2803
|
+ See `dynamic/2` for more information.
|
2652
2804
|
|
2653
2805
|
## Keywords example
|
2654
2806
|
|
|
@@ -2731,7 +2883,7 @@ defmodule Ecto.Query do
|
2731
2883
|
schema = assert_schema!(query)
|
2732
2884
|
pks = schema.__schema__(:primary_key)
|
2733
2885
|
expr = for pk <- pks, do: {dir, field(0, pk)}
|
2734
|
- %QueryExpr{expr: expr, file: __ENV__.file, line: __ENV__.line}
|
2886
|
+ %ByExpr{expr: expr, file: __ENV__.file, line: __ENV__.line}
|
2735
2887
|
end
|
2736
2888
|
|
2737
2889
|
defp assert_schema!(%{from: %Ecto.Query.FromExpr{source: {_source, schema}}})
|
|
@@ -2745,7 +2897,8 @@ defmodule Ecto.Query do
|
2745
2897
|
@doc """
|
2746
2898
|
Returns `true` if the query has a binding with the given name, otherwise `false`.
|
2747
2899
|
|
2748
|
- For more information on named bindings see "Named bindings" in this module doc.
|
2900
|
+ For more information on named bindings see ["Named bindings"](#module-named-bindings)
|
2901
|
+ in this module doc.
|
2749
2902
|
"""
|
2750
2903
|
def has_named_binding?(%Ecto.Query{aliases: aliases}, key) do
|
2751
2904
|
Map.has_key?(aliases, key)
|
|
@@ -2779,11 +2932,12 @@ defmodule Ecto.Query do
|
2779
2932
|
|
2780
2933
|
With this function it can be simplified to:
|
2781
2934
|
|
2782
|
- with_named_binding(query, :comments, fn query, binding ->
|
2935
|
+ with_named_binding(query, :comments, fn query, binding ->
|
2783
2936
|
join(query, :left, [p], a in assoc(p, ^binding), as: ^binding)
|
2784
2937
|
end)
|
2785
2938
|
|
2786
|
- For more information on named bindings see "Named bindings" in this module doc or `has_named_binding?/2`.
|
2939
|
+ For more information on named bindings see ["Named bindings"](#module-named-bindings)
|
2940
|
+ in this module doc or `has_named_binding?/2`.
|
2787
2941
|
"""
|
2788
2942
|
def with_named_binding(%Ecto.Query{} = query, key, fun) do
|
2789
2943
|
if has_named_binding?(query, key) do
|
|
@@ -2823,6 +2977,13 @@ defmodule Ecto.Query do
|
2823
2977
|
"callback function for with_named_binding/3 should return an Ecto.Query struct, got: #{inspect(other)}"
|
2824
2978
|
end
|
2825
2979
|
|
2980
|
+ @doc """
|
2981
|
+ The same as `has_named_binding?/2` but allowed in guards.
|
2982
|
+ """
|
2983
|
+ @doc guard: true
|
2984
|
+ defguard is_named_binding(query, name)
|
2985
|
+ when is_struct(query, Ecto.Query) and is_map_key(query.aliases, name)
|
2986
|
+
|
2826
2987
|
@doc """
|
2827
2988
|
Reverses the ordering of the query.
|
changed
lib/ecto/query/api.ex
|
@@ -48,62 +48,62 @@ defmodule Ecto.Query.API do
|
48
48
|
@doc """
|
49
49
|
Binary `==` operation.
|
50
50
|
"""
|
51
|
- def left == right, do: doc! [left, right]
|
51
|
+ def left == right, do: doc!([left, right])
|
52
52
|
|
53
53
|
@doc """
|
54
54
|
Binary `!=` operation.
|
55
55
|
"""
|
56
|
- def left != right, do: doc! [left, right]
|
56
|
+ def left != right, do: doc!([left, right])
|
57
57
|
|
58
58
|
@doc """
|
59
59
|
Binary `<=` operation.
|
60
60
|
"""
|
61
|
- def left <= right, do: doc! [left, right]
|
61
|
+ def left <= right, do: doc!([left, right])
|
62
62
|
|
63
63
|
@doc """
|
64
64
|
Binary `>=` operation.
|
65
65
|
"""
|
66
|
- def left >= right, do: doc! [left, right]
|
66
|
+ def left >= right, do: doc!([left, right])
|
67
67
|
|
68
68
|
@doc """
|
69
69
|
Binary `<` operation.
|
70
70
|
"""
|
71
|
- def left < right, do: doc! [left, right]
|
71
|
+ def left < right, do: doc!([left, right])
|
72
72
|
|
73
73
|
@doc """
|
74
74
|
Binary `>` operation.
|
75
75
|
"""
|
76
|
- def left > right, do: doc! [left, right]
|
76
|
+ def left > right, do: doc!([left, right])
|
77
77
|
|
78
78
|
@doc """
|
79
79
|
Binary `+` operation.
|
80
80
|
"""
|
81
|
- def left + right, do: doc! [left, right]
|
81
|
+ def left + right, do: doc!([left, right])
|
82
82
|
|
83
83
|
@doc """
|
84
84
|
Binary `-` operation.
|
85
85
|
"""
|
86
|
- def left - right, do: doc! [left, right]
|
86
|
+ def left - right, do: doc!([left, right])
|
87
87
|
|
88
88
|
@doc """
|
89
89
|
Binary `*` operation.
|
90
90
|
"""
|
91
|
- def left * right, do: doc! [left, right]
|
91
|
+ def left * right, do: doc!([left, right])
|
92
92
|
|
93
93
|
@doc """
|
94
94
|
Binary `/` operation.
|
95
95
|
"""
|
96
|
- def left / right, do: doc! [left, right]
|
96
|
+ def left / right, do: doc!([left, right])
|
97
97
|
|
98
98
|
@doc """
|
99
99
|
Binary `and` operation.
|
100
100
|
"""
|
101
|
- def left and right, do: doc! [left, right]
|
101
|
+ def left and right, do: doc!([left, right])
|
102
102
|
|
103
103
|
@doc """
|
104
104
|
Binary `or` operation.
|
105
105
|
"""
|
106
|
- def left or right, do: doc! [left, right]
|
106
|
+ def left or right, do: doc!([left, right])
|
107
107
|
|
108
108
|
@doc """
|
109
109
|
Unary `not` operation.
|
|
@@ -128,15 +128,16 @@ defmodule Ecto.Query.API do
|
128
128
|
)
|
129
129
|
|
130
130
|
"""
|
131
|
- def not(value), do: doc! [value]
|
131
|
+ def not value, do: doc!([value])
|
132
132
|
|
133
133
|
@doc """
|
134
134
|
Checks if the left-value is included in the right one.
|
135
135
|
|
136
136
|
from p in Post, where: p.id in [1, 2, 3]
|
137
137
|
|
138
|
- The right side may either be a list, a literal list
|
139
|
- or even a column in the database with array type:
|
138
|
+ The right side may either be a literal list, an interpolated list,
|
139
|
+ any struct that implements the `Enumerable` protocol, or even a
|
140
|
+ column in the database with array type:
|
140
141
|
|
141
142
|
from p in Post, where: "elixir" in p.tags
|
142
143
|
|
|
@@ -147,7 +148,7 @@ defmodule Ecto.Query.API do
|
147
148
|
from(p in Post, where: p.created_at > ^since, select: p.id)
|
148
149
|
)
|
149
150
|
"""
|
150
|
- def left in right, do: doc! [left, right]
|
151
|
+ def left in right, do: doc!([left, right])
|
151
152
|
|
152
153
|
@doc """
|
153
154
|
Evaluates to true if the provided subquery returns 1 or more rows.
|
|
@@ -168,7 +169,7 @@ defmodule Ecto.Query.API do
|
168
169
|
In the above example the query returns posts which have at least one comment that
|
169
170
|
has more than 5 replies.
|
170
171
|
"""
|
171
|
- def exists(subquery), do: doc! [subquery]
|
172
|
+ def exists(subquery), do: doc!([subquery])
|
172
173
|
|
173
174
|
@doc """
|
174
175
|
Tests whether one or more values returned from the provided subquery match in a comparison operation.
|
|
@@ -183,7 +184,7 @@ defmodule Ecto.Query.API do
|
183
184
|
Both `any` and `all` must be given a subquery as an argument, and they must be used on the right hand side of a comparison.
|
184
185
|
Both can be used with every comparison operator: `==`, `!=`, `>`, `>=`, `<`, `<=`.
|
185
186
|
"""
|
186
|
- def any(subquery), do: doc! [subquery]
|
187
|
+ def any(subquery), do: doc!([subquery])
|
187
188
|
|
188
189
|
@doc """
|
189
190
|
Evaluates whether all values returned from the provided subquery match in a comparison operation.
|
|
@@ -203,7 +204,7 @@ defmodule Ecto.Query.API do
|
203
204
|
Both `any` and `all` must be given a subquery as an argument, and they must be used on the right hand side of a comparison.
|
204
205
|
Both can be used with every comparison operator: `==`, `!=`, `>`, `>=`, `<`, `<=`.
|
205
206
|
"""
|
206
|
- def all(subquery), do: doc! [subquery]
|
207
|
+ def all(subquery), do: doc!([subquery])
|
207
208
|
|
208
209
|
@doc """
|
209
210
|
Searches for `search` in `string`.
|
|
@@ -220,7 +221,7 @@ defmodule Ecto.Query.API do
|
220
221
|
as part of LIKE query, since they allow to perform
|
221
222
|
[LIKE-injections](https://githubengineering.com/like-injection/).
|
222
223
|
"""
|
223
|
- def like(string, search), do: doc! [string, search]
|
224
|
+ def like(string, search), do: doc!([string, search])
|
224
225
|
|
225
226
|
@doc """
|
226
227
|
Searches for `search` in `string` in a case insensitive fashion.
|
|
@@ -230,7 +231,7 @@ defmodule Ecto.Query.API do
|
230
231
|
Translates to the underlying SQL ILIKE query. This operation is
|
231
232
|
only available on PostgreSQL.
|
232
233
|
"""
|
233
|
- def ilike(string, search), do: doc! [string, search]
|
234
|
+ def ilike(string, search), do: doc!([string, search])
|
234
235
|
|
235
236
|
@doc """
|
236
237
|
Checks if the given value is nil.
|
|
@@ -241,31 +242,31 @@ defmodule Ecto.Query.API do
|
241
242
|
|
242
243
|
from p in Post, where: not is_nil(p.published_at)
|
243
244
|
"""
|
244
|
- def is_nil(value), do: doc! [value]
|
245
|
+ def is_nil(value), do: doc!([value])
|
245
246
|
|
246
247
|
@doc """
|
247
248
|
Counts the entries in the table.
|
248
249
|
|
249
250
|
from p in Post, select: count()
|
250
251
|
"""
|
251
|
- def count, do: doc! []
|
252
|
+ def count, do: doc!([])
|
252
253
|
|
253
254
|
@doc """
|
254
255
|
Counts the given entry.
|
255
256
|
|
256
257
|
from p in Post, select: count(p.id)
|
257
258
|
"""
|
258
|
- def count(value), do: doc! [value]
|
259
|
+ def count(value), do: doc!([value])
|
259
260
|
|
260
261
|
@doc """
|
261
262
|
Counts the distinct values in given entry.
|
262
263
|
|
263
264
|
from p in Post, select: count(p.id, :distinct)
|
264
265
|
"""
|
265
|
- def count(value, :distinct), do: doc! [value, :distinct]
|
266
|
+ def count(value, :distinct), do: doc!([value, :distinct])
|
266
267
|
|
267
268
|
@doc """
|
268
|
- Takes whichever value is not null, or null if they both are.
|
269
|
+ Takes the first value which is not null, or null if they both are.
|
269
270
|
|
270
271
|
In SQL, COALESCE takes any number of arguments, but in ecto
|
271
272
|
it only takes two, so it must be chained to achieve the same
|
|
@@ -273,7 +274,7 @@ defmodule Ecto.Query.API do
|
273
274
|
|
274
275
|
from p in Payment, select: p.value |> coalesce(p.backup_value) |> coalesce(0)
|
275
276
|
"""
|
276
|
- def coalesce(value, expr), do: doc! [value, expr]
|
277
|
+ def coalesce(value, expr), do: doc!([value, expr])
|
277
278
|
|
278
279
|
@doc """
|
279
280
|
Applies the given expression as a FILTER clause against an
|
|
@@ -283,35 +284,35 @@ defmodule Ecto.Query.API do
|
283
284
|
|
284
285
|
from p in Payment, select: avg(p.value) |> filter(p.value < 0)
|
285
286
|
"""
|
286
|
- def filter(value, filter), do: doc! [value, filter]
|
287
|
+ def filter(value, filter), do: doc!([value, filter])
|
287
288
|
|
288
289
|
@doc """
|
289
290
|
Calculates the average for the given entry.
|
290
291
|
|
291
292
|
from p in Payment, select: avg(p.value)
|
292
293
|
"""
|
293
|
- def avg(value), do: doc! [value]
|
294
|
+ def avg(value), do: doc!([value])
|
294
295
|
|
295
296
|
@doc """
|
296
297
|
Calculates the sum for the given entry.
|
297
298
|
|
298
299
|
from p in Payment, select: sum(p.value)
|
299
300
|
"""
|
300
|
- def sum(value), do: doc! [value]
|
301
|
+ def sum(value), do: doc!([value])
|
301
302
|
|
302
303
|
@doc """
|
303
304
|
Calculates the minimum for the given entry.
|
304
305
|
|
305
306
|
from p in Payment, select: min(p.value)
|
306
307
|
"""
|
307
|
- def min(value), do: doc! [value]
|
308
|
+ def min(value), do: doc!([value])
|
308
309
|
|
309
310
|
@doc """
|
310
311
|
Calculates the maximum for the given entry.
|
311
312
|
|
312
313
|
from p in Payment, select: max(p.value)
|
313
314
|
"""
|
314
|
- def max(value), do: doc! [value]
|
315
|
+ def max(value), do: doc!([value])
|
315
316
|
|
316
317
|
@doc """
|
317
318
|
Adds a given interval to a datetime.
|
|
@@ -330,7 +331,7 @@ defmodule Ecto.Query.API do
|
330
331
|
|
331
332
|
See [Intervals](#module-intervals) for supported `interval` values.
|
332
333
|
"""
|
333
|
- def datetime_add(datetime, count, interval), do: doc! [datetime, count, interval]
|
334
|
+ def datetime_add(datetime, count, interval), do: doc!([datetime, count, interval])
|
334
335
|
|
335
336
|
@doc """
|
336
337
|
Adds a given interval to a date.
|
|
@@ -339,7 +340,7 @@ defmodule Ecto.Query.API do
|
339
340
|
|
340
341
|
See [Intervals](#module-intervals) for supported `interval` values.
|
341
342
|
"""
|
342
|
- def date_add(date, count, interval), do: doc! [date, count, interval]
|
343
|
+ def date_add(date, count, interval), do: doc!([date, count, interval])
|
343
344
|
|
344
345
|
@doc """
|
345
346
|
Adds the given interval to the current time in UTC.
|
|
@@ -354,7 +355,7 @@ defmodule Ecto.Query.API do
|
354
355
|
from a in Account, where: a.expires_at < from_now(3, "month")
|
355
356
|
|
356
357
|
"""
|
357
|
- def from_now(count, interval), do: doc! [count, interval]
|
358
|
+ def from_now(count, interval), do: doc!([count, interval])
|
358
359
|
|
359
360
|
@doc """
|
360
361
|
Subtracts the given interval from the current time in UTC.
|
|
@@ -368,7 +369,7 @@ defmodule Ecto.Query.API do
|
368
369
|
|
369
370
|
from p in Post, where: p.published_at > ago(3, "month")
|
370
371
|
"""
|
371
|
- def ago(count, interval), do: doc! [count, interval]
|
372
|
+ def ago(count, interval), do: doc!([count, interval])
|
372
373
|
|
373
374
|
@doc """
|
374
375
|
Send fragments directly to the database.
|
|
@@ -476,7 +477,7 @@ defmodule Ecto.Query.API do
|
476
477
|
where: fragment(title: ["$eq": ^some_value])
|
477
478
|
|
478
479
|
"""
|
479
|
- def fragment(fragments), do: doc! [fragments]
|
480
|
+ def fragment(fragments), do: doc!([fragments])
|
480
481
|
|
481
482
|
@doc """
|
482
483
|
Allows a literal identifier to be injected into a fragment:
|
|
@@ -489,7 +490,7 @@ defmodule Ecto.Query.API do
|
489
490
|
each different value of `collation` will emit a different query,
|
490
491
|
which will be independently prepared and cached.
|
491
492
|
"""
|
492
|
- def literal(binary), do: doc! [binary]
|
493
|
+ def literal(binary), do: doc!([binary])
|
493
494
|
|
494
495
|
@doc """
|
495
496
|
Allows a list argument to be spliced into a fragment.
|
|
@@ -499,8 +500,13 @@ defmodule Ecto.Query.API do
|
499
500
|
The example above will be transformed at runtime into the following:
|
500
501
|
|
501
502
|
from p in Post, where: fragment("? in (?,?,?)", p.id, ^1, ^2, ^3)
|
503
|
+
|
504
|
+ You may only splice runtime values. For example, this would not work because
|
505
|
+ query bindings are compile-time constructs:
|
506
|
+
|
507
|
+ from p in Post, where: fragment("concat(?)", splice(^[p.count, " ", "count"])
|
502
508
|
"""
|
503
|
- def splice(list), do: doc! [list]
|
509
|
+ def splice(list), do: doc!([list])
|
504
510
|
|
505
511
|
@doc """
|
506
512
|
Creates a values list/constant table.
|
|
@@ -515,6 +521,8 @@ defmodule Ecto.Query.API do
|
515
521
|
Each field must be given a type or an error is raised. Any type that can be specified in
|
516
522
|
a schema may be used.
|
517
523
|
|
524
|
+ Queries using a values list are not cacheable by Ecto.
|
525
|
+
|
518
526
|
## Select example
|
519
527
|
|
520
528
|
values = [%{id: 1, text: "abc"}, %{id: 2, text: "xyz"}]
|
|
@@ -551,7 +559,7 @@ defmodule Ecto.Query.API do
|
551
559
|
|
552
560
|
Repo.update_all(query, [])
|
553
561
|
"""
|
554
|
- def values(values, types), do: doc! [values, types]
|
562
|
+ def values(values, types), do: doc!([values, types])
|
555
563
|
|
556
564
|
@doc """
|
557
565
|
Allows a field to be dynamically accessed.
|
|
@@ -564,7 +572,7 @@ defmodule Ecto.Query.API do
|
564
572
|
In the example above, both `at_least_four(:doors)` and `at_least_four(:tires)`
|
565
573
|
would be valid calls as the field is dynamically generated.
|
566
574
|
"""
|
567
|
- def field(source, field), do: doc! [source, field]
|
575
|
+ def field(source, field), do: doc!([source, field])
|
568
576
|
|
569
577
|
@doc """
|
570
578
|
Used in `select` to specify which struct fields should be returned.
|
|
@@ -612,7 +620,7 @@ defmodule Ecto.Query.API do
|
612
620
|
MUST include the foreign keys used in the relationship,
|
613
621
|
otherwise Ecto will be unable to find associated records.
|
614
622
|
"""
|
615
|
- def struct(source, fields), do: doc! [source, fields]
|
623
|
+ def struct(source, fields), do: doc!([source, fields])
|
616
624
|
|
617
625
|
@doc """
|
618
626
|
Used in `select` to specify which fields should be returned as a map.
|
|
@@ -655,7 +663,7 @@ defmodule Ecto.Query.API do
|
655
663
|
MUST include the foreign keys used in the relationship,
|
656
664
|
otherwise Ecto will be unable to find associated records.
|
657
665
|
"""
|
658
|
- def map(source, fields), do: doc! [source, fields]
|
666
|
+ def map(source, fields), do: doc!([source, fields])
|
659
667
|
|
660
668
|
@doc """
|
661
669
|
Merges the map on the right over the map on the left.
|
|
@@ -669,7 +677,7 @@ defmodule Ecto.Query.API do
|
669
677
|
This function is primarily used by `Ecto.Query.select_merge/3`
|
670
678
|
to merge different select clauses.
|
671
679
|
"""
|
672
|
- def merge(left_map, right_map), do: doc! [left_map, right_map]
|
680
|
+ def merge(left_map, right_map), do: doc!([left_map, right_map])
|
673
681
|
|
674
682
|
@doc """
|
675
683
|
Returns value from the `json_field` pointed to by `path`.
|
|
@@ -725,7 +733,7 @@ defmodule Ecto.Query.API do
|
725
733
|
tries to compare incompatible types. You can, however, use `type/2`
|
726
734
|
to force the types on the database level.
|
727
735
|
"""
|
728
|
- def json_extract_path(json_field, path), do: doc! [json_field, path]
|
736
|
+ def json_extract_path(json_field, path), do: doc!([json_field, path])
|
729
737
|
|
730
738
|
@doc """
|
731
739
|
Casts the given value to the given type at the database level.
|
|
@@ -779,15 +787,21 @@ defmodule Ecto.Query.API do
|
779
787
|
child = from c in Comment, where: type(parent_as(:posts).id, :string) == c.text
|
780
788
|
from Post, as: :posts, inner_lateral_join: c in subquery(child), select: c.text
|
781
789
|
|
790
|
+ ## `type` vs `fragment`
|
791
|
+
|
792
|
+ `type/2` is all about Ecto types. Therefore, you can perform `type(expr, :string)`
|
793
|
+ but not `type(expr, :text)`, because `:text` is not an actual Ecto type. If you want
|
794
|
+ to perform casting exclusively at the database level, you can use fragment. For example,
|
795
|
+ in PostgreSQL, you might do `fragment("?::text", p.column)`.
|
782
796
|
"""
|
783
|
- def type(interpolated_value, type), do: doc! [interpolated_value, type]
|
797
|
+ def type(interpolated_value, type), do: doc!([interpolated_value, type])
|
784
798
|
|
785
799
|
@doc """
|
786
800
|
Refer to a named atom binding.
|
787
801
|
|
788
802
|
See the "Named binding" section in `Ecto.Query` for more information.
|
789
803
|
"""
|
790
|
- def as(binding), do: doc! [binding]
|
804
|
+ def as(binding), do: doc!([binding])
|
791
805
|
|
792
806
|
@doc """
|
793
807
|
Refer to a named atom binding in the parent query.
|
|
@@ -796,7 +810,7 @@ defmodule Ecto.Query.API do
|
796
810
|
|
797
811
|
See the "Named binding" section in `Ecto.Query` for more information.
|
798
812
|
"""
|
799
|
- def parent_as(binding), do: doc! [binding]
|
813
|
+ def parent_as(binding), do: doc!([binding])
|
800
814
|
|
801
815
|
@doc """
|
802
816
|
Refer to an alias of a selected value.
|
|
@@ -809,7 +823,7 @@ defmodule Ecto.Query.API do
|
809
823
|
referenced somewhere that is not allowed. Consult the documentation for the database
|
810
824
|
to ensure the alias is being referenced correctly.
|
811
825
|
"""
|
812
|
- def selected_as(name), do: doc! [name]
|
826
|
+ def selected_as(name), do: doc!([name])
|
813
827
|
|
814
828
|
@doc """
|
815
829
|
Creates an alias for the given selected value.
|
|
@@ -861,10 +875,10 @@ defmodule Ecto.Query.API do
|
861
875
|
The name given to `selected_as/2` can also be referenced in `selected_as/1`,
|
862
876
|
as in regular queries.
|
863
877
|
"""
|
864
|
- def selected_as(selected_value, name), do: doc! [selected_value, name]
|
878
|
+ def selected_as(selected_value, name), do: doc!([selected_value, name])
|
865
879
|
|
866
880
|
defp doc!(_) do
|
867
881
|
raise "the functions in Ecto.Query.API should not be invoked directly, " <>
|
868
|
- "they serve for documentation purposes only"
|
882
|
+ "they serve for documentation purposes only"
|
869
883
|
end
|
870
884
|
end
|
changed
lib/ecto/query/builder.ex
|
@@ -52,7 +52,7 @@ defmodule Ecto.Query.Builder do
|
52
52
|
which include all primitive types and custom user types. Also
|
53
53
|
note custom user types do not show up during compilation time.
|
54
54
|
"""
|
55
|
- @type quoted_type :: Ecto.Type.primitive | {non_neg_integer, atom | Macro.t}
|
55
|
+ @type quoted_type :: Ecto.Type.primitive() | {non_neg_integer, atom | Macro.t()}
|
56
56
|
|
57
57
|
@typedoc """
|
58
58
|
The accumulator during escape.
|
|
@@ -74,12 +74,18 @@ defmodule Ecto.Query.Builder do
|
74
74
|
with `^index` in the query where index is a number indexing into the
|
75
75
|
map.
|
76
76
|
"""
|
77
|
- @spec escape(Macro.t, quoted_type | {:in, quoted_type} | {:out, quoted_type} | {:splice, quoted_type},
|
78
|
- {list, acc}, Keyword.t, Macro.Env.t | {Macro.Env.t, fun}) :: {Macro.t, {list, acc}}
|
77
|
+ @spec escape(
|
78
|
+ Macro.t(),
|
79
|
+ quoted_type | {:in, quoted_type} | {:out, quoted_type} | {:splice, quoted_type},
|
80
|
+ {list, acc},
|
81
|
+ Keyword.t(),
|
82
|
+ Macro.Env.t() | {Macro.Env.t(), fun}
|
83
|
+ ) :: {Macro.t(), {list, acc}}
|
79
84
|
def escape(expr, type, params_acc, vars, env)
|
80
85
|
|
81
86
|
# var.x - where var is bound
|
82
|
- def escape({{:., _, [callee, field]}, _, []}, _type, params_acc, vars, _env) when is_atom(field) do
|
87
|
+ def escape({{:., _, [callee, field]}, _, []}, _type, params_acc, vars, _env)
|
88
|
+ when is_atom(field) do
|
83
89
|
{escape_field!(callee, field, vars), params_acc}
|
84
90
|
end
|
85
91
|
|
|
@@ -103,7 +109,13 @@ defmodule Ecto.Query.Builder do
|
103
109
|
{expr, {params, acc}}
|
104
110
|
end
|
105
111
|
|
106
|
- def escape({:type, _, [{{:., _, [{var, _, context}, field]}, _, []} = expr, type]}, _type, params_acc, vars, env)
|
112
|
+ def escape(
|
113
|
+ {:type, _, [{{:., _, [{var, _, context}, field]}, _, []} = expr, type]},
|
114
|
+ _type,
|
115
|
+ params_acc,
|
116
|
+ vars,
|
117
|
+ env
|
118
|
+ )
|
107
119
|
when is_atom(var) and is_atom(context) and is_atom(field) do
|
108
120
|
escape_with_type(expr, type, params_acc, vars, env)
|
109
121
|
end
|
|
@@ -126,22 +138,40 @@ defmodule Ecto.Query.Builder do
|
126
138
|
escape_with_type(expr, type, params_acc, vars, env)
|
127
139
|
end
|
128
140
|
|
129
|
- def escape({:type, _, [{:json_extract_path, _, [_ | _]} = expr, type]}, _type, params_acc, vars, env) do
|
141
|
+ def escape(
|
142
|
+ {:type, _, [{:json_extract_path, _, [_ | _]} = expr, type]},
|
143
|
+ _type,
|
144
|
+ params_acc,
|
145
|
+ vars,
|
146
|
+ env
|
147
|
+ ) do
|
130
148
|
escape_with_type(expr, type, params_acc, vars, env)
|
131
149
|
end
|
132
150
|
|
133
|
- def escape({:type, _, [{{:., _, [Access, :get]}, _, _} = access_expr, type]}, _type, params_acc, vars, env) do
|
151
|
+ def escape(
|
152
|
+ {:type, _, [{{:., _, [Access, :get]}, _, _} = access_expr, type]},
|
153
|
+ _type,
|
154
|
+ params_acc,
|
155
|
+ vars,
|
156
|
+ env
|
157
|
+ ) do
|
134
158
|
escape_with_type(access_expr, type, params_acc, vars, env)
|
135
159
|
end
|
136
160
|
|
137
|
- def escape({:type, _, [{{:., _, [{:parent_as, _, [_parent]}, _field]}, _, []} = expr, type]}, _type, params_acc, vars, env) do
|
161
|
+ def escape(
|
162
|
+ {:type, _, [{{:., _, [{:parent_as, _, [_parent]}, _field]}, _, []} = expr, type]},
|
163
|
+ _type,
|
164
|
+ params_acc,
|
165
|
+ vars,
|
166
|
+ env
|
167
|
+ ) do
|
138
168
|
escape_with_type(expr, type, params_acc, vars, env)
|
139
169
|
end
|
140
170
|
|
141
171
|
def escape({:type, meta, [expr, type]}, given_type, params_acc, vars, env) do
|
142
172
|
case Macro.expand_once(expr, get_env(env)) do
|
143
173
|
^expr ->
|
144
|
- error! """
|
174
|
+ error!("""
|
145
175
|
the first argument of type/2 must be one of:
|
146
176
|
|
147
177
|
* interpolations, such as ^value
|
|
@@ -154,7 +184,7 @@ defmodule Ecto.Query.Builder do
|
154
184
|
* parent_as/1 (parent_as(:parent).field)
|
155
185
|
|
156
186
|
Got: #{Macro.to_string(expr)}
|
157
|
- """
|
187
|
+ """)
|
158
188
|
|
159
189
|
expanded ->
|
160
190
|
escape({:type, meta, [expanded, type]}, given_type, params_acc, vars, env)
|
|
@@ -165,6 +195,7 @@ defmodule Ecto.Query.Builder do
|
165
195
|
def escape({:fragment, _, [query]}, _type, params_acc, vars, env) when is_list(query) do
|
166
196
|
{escaped, params_acc} =
|
167
197
|
Enum.map_reduce(query, params_acc, &escape_kw_fragment(&1, &2, vars, env))
|
198
|
+
|
168
199
|
{{:{}, [], [:fragment, [], [escaped]]}, params_acc}
|
169
200
|
end
|
170
201
|
|
|
@@ -177,8 +208,10 @@ defmodule Ecto.Query.Builder do
|
177
208
|
pieces = expand_and_split_fragment(query, env)
|
178
209
|
|
179
210
|
if length(pieces) != length(frags) + 1 do
|
180
|
- error! "fragment(...) expects extra arguments in the same amount of question marks in string. " <>
|
181
|
- "It received #{length(frags)} extra argument(s) but expected #{length(pieces) - 1}"
|
211
|
+ error!(
|
212
|
+ "fragment(...) expects extra arguments in the same amount of question marks in string. " <>
|
213
|
+ "It received #{length(frags)} extra argument(s) but expected #{length(pieces) - 1}"
|
214
|
+ )
|
182
215
|
end
|
183
216
|
|
184
217
|
{frags, params_acc} = Enum.map_reduce(frags, params_acc, &escape_fragment(&1, &2, vars, env))
|
|
@@ -204,20 +237,23 @@ defmodule Ecto.Query.Builder do
|
204
237
|
|
205
238
|
def escape({:ago, meta, [count, interval]}, type, params_acc, vars, env) do
|
206
239
|
utc = quote do: ^DateTime.utc_now()
|
240
|
+
|
207
241
|
count =
|
208
242
|
case count do
|
209
243
|
{:^, meta, [value]} ->
|
210
244
|
negate = quote do: Ecto.Query.Builder.negate!(unquote(value))
|
211
245
|
{:^, meta, [negate]}
|
246
|
+
|
212
247
|
value ->
|
213
248
|
{:-, [], [value]}
|
214
249
|
end
|
250
|
+
|
215
251
|
escape({:datetime_add, meta, [utc, count, interval]}, type, params_acc, vars, env)
|
216
252
|
end
|
217
253
|
|
218
254
|
def escape({:datetime_add, _, [datetime, count, interval]} = expr, type, params_acc, vars, env) do
|
219
|
- assert_type!(expr, type, {:param, :any_datetime})
|
220
|
- {datetime, params_acc} = escape(datetime, {:param, :any_datetime}, params_acc, vars, env)
|
255
|
+ assert_type!(expr, type, {:supertype, :datetime})
|
256
|
+ {datetime, params_acc} = escape(datetime, {:supertype, :datetime}, params_acc, vars, env)
|
221
257
|
{count, interval, params_acc} = escape_interval(count, interval, params_acc, vars, env)
|
222
258
|
{{:{}, [], [:datetime_add, [], [datetime, count, interval]]}, params_acc}
|
223
259
|
end
|
|
@@ -257,7 +293,7 @@ defmodule Ecto.Query.Builder do
|
257
293
|
|
258
294
|
# lists
|
259
295
|
def escape(list, type, params_acc, vars, env) when is_list(list) do
|
260
|
- if Enum.all?(list, &is_binary(&1) or is_number(&1) or is_boolean(&1)) do
|
296
|
+ if Enum.all?(list, &(is_binary(&1) or is_number(&1) or is_boolean(&1))) do
|
261
297
|
{literal(list, type, vars), params_acc}
|
262
298
|
else
|
263
299
|
fun =
|
|
@@ -278,15 +314,18 @@ defmodule Ecto.Query.Builder do
|
278
314
|
|
279
315
|
# literals
|
280
316
|
def escape({:<<>>, _, args} = expr, type, params_acc, vars, _env) do
|
281
|
- valid? = Enum.all?(args, fn
|
282
|
- {:"::", _, [left, _]} -> is_integer(left) or is_binary(left)
|
283
|
- left -> is_integer(left) or is_binary(left)
|
284
|
- end)
|
317
|
+ valid? =
|
318
|
+ Enum.all?(args, fn
|
319
|
+ {:"::", _, [left, _]} -> is_integer(left) or is_binary(left)
|
320
|
+ left -> is_integer(left) or is_binary(left)
|
321
|
+ end)
|
285
322
|
|
286
323
|
unless valid? do
|
287
|
- error! "`#{Macro.to_string(expr)}` is not a valid query expression. " <>
|
288
|
- "Only literal binaries and strings are allowed, " <>
|
289
|
- "dynamic values need to be explicitly interpolated in queries with ^"
|
324
|
+ error!(
|
325
|
+ "`#{Macro.to_string(expr)}` is not a valid query expression. " <>
|
326
|
+ "Only literal binaries and strings are allowed, " <>
|
327
|
+ "dynamic values need to be explicitly interpolated in queries with ^"
|
328
|
+ )
|
290
329
|
end
|
291
330
|
|
292
331
|
{literal(expr, type, vars), params_acc}
|
|
@@ -294,12 +333,16 @@ defmodule Ecto.Query.Builder do
|
294
333
|
|
295
334
|
def escape({:-, _, [number]}, type, params_acc, vars, _env) when is_number(number),
|
296
335
|
do: {literal(-number, type, vars), params_acc}
|
336
|
+
|
297
337
|
def escape(number, type, params_acc, vars, _env) when is_number(number),
|
298
338
|
do: {literal(number, type, vars), params_acc}
|
339
|
+
|
299
340
|
def escape(binary, type, params_acc, vars, _env) when is_binary(binary),
|
300
341
|
do: {literal(binary, type, vars), params_acc}
|
342
|
+
|
301
343
|
def escape(nil, _type, params_acc, _vars, _env),
|
302
344
|
do: {nil, params_acc}
|
345
|
+
|
303
346
|
def escape(atom, type, params_acc, vars, _env) when is_atom(atom),
|
304
347
|
do: {literal(atom, type, vars), params_acc}
|
305
348
|
|
|
@@ -316,25 +359,26 @@ defmodule Ecto.Query.Builder do
|
316
359
|
assert_type!(expr, type, :boolean)
|
317
360
|
|
318
361
|
if is_nil(left) or is_nil(right) do
|
319
|
- error! "comparison with nil is forbidden as it is unsafe. " <>
|
320
|
- "If you want to check if a value is nil, use is_nil/1 instead"
|
362
|
+ error!(
|
363
|
+ "comparison with nil is forbidden as it is unsafe. " <>
|
364
|
+ "If you want to check if a value is nil, use is_nil/1 instead"
|
365
|
+ )
|
321
366
|
end
|
322
367
|
|
323
368
|
ltype = quoted_type(right, vars)
|
324
369
|
rtype = quoted_type(left, vars)
|
325
370
|
|
326
|
- {left, params_acc} = escape(left, ltype, params_acc, vars, env)
|
371
|
+ {left, params_acc} = escape(left, ltype, params_acc, vars, env)
|
327
372
|
{right, params_acc} = escape(right, rtype, params_acc, vars, env)
|
328
373
|
|
329
374
|
{params, acc} = params_acc
|
330
|
- {{:{}, [], [comp_op, [], [left, right]]},
|
331
|
- {params |> wrap_nil(left) |> wrap_nil(right), acc}}
|
375
|
+ {{:{}, [], [comp_op, [], [left, right]]}, {params |> wrap_nil(left) |> wrap_nil(right), acc}}
|
332
376
|
end
|
333
377
|
|
334
378
|
# mathematical operators
|
335
379
|
def escape({math_op, _, [left, right]}, type, params_acc, vars, env)
|
336
380
|
when math_op in ~w(+ - * /)a do
|
337
|
- {left, params_acc} = escape(left, type, params_acc, vars, env)
|
381
|
+ {left, params_acc} = escape(left, type, params_acc, vars, env)
|
338
382
|
{right, params_acc} = escape(right, type, params_acc, vars, env)
|
339
383
|
|
340
384
|
{{:{}, [], [math_op, [], [left, right]]}, params_acc}
|
|
@@ -404,7 +448,7 @@ defmodule Ecto.Query.Builder do
|
404
448
|
end
|
405
449
|
|
406
450
|
def escape({:selected_as, _, [_expr, _name]}, _type, _params_acc, _vars, _env) do
|
407
|
- error! """
|
451
|
+ error!("""
|
408
452
|
selected_as/2 can only be used at the root of a select statement. \
|
409
453
|
If you are trying to use it inside of an expression, consider putting the \
|
410
454
|
expression inside of `selected_as/2` instead. For instance, instead of:
|
|
@@ -414,7 +458,7 @@ defmodule Ecto.Query.Builder do
|
414
458
|
use:
|
415
459
|
|
416
460
|
from p in Post, select: selected_as(coalesce(p.visits, 0), :v)
|
417
|
- """
|
461
|
+ """)
|
418
462
|
end
|
419
463
|
|
420
464
|
def escape({:selected_as, _, [name]}, _type, params_acc, _vars, _env) do
|
|
@@ -423,20 +467,25 @@ defmodule Ecto.Query.Builder do
|
423
467
|
{expr, params_acc}
|
424
468
|
end
|
425
469
|
|
426
|
- def escape({quantifier, meta, [subquery]}, type, params_acc, vars, env) when quantifier in [:all, :any, :exists] do
|
470
|
+ def escape({quantifier, meta, [subquery]}, type, params_acc, vars, env)
|
471
|
+ when quantifier in [:all, :any, :exists] do
|
427
472
|
{subquery, params_acc} = escape({:subquery, meta, [subquery]}, type, params_acc, vars, env)
|
428
473
|
{{:{}, [], [quantifier, [], [subquery]]}, params_acc}
|
429
474
|
end
|
430
475
|
|
431
476
|
def escape({:=, _, _} = expr, _type, _params_acc, _vars, _env) do
|
432
|
- error! "`#{Macro.to_string(expr)}` is not a valid query expression. " <>
|
433
|
- "The match operator is not supported: `=`. " <>
|
434
|
- "Did you mean to use `==` instead?"
|
477
|
+ error!(
|
478
|
+ "`#{Macro.to_string(expr)}` is not a valid query expression. " <>
|
479
|
+ "The match operator is not supported: `=`. " <>
|
480
|
+ "Did you mean to use `==` instead?"
|
481
|
+ )
|
435
482
|
end
|
436
483
|
|
437
484
|
def escape({op, _, _}, _type, _params_acc, _vars, _env) when op in ~w(|| && !)a do
|
438
|
- error! "short-circuit operators are not supported: `#{op}`. " <>
|
439
|
- "Instead use boolean operators: `and`, `or`, and `not`"
|
485
|
+ error!(
|
486
|
+ "short-circuit operators are not supported: `#{op}`. " <>
|
487
|
+ "Instead use boolean operators: `and`, `or`, and `not`"
|
488
|
+ )
|
440
489
|
end
|
441
490
|
|
442
491
|
# Tuple
|
|
@@ -451,8 +500,9 @@ defmodule Ecto.Query.Builder do
|
451
500
|
list
|
452
501
|
|> Enum.zip(types)
|
453
502
|
|> Enum.map_reduce(params_acc, fn {expr, type}, params_acc ->
|
454
|
- escape(expr, type, params_acc, vars, env)
|
455
|
- end)
|
503
|
+ escape(expr, type, params_acc, vars, env)
|
504
|
+ end)
|
505
|
+
|
456
506
|
expr = {:{}, [], [:{}, [], list]}
|
457
507
|
{expr, params_acc}
|
458
508
|
else
|
|
@@ -462,7 +512,7 @@ defmodule Ecto.Query.Builder do
|
462
512
|
|
463
513
|
# Tuple
|
464
514
|
def escape({:{}, _, _}, _, _, _, _) do
|
465
|
- error! "Tuples can only be used in comparisons with literal tuples of the same size"
|
515
|
+ error!("Tuples can only be used in comparisons with literal tuples of the same size")
|
466
516
|
end
|
467
517
|
|
468
518
|
# Unnecessary parentheses around an expression
|
|
@@ -471,25 +521,28 @@ defmodule Ecto.Query.Builder do
|
471
521
|
end
|
472
522
|
|
473
523
|
# Other functions - no type casting
|
474
|
- def escape({name, _, args} = expr, type, params_acc, vars, env) when is_atom(name) and is_list(args) do
|
524
|
+ def escape({name, _, args} = expr, type, params_acc, vars, env)
|
525
|
+ when is_atom(name) and is_list(args) do
|
475
526
|
case call_type(name, length(args)) do
|
476
527
|
{in_type, out_type} ->
|
477
528
|
assert_type!(expr, type, out_type)
|
478
529
|
escape_call(expr, in_type, params_acc, vars, env)
|
530
|
+
|
479
531
|
nil ->
|
480
532
|
try_expansion(expr, type, params_acc, vars, env)
|
481
533
|
end
|
482
534
|
end
|
483
535
|
|
484
536
|
# Finally handle vars
|
485
|
- def escape({var, _, context}, _type, params_acc, vars, _env) when is_atom(var) and is_atom(context) do
|
537
|
+ def escape({var, _, context}, _type, params_acc, vars, _env)
|
538
|
+ when is_atom(var) and is_atom(context) do
|
486
539
|
{escape_var!(var, vars), params_acc}
|
487
540
|
end
|
488
541
|
|
489
542
|
# Raise nice error messages for fun calls.
|
490
543
|
def escape({fun, _, args} = other, _type, _params_acc, _vars, _env)
|
491
544
|
when is_atom(fun) and is_list(args) do
|
492
|
- error! """
|
545
|
+ error!("""
|
493
546
|
`#{Macro.to_string(other)}` is not a valid query expression. \
|
494
547
|
If you are trying to invoke a function that is not supported by Ecto, \
|
495
548
|
you can use fragments:
|
|
@@ -498,7 +551,7 @@ defmodule Ecto.Query.Builder do
|
498
551
|
|
499
552
|
See Ecto.Query.API to learn more about the supported functions and \
|
500
553
|
Ecto.Query.API.fragment/1 to learn more about fragments.
|
501
|
- """
|
554
|
+ """)
|
502
555
|
end
|
503
556
|
|
504
557
|
# Raise nice error message for remote calls
|
|
@@ -509,7 +562,7 @@ defmodule Ecto.Query.Builder do
|
509
562
|
|
510
563
|
# For everything else we raise
|
511
564
|
def escape(other, _type, _params_acc, _vars, _env) do
|
512
|
- error! "`#{Macro.to_string(other)}` is not a valid query expression"
|
565
|
+ error!("`#{Macro.to_string(other)}` is not a valid query expression")
|
513
566
|
end
|
514
567
|
|
515
568
|
defp escape_with_type(expr, {:^, _, [type]}, params_acc, vars, env) do
|
|
@@ -523,14 +576,18 @@ defmodule Ecto.Query.Builder do
|
523
576
|
{{:{}, [], [:type, [], [expr, escape_type(type)]]}, params_acc}
|
524
577
|
end
|
525
578
|
|
526
|
- defp escape_type({:parameterized, _, _} = param), do: Macro.escape(param)
|
579
|
+ defp escape_type({:parameterized, _} = param), do: Macro.escape(param)
|
527
580
|
defp escape_type(type), do: type
|
528
581
|
|
529
582
|
defp validate_json_field!({{:., _, _}, _, _}), do: :ok
|
530
583
|
defp validate_json_field!({:field, _, _}), do: :ok
|
531
|
- defp validate_json_field!(unsupported_field), do: error!("`#{Macro.to_string(unsupported_field)}` is not a valid json field")
|
532
584
|
|
533
|
- defp wrap_nil(params, {:{}, _, [:^, _, [ix]]}), do: wrap_nil(params, length(params) - ix - 1, [])
|
585
|
+ defp validate_json_field!(unsupported_field),
|
586
|
+ do: error!("`#{Macro.to_string(unsupported_field)}` is not a valid json field")
|
587
|
+
|
588
|
+ defp wrap_nil(params, {:{}, _, [:^, _, [ix]]}),
|
589
|
+ do: wrap_nil(params, length(params) - ix - 1, [])
|
590
|
+
|
534
591
|
defp wrap_nil(params, _other), do: params
|
535
592
|
|
536
593
|
defp wrap_nil([{val, type} | params], 0, acc) do
|
|
@@ -548,7 +605,7 @@ defmodule Ecto.Query.Builder do
|
548
605
|
split_fragment(binary, "")
|
549
606
|
|
550
607
|
_ ->
|
551
|
- error! bad_fragment_message(Macro.to_string(query))
|
608
|
+ error!(bad_fragment_message(Macro.to_string(query)))
|
552
609
|
end
|
553
610
|
end
|
554
611
|
|
|
@@ -559,12 +616,15 @@ defmodule Ecto.Query.Builder do
|
559
616
|
|
560
617
|
defp split_fragment(<<>>, consumed),
|
561
618
|
do: [consumed]
|
562
|
- defp split_fragment(<<??, rest :: binary>>, consumed),
|
619
|
+
|
620
|
+ defp split_fragment(<<??, rest::binary>>, consumed),
|
563
621
|
do: [consumed | split_fragment(rest, "")]
|
564
|
- defp split_fragment(<<?\\, ??, rest :: binary>>, consumed),
|
622
|
+
|
623
|
+ defp split_fragment(<<?\\, ??, rest::binary>>, consumed),
|
565
624
|
do: split_fragment(rest, consumed <> <<??>>)
|
566
|
- defp split_fragment(<<first :: utf8, rest :: binary>>, consumed),
|
567
|
- do: split_fragment(rest, consumed <> <<first :: utf8>>)
|
625
|
+
|
626
|
+ defp split_fragment(<<first::utf8, rest::binary>>, consumed),
|
627
|
+ do: split_fragment(rest, consumed <> <<first::utf8>>)
|
568
628
|
|
569
629
|
@doc "Returns fragment pieces, given a fragment string and arguments."
|
570
630
|
def fragment_pieces(frag, args) do
|
|
@@ -575,16 +635,21 @@ defmodule Ecto.Query.Builder do
|
575
635
|
|
576
636
|
defp escape_window_description([], params_acc, _vars, _env),
|
577
637
|
do: {[], params_acc}
|
578
|
- defp escape_window_description([window_name], params_acc, _vars, _env) when is_atom(window_name),
|
579
|
- do: {window_name, params_acc}
|
638
|
+
|
639
|
+ defp escape_window_description([window_name], params_acc, _vars, _env)
|
640
|
+ when is_atom(window_name),
|
641
|
+ do: {window_name, params_acc}
|
642
|
+
|
580
643
|
defp escape_window_description([kw], params_acc, vars, env) do
|
581
644
|
case Ecto.Query.Builder.Windows.escape(kw, params_acc, vars, env) do
|
582
645
|
{runtime, [], params_acc} ->
|
583
646
|
{runtime, params_acc}
|
584
647
|
|
585
648
|
{_, [{key, _} | _], _} ->
|
586
|
- error! "windows definitions given to over/2 do not allow interpolations at the root of " <>
|
587
|
- "`#{key}`. Please use Ecto.Query.windows/3 to explicitly define a window instead"
|
649
|
+ error!(
|
650
|
+ "windows definitions given to over/2 do not allow interpolations at the root of " <>
|
651
|
+ "`#{key}`. Please use Ecto.Query.windows/3 to explicitly define a window instead"
|
652
|
+ )
|
588
653
|
end
|
589
654
|
end
|
590
655
|
|
|
@@ -604,8 +669,11 @@ defmodule Ecto.Query.Builder do
|
604
669
|
else
|
605
670
|
case Macro.expand_once(expr, get_env(env)) do
|
606
671
|
^expr ->
|
607
|
- error! "unknown window function #{agg}/#{length(args)}. " <>
|
608
|
- "See Ecto.Query.WindowAPI for all available functions"
|
672
|
+ error!(
|
673
|
+ "unknown window function #{agg}/#{length(args)}. " <>
|
674
|
+ "See Ecto.Query.WindowAPI for all available functions"
|
675
|
+ )
|
676
|
+
|
609
677
|
expr ->
|
610
678
|
validate_window_function!(expr, env)
|
611
679
|
end
|
|
@@ -622,18 +690,18 @@ defmodule Ecto.Query.Builder do
|
622
690
|
|
623
691
|
defp escape_field!({var, _, context}, field, vars)
|
624
692
|
when is_atom(var) and is_atom(context) do
|
625
|
- var = escape_var!(var, vars)
|
693
|
+ var = escape_var!(var, vars)
|
626
694
|
field = quoted_atom!(field, "field/2")
|
627
|
- dot = {:{}, [], [:., [], [var, field]]}
|
695
|
+ dot = {:{}, [], [:., [], [var, field]]}
|
628
696
|
{:{}, [], [dot, [], []]}
|
629
697
|
end
|
630
698
|
|
631
699
|
defp escape_field!({kind, _, [value]}, field, _vars)
|
632
700
|
when kind in [:as, :parent_as] do
|
633
701
|
value = late_binding!(kind, value)
|
634
|
- as = {:{}, [], [kind, [], [value]]}
|
702
|
+ as = {:{}, [], [kind, [], [value]]}
|
635
703
|
field = quoted_atom!(field, "field/2")
|
636
|
- dot = {:{}, [], [:., [], [as, field]]}
|
704
|
+ dot = {:{}, [], [:., [], [as, field]]}
|
637
705
|
{:{}, [], [dot, [], []]}
|
638
706
|
end
|
639
707
|
|
|
@@ -650,17 +718,19 @@ defmodule Ecto.Query.Builder do
|
650
718
|
defp escape_interval(count, interval, params_acc, vars, env) do
|
651
719
|
type =
|
652
720
|
cond do
|
653
|
- is_float(count) -> :float
|
721
|
+ is_float(count) -> :float
|
654
722
|
is_integer(count) -> :integer
|
655
|
- true -> :decimal
|
723
|
+ true -> :decimal
|
656
724
|
end
|
657
725
|
|
658
726
|
{count, params_acc} = escape(count, type, params_acc, vars, env)
|
659
727
|
{count, quoted_interval!(interval), params_acc}
|
660
728
|
end
|
661
729
|
|
662
|
- defp escape_kw_fragment({key, [{_, _}|_] = exprs}, params_acc, vars, env) when is_atom(key) do
|
663
|
- {escaped, params_acc} = Enum.map_reduce(exprs, params_acc, &escape_kw_fragment(&1, &2, vars, env))
|
730
|
+ defp escape_kw_fragment({key, [{_, _} | _] = exprs}, params_acc, vars, env) when is_atom(key) do
|
731
|
+ {escaped, params_acc} =
|
732
|
+ Enum.map_reduce(exprs, params_acc, &escape_kw_fragment(&1, &2, vars, env))
|
733
|
+
|
664
734
|
{{key, escaped}, params_acc}
|
665
735
|
end
|
666
736
|
|
|
@@ -670,7 +740,9 @@ defmodule Ecto.Query.Builder do
|
670
740
|
end
|
671
741
|
|
672
742
|
defp escape_kw_fragment({key, _expr}, _params_acc, _vars, _env) do
|
673
|
- error! "fragment(...) with keywords accepts only atoms as keys, got `#{Macro.to_string(key)}`"
|
743
|
+ error!(
|
744
|
+ "fragment(...) with keywords accepts only atoms as keys, got `#{Macro.to_string(key)}`"
|
745
|
+ )
|
674
746
|
end
|
675
747
|
|
676
748
|
defp escape_fragment({:literal, _meta, [expr]}, params_acc, _vars, _env) do
|
|
@@ -681,7 +753,9 @@ defmodule Ecto.Query.Builder do
|
681
753
|
{escaped, params_acc}
|
682
754
|
|
683
755
|
_ ->
|
684
|
- error! "literal/1 in fragment expects an interpolated value, such as literal(^value), got `#{Macro.to_string(expr)}`"
|
756
|
+ error!(
|
757
|
+ "literal/1 in fragment expects an interpolated value, such as literal(^value), got `#{Macro.to_string(expr)}`"
|
758
|
+ )
|
685
759
|
end
|
686
760
|
end
|
687
761
|
|
|
@@ -691,11 +765,13 @@ defmodule Ecto.Query.Builder do
|
691
765
|
checked = quote do: Ecto.Query.Builder.splice!(unquote(value))
|
692
766
|
length = quote do: length(unquote(checked))
|
693
767
|
{expr, params_acc} = escape(expr, {:splice, :any}, params_acc, vars, env)
|
694
|
- escaped = {:{}, [], [:splice, [], [expr, length]]}
|
768
|
+ escaped = {:{}, [], [:splice, [], [expr, length]]}
|
695
769
|
{escaped, params_acc}
|
696
770
|
|
697
771
|
_ ->
|
698
|
- error! "splice/1 in fragment expects an interpolated value, such as splice(^value), got `#{Macro.to_string(splice)}`"
|
772
|
+ error!(
|
773
|
+ "splice/1 in fragment expects an interpolated value, such as splice(^value), got `#{Macro.to_string(splice)}`"
|
774
|
+ )
|
699
775
|
end
|
700
776
|
end
|
701
777
|
|
|
@@ -703,7 +779,7 @@ defmodule Ecto.Query.Builder do
|
703
779
|
escape(expr, :any, params_acc, vars, env)
|
704
780
|
end
|
705
781
|
|
706
|
- defp merge_fragments([h1|t1], [h2|t2]),
|
782
|
+ defp merge_fragments([h1 | t1], [h2 | t2]),
|
707
783
|
do: [{:raw, h1}, {:expr, h2} | merge_fragments(t1, t2)]
|
708
784
|
|
709
785
|
defp merge_fragments([h1], []),
|
|
@@ -737,38 +813,49 @@ defmodule Ecto.Query.Builder do
|
737
813
|
:ok
|
738
814
|
|
739
815
|
true ->
|
740
|
- error! "expression `#{Macro.to_string(expr)}` does not type check. " <>
|
741
|
- "It returns a value of type #{inspect actual} but a value of " <>
|
742
|
- "type #{inspect type} is expected"
|
816
|
+ error!(
|
817
|
+ "expression `#{Macro.to_string(expr)}` does not type check. " <>
|
818
|
+ "It returns a value of type #{inspect(actual)} but a value of " <>
|
819
|
+ "type #{inspect(type)} is expected"
|
820
|
+ )
|
743
821
|
end
|
744
822
|
end
|
745
823
|
|
746
824
|
@doc """
|
747
825
|
Validates the type with the given vars.
|
748
826
|
"""
|
827
|
+ def validate_type!({:parameterized, _} = type, _vars, _env),
|
828
|
+ do: type
|
829
|
+
|
749
830
|
def validate_type!({composite, type}, vars, env),
|
750
831
|
do: {composite, validate_type!(type, vars, env)}
|
832
|
+
|
751
833
|
def validate_type!({:^, _, [type]}, _vars, _env),
|
752
834
|
do: type
|
835
|
+
|
753
836
|
def validate_type!({:__aliases__, _, _} = type, _vars, env),
|
754
837
|
do: Macro.expand(type, get_env(env))
|
755
|
- def validate_type!({:parameterized, _, _} = type, _vars, _env),
|
756
|
- do: type
|
838
|
+
|
757
839
|
def validate_type!(type, _vars, _env) when is_atom(type),
|
758
840
|
do: type
|
841
|
+
|
759
842
|
def validate_type!({{:., _, [{var, _, context}, field]}, _, []}, vars, _env)
|
760
|
- when is_atom(var) and is_atom(context) and is_atom(field),
|
761
|
- do: {find_var!(var, vars), field}
|
843
|
+ when is_atom(var) and is_atom(context) and is_atom(field),
|
844
|
+ do: {find_var!(var, vars), field}
|
845
|
+
|
762
846
|
def validate_type!({:field, _, [{var, _, context}, field]}, vars, _env)
|
763
|
- when is_atom(var) and is_atom(context) and is_atom(field),
|
764
|
- do: {find_var!(var, vars), field}
|
847
|
+ when is_atom(var) and is_atom(context) and is_atom(field),
|
848
|
+ do: {find_var!(var, vars), field}
|
849
|
+
|
765
850
|
def validate_type!({:field, _, [{var, _, context}, {:^, _, [field]}]}, vars, _env)
|
766
|
- when is_atom(var) and is_atom(context),
|
767
|
- do: {find_var!(var, vars), field}
|
851
|
+ when is_atom(var) and is_atom(context),
|
852
|
+ do: {find_var!(var, vars), field}
|
768
853
|
|
769
854
|
def validate_type!(type, _vars, _env) do
|
770
|
- error! "type/2 expects an alias, atom, initialized parameterized type or " <>
|
771
|
- "source.field as second argument, got: `#{Macro.to_string(type)}`"
|
855
|
+ error!(
|
856
|
+ "type/2 expects an alias, atom, initialized parameterized type or " <>
|
857
|
+ "source.field as second argument, got: `#{Macro.to_string(type)}`"
|
858
|
+ )
|
772
859
|
end
|
773
860
|
|
774
861
|
@always_tagged [:binary]
|
|
@@ -777,11 +864,16 @@ defmodule Ecto.Query.Builder do
|
777
864
|
do: do_literal(value, expected, quoted_type(value, vars))
|
778
865
|
|
779
866
|
defp do_literal(value, _, current) when current in @always_tagged,
|
780
|
- do: {:%, [], [Ecto.Query.Tagged, {:%{}, [], [value: value, type: current]}]}
|
867
|
+ do:
|
868
|
+ {:%, [],
|
869
|
+ [Ecto.Query.Tagged, {:%{}, [], [value: value, type: normalize_type(value, current)]}]}
|
870
|
+
|
781
871
|
defp do_literal(value, :any, _current),
|
782
872
|
do: value
|
873
|
+
|
783
874
|
defp do_literal(value, expected, expected),
|
784
875
|
do: value
|
876
|
+
|
785
877
|
defp do_literal(value, expected, _current),
|
786
878
|
do: {:%, [], [Ecto.Query.Tagged, {:%{}, [], [value: value, type: expected]}]}
|
787
879
|
|
|
@@ -794,7 +886,7 @@ defmodule Ecto.Query.Builder do
|
794
886
|
@doc """
|
795
887
|
Escape the select alias map
|
796
888
|
"""
|
797
|
- @spec escape_select_aliases(map()) :: Macro.t
|
889
|
+ @spec escape_select_aliases(map()) :: Macro.t()
|
798
890
|
def escape_select_aliases(%{} = aliases), do: {:%{}, [], Map.to_list(aliases)}
|
799
891
|
|
800
892
|
@doc """
|
|
@@ -803,7 +895,7 @@ defmodule Ecto.Query.Builder do
|
803
895
|
A escaped variable is represented internally as
|
804
896
|
`&0`, `&1` and so on.
|
805
897
|
"""
|
806
|
- @spec escape_var!(atom, Keyword.t) :: Macro.t
|
898
|
+ @spec escape_var!(atom, Keyword.t()) :: Macro.t()
|
807
899
|
def escape_var!(var, vars) do
|
808
900
|
{:{}, [], [:&, [], [find_var!(var, vars)]]}
|
809
901
|
end
|
|
@@ -829,21 +921,24 @@ defmodule Ecto.Query.Builder do
|
829
921
|
** (Ecto.Query.CompileError) binding list should contain only variables or `{as, var}` tuples, got: :foo
|
830
922
|
|
831
923
|
"""
|
832
|
- @spec escape_binding(Macro.t, list, Macro.Env.t) :: {Macro.t, Keyword.t}
|
924
|
+ @spec escape_binding(Macro.t(), list, Macro.Env.t()) :: {Macro.t(), Keyword.t()}
|
833
925
|
def escape_binding(query, binding, _env) when is_list(binding) do
|
834
|
- vars = binding |> Enum.with_index |> Enum.map(&escape_bind/1)
|
926
|
+ vars = binding |> Enum.with_index() |> Enum.map(&escape_bind/1)
|
835
927
|
assert_no_duplicate_binding!(vars)
|
836
928
|
|
837
|
- {positional_vars, named_vars} = Enum.split_while(vars, ¬ named_bind?(&1))
|
929
|
+ {positional_vars, named_vars} = Enum.split_while(vars, &(not named_bind?(&1)))
|
838
930
|
assert_named_binds_in_tail!(named_vars, binding)
|
839
931
|
|
840
932
|
{query, positional_binds} = calculate_positional_binds(query, positional_vars)
|
841
933
|
{query, named_binds} = calculate_named_binds(query, named_vars)
|
842
934
|
{query, positional_binds ++ named_binds}
|
843
935
|
end
|
936
|
+
|
844
937
|
def escape_binding(_query, bind, _env) do
|
845
|
- error! "binding should be list of variables and `{as, var}` tuples " <>
|
846
|
- "at the end, got: #{Macro.to_string(bind)}"
|
938
|
+ error!(
|
939
|
+ "binding should be list of variables and `{as, var}` tuples " <>
|
940
|
+ "at the end, got: #{Macro.to_string(bind)}"
|
941
|
+ )
|
847
942
|
end
|
848
943
|
|
849
944
|
defp named_bind?({kind, _, _}), do: kind == :named
|
|
@@ -852,8 +947,10 @@ defmodule Ecto.Query.Builder do
|
852
947
|
if Enum.all?(named_vars, &named_bind?/1) do
|
853
948
|
:ok
|
854
949
|
else
|
855
|
- error! "named binds in the form of `{as, var}` tuples must be at the end " <>
|
856
|
- "of the binding list, got: #{Macro.to_string(binding)}"
|
950
|
+ error!(
|
951
|
+ "named binds in the form of `{as, var}` tuples must be at the end " <>
|
952
|
+ "of the binding list, got: #{Macro.to_string(binding)}"
|
953
|
+ )
|
857
954
|
end
|
858
955
|
end
|
859
956
|
|
|
@@ -861,16 +958,17 @@ defmodule Ecto.Query.Builder do
|
861
958
|
bound_vars = for {_, var, _} <- vars, var != :_, do: var
|
862
959
|
|
863
960
|
case bound_vars -- Enum.uniq(bound_vars) do
|
864
|
- [] -> :ok
|
865
|
- [var | _] -> error! "variable `#{var}` is bound twice"
|
961
|
+ [] -> :ok
|
962
|
+ [var | _] -> error!("variable `#{var}` is bound twice")
|
866
963
|
end
|
867
964
|
end
|
868
965
|
|
869
966
|
defp calculate_positional_binds(query, vars) do
|
870
|
- case Enum.split_while(vars, &elem(&1, 1) != :...) do
|
967
|
+ case Enum.split_while(vars, &(elem(&1, 1) != :...)) do
|
871
968
|
{vars, []} ->
|
872
969
|
vars = for {:pos, var, count} <- vars, do: {var, count}
|
873
970
|
{query, vars}
|
971
|
+
|
874
972
|
{vars, [_ | tail]} ->
|
875
973
|
query =
|
876
974
|
quote do
|
|
@@ -882,7 +980,9 @@ defmodule Ecto.Query.Builder do
|
882
980
|
tail =
|
883
981
|
tail
|
884
982
|
|> Enum.with_index(-length(tail))
|
885
|
- |> Enum.map(fn {{:pos, k, _}, count} -> {k, quote(do: escape_count + unquote(count))} end)
|
983
|
+ |> Enum.map(fn {{:pos, k, _}, count} ->
|
984
|
+ {k, quote(do: escape_count + unquote(count))}
|
985
|
+ end)
|
886
986
|
|
887
987
|
vars = for {:pos, var, count} <- vars, do: {var, count}
|
888
988
|
{query, vars ++ tail}
|
|
@@ -890,6 +990,7 @@ defmodule Ecto.Query.Builder do
|
890
990
|
end
|
891
991
|
|
892
992
|
defp calculate_named_binds(query, []), do: {query, []}
|
993
|
+
|
893
994
|
defp calculate_named_binds(query, vars) do
|
894
995
|
assignments =
|
895
996
|
for {:named, key, name} <- vars do
|
|
@@ -922,23 +1023,33 @@ defmodule Ecto.Query.Builder do
|
922
1023
|
ix
|
923
1024
|
|
924
1025
|
%{} ->
|
925
|
- raise Ecto.QueryError, message: "unknown bind name `#{inspect name}`", query: query
|
1026
|
+ raise Ecto.QueryError, message: "unknown bind name `#{inspect(name)}`", query: query
|
926
1027
|
end
|
927
1028
|
end
|
928
1029
|
|
929
1030
|
defp escape_bind({{:..., _, _context}, ix}),
|
930
1031
|
do: {:pos, :..., ix}
|
1032
|
+
|
931
1033
|
defp escape_bind({{var, _, context}, ix}) when is_atom(var) and is_atom(context),
|
932
1034
|
do: {:pos, var, ix}
|
1035
|
+
|
933
1036
|
defp escape_bind({{{var, _, context}, ix}, _}) when is_atom(var) and is_atom(context),
|
934
1037
|
do: {:pos, var, ix}
|
935
|
- defp escape_bind({{name, {var, _, context}}, _ix}) when is_atom(name) and is_atom(var) and is_atom(context),
|
936
|
- do: {:named, var, name}
|
937
|
- defp escape_bind({{{:^, _, [expr]}, {var, _, context}}, _ix}) when is_atom(var) and is_atom(context),
|
938
|
- do: {:named, var, expr}
|
1038
|
+
|
1039
|
+ defp escape_bind({{name, {var, _, context}}, _ix})
|
1040
|
+ when is_atom(name) and is_atom(var) and is_atom(context),
|
1041
|
+ do: {:named, var, name}
|
1042
|
+
|
1043
|
+ defp escape_bind({{{:^, _, [expr]}, {var, _, context}}, _ix})
|
1044
|
+ when is_atom(var) and is_atom(context),
|
1045
|
+ do: {:named, var, expr}
|
1046
|
+
|
939
1047
|
defp escape_bind({bind, _ix}),
|
940
|
- do: error!("binding list should contain only variables or " <>
|
941
|
- "`{as, var}` tuples, got: #{Macro.to_string(bind)}")
|
1048
|
+ do:
|
1049
|
+ error!(
|
1050
|
+ "binding list should contain only variables or " <>
|
1051
|
+ "`{as, var}` tuples, got: #{Macro.to_string(bind)}"
|
1052
|
+ )
|
942
1053
|
|
943
1054
|
defp try_expansion(expr, type, params, vars, %Macro.Env{} = env) do
|
944
1055
|
try_expansion(expr, type, params, vars, {env, &escape/5})
|
|
@@ -947,7 +1058,7 @@ defmodule Ecto.Query.Builder do
|
947
1058
|
defp try_expansion(expr, type, params, vars, {env, fun}) do
|
948
1059
|
case Macro.expand_once(expr, env) do
|
949
1060
|
^expr ->
|
950
|
- error! """
|
1061
|
+ error!("""
|
951
1062
|
`#{Macro.to_string(expr)}` is not a valid query expression.
|
952
1063
|
|
953
1064
|
* If you intended to call an Elixir function or introduce a value,
|
|
@@ -959,7 +1070,7 @@ defmodule Ecto.Query.Builder do
|
959
1070
|
* If you intended to extend Ecto's query DSL, make sure that you have required
|
960
1071
|
the module or imported the relevant function. Note that you need macros to
|
961
1072
|
extend Ecto's querying capabilities
|
962
|
- """
|
1073
|
+ """)
|
963
1074
|
|
964
1075
|
expanded ->
|
965
1076
|
fun.(expanded, type, params, vars, env)
|
|
@@ -970,12 +1081,15 @@ defmodule Ecto.Query.Builder do
|
970
1081
|
Finds the index value for the given var in vars or raises.
|
971
1082
|
"""
|
972
1083
|
def find_var!(var, vars) do
|
973
|
- vars[var] || error! "unbound variable `#{var}` in query. If you are attempting to interpolate a value, use ^var"
|
1084
|
+ vars[var] ||
|
1085
|
+ error!(
|
1086
|
+ "unbound variable `#{var}` in query. If you are attempting to interpolate a value, use ^var"
|
1087
|
+ )
|
974
1088
|
end
|
975
1089
|
|
976
1090
|
@doc """
|
977
1091
|
Checks if the field is an atom at compilation time or
|
978
|
- delegate the check to runtime for interpolation.
|
1092
|
+ delegates the check to runtime for interpolation.
|
979
1093
|
"""
|
980
1094
|
def quoted_atom!({:^, _, [expr]}, used_ref),
|
981
1095
|
do: quote(do: Ecto.Query.Builder.atom!(unquote(expr), unquote(used_ref)))
|
|
@@ -987,7 +1101,7 @@ defmodule Ecto.Query.Builder do
|
987
1101
|
do:
|
988
1102
|
error!(
|
989
1103
|
"expected literal atom or interpolated value in #{used_ref}, got: " <>
|
990
|
- "`#{Macro.to_string(other)}`"
|
1104
|
+ "`#{Macro.to_string(other)}`"
|
991
1105
|
)
|
992
1106
|
|
993
1107
|
@doc """
|
|
@@ -997,7 +1111,7 @@ defmodule Ecto.Query.Builder do
|
997
1111
|
do: atom
|
998
1112
|
|
999
1113
|
def atom!(other, used_ref),
|
1000
|
- do: error!("expected atom in #{used_ref}, got: `#{inspect other}`")
|
1114
|
+ do: error!("expected atom in #{used_ref}, got: `#{inspect(other)}`")
|
1001
1115
|
|
1002
1116
|
@doc """
|
1003
1117
|
Checks if the value of a late binding is an interpolation or
|
|
@@ -1017,7 +1131,7 @@ defmodule Ecto.Query.Builder do
|
1017
1131
|
Enum.map(path, "ed_json_path_element!/1)
|
1018
1132
|
end
|
1019
1133
|
|
1020
|
- defp escape_json_path({:^, _, [path]}) do
|
1134
|
+ defp escape_json_path({:^, _, [path]}) do
|
1021
1135
|
quote do
|
1022
1136
|
path = Ecto.Query.Builder.json_path!(unquote(path))
|
1023
1137
|
Enum.map(path, &Ecto.Query.Builder.json_path_element!/1)
|
|
@@ -1025,7 +1139,9 @@ defmodule Ecto.Query.Builder do
|
1025
1139
|
end
|
1026
1140
|
|
1027
1141
|
defp escape_json_path(other) do
|
1028
|
- error!("expected JSON path to be a literal list or interpolated value, got: `#{Macro.to_string(other)}`")
|
1142
|
+ error!(
|
1143
|
+ "expected JSON path to be a literal list or interpolated value, got: `#{Macro.to_string(other)}`"
|
1144
|
+ )
|
1029
1145
|
end
|
1030
1146
|
|
1031
1147
|
defp quoted_json_path_element!({:^, _, [expr]}),
|
|
@@ -1041,7 +1157,7 @@ defmodule Ecto.Query.Builder do
|
1041
1157
|
do:
|
1042
1158
|
error!(
|
1043
1159
|
"expected JSON path to contain literal strings, literal integers, or interpolated values, got: " <>
|
1044
|
- "`#{Macro.to_string(other)}`"
|
1160
|
+ "`#{Macro.to_string(other)}`"
|
1045
1161
|
)
|
1046
1162
|
|
1047
1163
|
@doc """
|
|
@@ -1049,26 +1165,31 @@ defmodule Ecto.Query.Builder do
|
1049
1165
|
"""
|
1050
1166
|
def json_path_element!(binary) when is_binary(binary),
|
1051
1167
|
do: binary
|
1168
|
+
|
1052
1169
|
def json_path_element!(integer) when is_integer(integer),
|
1053
1170
|
do: integer
|
1171
|
+
|
1054
1172
|
def json_path_element!(other),
|
1055
|
- do: error!("expected string or integer in json_extract_path/2, got: `#{inspect other}`")
|
1173
|
+ do: error!("expected string or integer in json_extract_path/2, got: `#{inspect(other)}`")
|
1056
1174
|
|
1057
1175
|
@doc """
|
1058
1176
|
Called by escaper at runtime to verify that path is a list
|
1059
1177
|
"""
|
1060
1178
|
def json_path!(path) when is_list(path),
|
1061
1179
|
do: path
|
1180
|
+
|
1062
1181
|
def json_path!(path),
|
1063
|
- do: error!("expected `path` to be a list in json_extract_path/2, got: `#{inspect path}`")
|
1182
|
+ do: error!("expected `path` to be a list in json_extract_path/2, got: `#{inspect(path)}`")
|
1064
1183
|
|
1065
1184
|
@doc """
|
1066
1185
|
Called by escaper at runtime to verify that a value is not nil.
|
1067
1186
|
"""
|
1068
1187
|
def not_nil!(nil) do
|
1069
|
- raise ArgumentError, "comparison with nil is forbidden as it is unsafe. " <>
|
1070
|
- "If you want to check if a value is nil, use is_nil/1 instead"
|
1188
|
+ raise ArgumentError,
|
1189
|
+ "comparison with nil is forbidden as it is unsafe. " <>
|
1190
|
+ "If you want to check if a value is nil, use is_nil/1 instead"
|
1071
1191
|
end
|
1192
|
+
|
1072
1193
|
def not_nil!(not_nil) do
|
1073
1194
|
not_nil
|
1074
1195
|
end
|
|
@@ -1079,6 +1200,7 @@ defmodule Ecto.Query.Builder do
|
1079
1200
|
"""
|
1080
1201
|
def quoted_interval!({:^, _, [expr]}),
|
1081
1202
|
do: quote(do: Ecto.Query.Builder.interval!(unquote(expr)))
|
1203
|
+
|
1082
1204
|
def quoted_interval!(other),
|
1083
1205
|
do: interval!(other)
|
1084
1206
|
|
|
@@ -1123,10 +1245,15 @@ defmodule Ecto.Query.Builder do
|
1123
1245
|
@interval ~w(year month week day hour minute second millisecond microsecond)
|
1124
1246
|
def interval!(interval) when interval in @interval,
|
1125
1247
|
do: interval
|
1248
|
+
|
1126
1249
|
def interval!(other_string) when is_binary(other_string),
|
1127
|
- do: error!("invalid interval: `#{inspect other_string}` (expected one of #{Enum.join(@interval, ", ")})")
|
1250
|
+ do:
|
1251
|
+ error!(
|
1252
|
+ "invalid interval: `#{inspect(other_string)}` (expected one of #{Enum.join(@interval, ", ")})"
|
1253
|
+ )
|
1254
|
+
|
1128
1255
|
def interval!(not_string),
|
1129
|
- do: error!("invalid interval: `#{inspect not_string}` (expected a string)")
|
1256
|
+ do: error!("invalid interval: `#{inspect(not_string)}` (expected a string)")
|
1130
1257
|
|
1131
1258
|
@doc """
|
1132
1259
|
Negates the given number.
|
|
@@ -1137,12 +1264,12 @@ defmodule Ecto.Query.Builder do
|
1137
1264
|
@doc """
|
1138
1265
|
Returns the type of an expression at build time.
|
1139
1266
|
"""
|
1140
|
- @spec quoted_type(Macro.t, Keyword.t) :: quoted_type
|
1267
|
+ @spec quoted_type(Macro.t(), Keyword.t()) :: quoted_type
|
1141
1268
|
|
1142
1269
|
# Fields
|
1143
1270
|
def quoted_type({{:., _, [{var, _, context}, field]}, _, []}, vars)
|
1144
|
- when is_atom(var) and is_atom(context) and is_atom(field),
|
1145
|
- do: {find_var!(var, vars), field}
|
1271
|
+ when is_atom(var) and is_atom(context) and is_atom(field),
|
1272
|
+ do: {find_var!(var, vars), field}
|
1146
1273
|
|
1147
1274
|
def quoted_type({{:., _, [{kind, _, [value]}, field]}, _, []}, _vars)
|
1148
1275
|
when kind in [:as, :parent_as] do
|
|
@@ -1151,8 +1278,8 @@ defmodule Ecto.Query.Builder do
|
1151
1278
|
end
|
1152
1279
|
|
1153
1280
|
def quoted_type({:field, _, [{var, _, context}, field]}, vars)
|
1154
|
- when is_atom(var) and is_atom(context) and is_atom(field),
|
1155
|
- do: {find_var!(var, vars), field}
|
1281
|
+ when is_atom(var) and is_atom(context) and is_atom(field),
|
1282
|
+ do: {find_var!(var, vars), field}
|
1156
1283
|
|
1157
1284
|
def quoted_type({:field, _, [{kind, _, [value]}, field]}, _vars)
|
1158
1285
|
when kind in [:as, :parent_as] and is_atom(field) do
|
|
@@ -1165,8 +1292,8 @@ defmodule Ecto.Query.Builder do
|
1165
1292
|
# in the query itself. We are assuming this is not an issue
|
1166
1293
|
# as the solution is somewhat complicated.
|
1167
1294
|
def quoted_type({:field, _, [{var, _, context}, {:^, _, [code]}]}, vars)
|
1168
|
- when is_atom(var) and is_atom(context),
|
1169
|
- do: {find_var!(var, vars), code}
|
1295
|
+ when is_atom(var) and is_atom(context),
|
1296
|
+ do: {find_var!(var, vars), code}
|
1170
1297
|
|
1171
1298
|
def quoted_type({:field, _, [{kind, _, [value]}, {:^, _, [code]}]}, _vars)
|
1172
1299
|
when kind in [:as, :parent_as] do
|
|
@@ -1184,7 +1311,9 @@ defmodule Ecto.Query.Builder do
|
1184
1311
|
|
1185
1312
|
# Sigils
|
1186
1313
|
def quoted_type({sigil, _, [_, []]}, _vars) when sigil in ~w(sigil_s sigil_S)a, do: :string
|
1187
|
- def quoted_type({sigil, _, [_, []]}, _vars) when sigil in ~w(sigil_w sigil_W)a, do: {:array, :string}
|
1314
|
+
|
1315
|
+ def quoted_type({sigil, _, [_, []]}, _vars) when sigil in ~w(sigil_w sigil_W)a,
|
1316
|
+ do: {:array, :string}
|
1188
1317
|
|
1189
1318
|
# Lists
|
1190
1319
|
def quoted_type(list, vars) when is_list(list) do
|
|
@@ -1208,8 +1337,8 @@ defmodule Ecto.Query.Builder do
|
1208
1337
|
end
|
1209
1338
|
|
1210
1339
|
# Literals
|
1211
|
- def quoted_type(literal, _vars) when is_float(literal), do: :float
|
1212
|
- def quoted_type(literal, _vars) when is_binary(literal), do: :string
|
1340
|
+ def quoted_type(literal, _vars) when is_float(literal), do: :float
|
1341
|
+ def quoted_type(literal, _vars) when is_binary(literal), do: :string
|
1213
1342
|
def quoted_type(literal, _vars) when is_boolean(literal), do: :boolean
|
1214
1343
|
def quoted_type(literal, _vars) when is_atom(literal) and not is_nil(literal), do: :atom
|
1215
1344
|
def quoted_type(literal, _vars) when is_integer(literal), do: :integer
|
|
@@ -1221,7 +1350,7 @@ defmodule Ecto.Query.Builder do
|
1221
1350
|
def quoted_type({name, _, args}, _vars) when is_atom(name) and is_list(args) do
|
1222
1351
|
case call_type(name, length(args)) do
|
1223
1352
|
{_in, out} -> out
|
1224
|
- nil -> :any
|
1353
|
+ nil -> :any
|
1225
1354
|
end
|
1226
1355
|
end
|
1227
1356
|
|
|
@@ -1230,18 +1359,23 @@ defmodule Ecto.Query.Builder do
|
1230
1359
|
defp get_env({env, _}), do: env
|
1231
1360
|
defp get_env(env), do: env
|
1232
1361
|
|
1362
|
+ defp normalize_type(value, :binary),
|
1363
|
+ do: quote(do: (is_binary(unquote(value)) && :binary) || :bitstring)
|
1364
|
+
|
1233
1365
|
@doc """
|
1234
1366
|
Raises a query building error.
|
1235
1367
|
"""
|
1236
1368
|
def error!(message) when is_binary(message) do
|
1237
|
- {:current_stacktrace, [_|t]} = Process.info(self(), :current_stacktrace)
|
1369
|
+ {:current_stacktrace, [_ | t]} = Process.info(self(), :current_stacktrace)
|
1238
1370
|
|
1239
|
- t = Enum.drop_while t, fn
|
1240
|
- {mod, _, _, _} ->
|
1241
|
- String.starts_with?(Atom.to_string(mod), ["Elixir.Ecto.Query.", "Elixir.Enum"])
|
1242
|
- _ ->
|
1243
|
- false
|
1244
|
- end
|
1371
|
+ t =
|
1372
|
+ Enum.drop_while(t, fn
|
1373
|
+ {mod, _, _, _} ->
|
1374
|
+ String.starts_with?(Atom.to_string(mod), ["Elixir.Ecto.Query.", "Elixir.Enum"])
|
1375
|
+
|
1376
|
+ _ ->
|
1377
|
+ false
|
1378
|
+ end)
|
1245
1379
|
|
1246
1380
|
reraise Ecto.Query.CompileError, [message: message], t
|
1247
1381
|
end
|
|
@@ -1255,7 +1389,7 @@ defmodule Ecto.Query.Builder do
|
1255
1389
|
4
|
1256
1390
|
|
1257
1391
|
"""
|
1258
|
- @spec count_binds(Ecto.Query.t) :: non_neg_integer
|
1392
|
+ @spec count_binds(Ecto.Query.t()) :: non_neg_integer
|
1259
1393
|
def count_binds(%Query{joins: joins}) do
|
1260
1394
|
1 + length(joins)
|
1261
1395
|
end
|
|
@@ -1294,7 +1428,9 @@ defmodule Ecto.Query.Builder do
|
1294
1428
|
def add_select_alias(aliases, name) when is_map(aliases) and is_atom(name) do
|
1295
1429
|
case aliases do
|
1296
1430
|
%{^name => _} ->
|
1297
|
- error! "the alias `#{inspect(name)}` has been specified more than once using `selected_as/2`"
|
1431
|
+ error!(
|
1432
|
+ "the alias `#{inspect(name)}` has been specified more than once using `selected_as/2`"
|
1433
|
+ )
|
1298
1434
|
|
1299
1435
|
aliases ->
|
1300
1436
|
Map.put(aliases, name, @select_alias_dummy_value)
|
|
@@ -1352,7 +1488,7 @@ defmodule Ecto.Query.Builder do
|
1352
1488
|
the query properly, but they will be in their runtime form
|
1353
1489
|
when invoked at runtime.
|
1354
1490
|
"""
|
1355
|
- @spec apply_query(Macro.t, Macro.t, Macro.t, Macro.Env.t) :: Macro.t
|
1491
|
+ @spec apply_query(Macro.t(), Macro.t(), Macro.t(), Macro.Env.t()) :: Macro.t()
|
1356
1492
|
def apply_query(query, module, args, env) do
|
1357
1493
|
case Macro.expand(query, env) |> unescape_query() do
|
1358
1494
|
%Query{} = compiletime_query ->
|
|
@@ -1369,10 +1505,11 @@ defmodule Ecto.Query.Builder do
|
1369
1505
|
end
|
1370
1506
|
|
1371
1507
|
# Unescapes an `Ecto.Query` struct.
|
1372
|
- @spec unescape_query(Macro.t) :: Query.t | Macro.t
|
1508
|
+ @spec unescape_query(Macro.t()) :: Query.t() | Macro.t()
|
1373
1509
|
defp unescape_query({:%, _, [Query, {:%{}, _, list}]}) do
|
1374
1510
|
struct(Query, list)
|
1375
1511
|
end
|
1512
|
+
|
1376
1513
|
defp unescape_query({:%{}, _, list} = ast) do
|
1377
1514
|
if List.keyfind(list, :__struct__, 0) == {:__struct__, Query} do
|
1378
1515
|
Map.new(list)
|
|
@@ -1380,12 +1517,13 @@ defmodule Ecto.Query.Builder do
|
1380
1517
|
ast
|
1381
1518
|
end
|
1382
1519
|
end
|
1520
|
+
|
1383
1521
|
defp unescape_query(other) do
|
1384
1522
|
other
|
1385
1523
|
end
|
1386
1524
|
|
1387
1525
|
# Escapes an `Ecto.Query` and associated structs.
|
1388
|
- @spec escape_query(Query.t) :: Macro.t
|
1526
|
+ @spec escape_query(Query.t()) :: Macro.t()
|
1389
1527
|
defp escape_query(%Query{} = query), do: {:%{}, [], Map.to_list(query)}
|
1390
1528
|
|
1391
1529
|
defp parse_access_get({{:., _, [Access, :get]}, _, [left, right]}, acc) do
|
changed
lib/ecto/query/builder/distinct.ex
|
@@ -30,11 +30,20 @@ defmodule Ecto.Query.Builder.Distinct do
|
30
30
|
Called at runtime to verify distinct.
|
31
31
|
"""
|
32
32
|
def distinct!(query, distinct, file, line) when is_boolean(distinct) do
|
33
|
- apply(query, %Ecto.Query.QueryExpr{expr: distinct, params: [], line: line, file: file})
|
33
|
+ apply(query, %Ecto.Query.ByExpr{expr: distinct, params: [], line: line, file: file})
|
34
34
|
end
|
35
35
|
def distinct!(query, distinct, file, line) do
|
36
|
- {expr, params} = Builder.OrderBy.order_by_or_distinct!(:distinct, query, distinct, [])
|
37
|
- expr = %Ecto.Query.QueryExpr{expr: expr, params: Enum.reverse(params), line: line, file: file}
|
36
|
+ {expr, params, subqueries} =
|
37
|
+ Builder.OrderBy.order_by_or_distinct!(:distinct, query, distinct, [])
|
38
|
+
|
39
|
+ expr = %Ecto.Query.ByExpr{
|
40
|
+ expr: expr,
|
41
|
+ params: Enum.reverse(params),
|
42
|
+ line: line,
|
43
|
+ file: file,
|
44
|
+ subqueries: subqueries
|
45
|
+ }
|
46
|
+
|
38
47
|
apply(query, expr)
|
39
48
|
end
|
40
49
|
|
|
@@ -54,12 +63,13 @@ defmodule Ecto.Query.Builder.Distinct do
|
54
63
|
|
55
64
|
def build(query, binding, expr, env) do
|
56
65
|
{query, binding} = Builder.escape_binding(query, binding, env)
|
57
|
- {expr, {params, _acc}} = escape(expr, {[], %{}}, binding, env)
|
66
|
+ {expr, {params, acc}} = escape(expr, {[], %{subqueries: []}}, binding, env)
|
58
67
|
params = Builder.escape_params(params)
|
59
68
|
|
60
|
- distinct = quote do: %Ecto.Query.QueryExpr{
|
69
|
+ distinct = quote do: %Ecto.Query.ByExpr{
|
61
70
|
expr: unquote(expr),
|
62
71
|
params: unquote(params),
|
72
|
+ subqueries: unquote(acc.subqueries),
|
63
73
|
file: unquote(env.file),
|
64
74
|
line: unquote(env.line)}
|
65
75
|
Builder.apply_query(query, __MODULE__, [distinct], env)
|
changed
lib/ecto/query/builder/dynamic.ex
|
@@ -31,6 +31,10 @@ defmodule Ecto.Query.Builder.Dynamic do
|
31
31
|
Select.escape(expr, vars, env)
|
32
32
|
end
|
33
33
|
|
34
|
+ defp escape({:%{}, _, _} = expr, _params_acc, vars, env) do
|
35
|
+ Select.escape(expr, vars, env)
|
36
|
+ end
|
37
|
+
|
34
38
|
defp escape(expr, params_acc, vars, env) do
|
35
39
|
Builder.escape(expr, :any, params_acc, vars, {env, &escape_expansion/5})
|
36
40
|
end
|
changed
lib/ecto/query/builder/group_by.ex
|
@@ -49,21 +49,21 @@ defmodule Ecto.Query.Builder.GroupBy do
|
49
49
|
Shared between group_by and partition_by.
|
50
50
|
"""
|
51
51
|
def group_or_partition_by!(kind, query, exprs, params) do
|
52
|
- {expr, {params, _}} =
|
53
|
- Enum.map_reduce(List.wrap(exprs), {params, length(params)}, fn
|
52
|
+ {expr, {params, _, subqueries}} =
|
53
|
+ Enum.map_reduce(List.wrap(exprs), {params, length(params), []}, fn
|
54
54
|
field, params_count when is_atom(field) ->
|
55
55
|
{to_field(field), params_count}
|
56
56
|
|
57
|
- %Ecto.Query.DynamicExpr{} = dynamic, {params, count} ->
|
58
|
- {expr, params, count} = Builder.Dynamic.partially_expand(kind, query, dynamic, params, count)
|
59
|
- {expr, {params, count}}
|
57
|
+ %Ecto.Query.DynamicExpr{} = dynamic, {params, count, subqueries} ->
|
58
|
+ {expr, params, subqueries, _aliases, count} = Builder.Dynamic.partially_expand(query, dynamic, params, subqueries, %{}, count)
|
59
|
+ {expr, {params, count, subqueries}}
|
60
60
|
|
61
61
|
other, _params_count ->
|
62
62
|
raise ArgumentError,
|
63
63
|
"expected a list of fields and dynamics in `#{kind}`, got: `#{inspect other}`"
|
64
64
|
end)
|
65
65
|
|
66
|
- {expr, params}
|
66
|
+ {expr, params, subqueries}
|
67
67
|
end
|
68
68
|
|
69
69
|
defp to_field(field), do: {{:., [], [{:&, [], [0]}, field]}, [], []}
|
|
@@ -72,8 +72,8 @@ defmodule Ecto.Query.Builder.GroupBy do
|
72
72
|
Called at runtime to assemble group_by.
|
73
73
|
"""
|
74
74
|
def group_by!(query, group_by, file, line) do
|
75
|
- {expr, params} = group_or_partition_by!(:group_by, query, group_by, [])
|
76
|
- expr = %Ecto.Query.QueryExpr{expr: expr, params: Enum.reverse(params), line: line, file: file}
|
75
|
+ {expr, params, subqueries} = group_or_partition_by!(:group_by, query, group_by, [])
|
76
|
+ expr = %Ecto.Query.ByExpr{expr: expr, params: Enum.reverse(params), line: line, file: file, subqueries: subqueries}
|
77
77
|
apply(query, expr)
|
78
78
|
end
|
79
79
|
|
|
@@ -93,12 +93,13 @@ defmodule Ecto.Query.Builder.GroupBy do
|
93
93
|
|
94
94
|
def build(query, binding, expr, env) do
|
95
95
|
{query, binding} = Builder.escape_binding(query, binding, env)
|
96
|
- {expr, {params, _acc}} = escape(:group_by, expr, {[], %{}}, binding, env)
|
96
|
+ {expr, {params, acc}} = escape(:group_by, expr, {[], %{subqueries: []}}, binding, env)
|
97
97
|
params = Builder.escape_params(params)
|
98
98
|
|
99
|
- group_by = quote do: %Ecto.Query.QueryExpr{
|
99
|
+ group_by = quote do: %Ecto.Query.ByExpr{
|
100
100
|
expr: unquote(expr),
|
101
101
|
params: unquote(params),
|
102
|
+ subqueries: unquote(acc.subqueries),
|
102
103
|
file: unquote(env.file),
|
103
104
|
line: unquote(env.line)}
|
104
105
|
Builder.apply_query(query, __MODULE__, [group_by], env)
|
changed
lib/ecto/query/builder/join.ex
|
@@ -254,7 +254,7 @@ defmodule Ecto.Query.Builder.Join do
|
254
254
|
|
255
255
|
defp ensure_on(on, _assoc, _qual, _source, _env) when on != nil, do: on
|
256
256
|
|
257
|
- defp ensure_on(nil, _assoc = nil, qual, source, env) when qual not in [:cross, :cross_lateral, :array, :left_array] do
|
257
|
+ defp ensure_on(nil, _assoc = nil, qual, source, env) when qual not in [:cross, :cross_lateral] do
|
258
258
|
maybe_source =
|
259
259
|
with {source, alias} <- source,
|
260
260
|
source when source != nil <- source || alias do
|
|
@@ -341,7 +341,7 @@ defmodule Ecto.Query.Builder.Join do
|
341
341
|
end
|
342
342
|
end
|
343
343
|
|
344
|
- @qualifiers [:inner, :inner_lateral, :left, :left_lateral, :right, :full, :cross, :cross_lateral, :array, :left_array]
|
344
|
+ @qualifiers [:inner, :inner_lateral, :left, :left_lateral, :right, :full, :cross, :cross_lateral]
|
345
345
|
|
346
346
|
@doc """
|
347
347
|
Called at runtime to check dynamic qualifier.
|
changed
lib/ecto/query/builder/order_by.ex
|
@@ -51,6 +51,7 @@ defmodule Ecto.Query.Builder.OrderBy do
|
51
51
|
{Macro.t(), {list, term}}
|
52
52
|
def escape(kind, expr, params_acc, vars, env) do
|
53
53
|
expr
|
54
|
+ |> Macro.expand_once(get_env(env))
|
54
55
|
|> List.wrap()
|
55
56
|
|> Enum.map_reduce(params_acc, &do_escape(&1, &2, kind, vars, env))
|
56
57
|
end
|
|
@@ -83,6 +84,9 @@ defmodule Ecto.Query.Builder.OrderBy do
|
83
84
|
{{:asc, ast}, params_acc}
|
84
85
|
end
|
85
86
|
|
87
|
+ defp get_env({env, _}), do: env
|
88
|
+ defp get_env(env), do: env
|
89
|
+
|
86
90
|
@doc """
|
87
91
|
Checks the variable is a quoted direction at compilation time or
|
88
92
|
delegate the check to runtime for interpolation.
|
|
@@ -101,7 +105,7 @@ defmodule Ecto.Query.Builder.OrderBy do
|
101
105
|
end
|
102
106
|
|
103
107
|
@doc """
|
104
|
- Called by at runtime to verify the direction.
|
108
|
+ Called at runtime to verify the direction.
|
105
109
|
"""
|
106
110
|
def dir!(_kind, dir) when dir in @directions,
|
107
111
|
do: dir
|
|
@@ -136,8 +140,8 @@ defmodule Ecto.Query.Builder.OrderBy do
|
136
140
|
Shared between order_by and distinct.
|
137
141
|
"""
|
138
142
|
def order_by_or_distinct!(kind, query, exprs, params) do
|
139
|
- {expr, {params, _}} =
|
140
|
- Enum.map_reduce(List.wrap(exprs), {params, length(params)}, fn
|
143
|
+ {expr, {params, _, subqueries}} =
|
144
|
+ Enum.map_reduce(List.wrap(exprs), {params, length(params), []}, fn
|
141
145
|
{dir, expr}, params_count when dir in @directions ->
|
142
146
|
{expr, params} = dynamic_or_field!(kind, expr, query, params_count)
|
143
147
|
{{dir, expr}, params}
|
|
@@ -147,21 +151,35 @@ defmodule Ecto.Query.Builder.OrderBy do
|
147
151
|
{{:asc, expr}, params}
|
148
152
|
end)
|
149
153
|
|
150
|
- {expr, params}
|
154
|
+ {expr, params, subqueries}
|
151
155
|
end
|
152
156
|
|
153
157
|
@doc """
|
154
158
|
Called at runtime to assemble order_by.
|
155
159
|
"""
|
156
160
|
def order_by!(query, exprs, op, file, line) do
|
157
|
- {expr, params} = order_by_or_distinct!(:order_by, query, exprs, [])
|
158
|
- expr = %Ecto.Query.QueryExpr{expr: expr, params: Enum.reverse(params), line: line, file: file}
|
161
|
+ {expr, params, subqueries} = order_by_or_distinct!(:order_by, query, exprs, [])
|
162
|
+ expr = %Ecto.Query.ByExpr{expr: expr, params: Enum.reverse(params), line: line, file: file, subqueries: subqueries}
|
159
163
|
apply(query, expr, op)
|
160
164
|
end
|
161
165
|
|
162
|
- defp dynamic_or_field!(kind, %Ecto.Query.DynamicExpr{} = dynamic, query, {params, count}) do
|
163
|
- {expr, params, count} = Builder.Dynamic.partially_expand(kind, query, dynamic, params, count)
|
164
|
- {expr, {params, count}}
|
166
|
+ defp dynamic_or_field!(
|
167
|
+ _kind,
|
168
|
+ %Ecto.Query.DynamicExpr{} = dynamic,
|
169
|
+ query,
|
170
|
+ {params, count, subqueries}
|
171
|
+ ) do
|
172
|
+ {expr, params, subqueries, _aliases, count} =
|
173
|
+ Ecto.Query.Builder.Dynamic.partially_expand(
|
174
|
+ query,
|
175
|
+ dynamic,
|
176
|
+ params,
|
177
|
+ subqueries,
|
178
|
+ %{},
|
179
|
+ count
|
180
|
+ )
|
181
|
+
|
182
|
+ {expr, {params, count, subqueries}}
|
165
183
|
end
|
166
184
|
|
167
185
|
defp dynamic_or_field!(_kind, field, _query, params_count) when is_atom(field) do
|
|
@@ -196,13 +214,14 @@ defmodule Ecto.Query.Builder.OrderBy do
|
196
214
|
|
197
215
|
def build(query, binding, expr, op, env) do
|
198
216
|
{query, binding} = Builder.escape_binding(query, binding, env)
|
199
|
- {expr, {params, _acc}} = escape(:order_by, expr, {[], %{}}, binding, env)
|
217
|
+ {expr, {params, acc}} = escape(:order_by, expr, {[], %{subqueries: []}}, binding, env)
|
200
218
|
params = Builder.escape_params(params)
|
201
219
|
|
202
220
|
order_by =
|
203
|
- quote do: %Ecto.Query.QueryExpr{
|
221
|
+ quote do: %Ecto.Query.ByExpr{
|
204
222
|
expr: unquote(expr),
|
205
223
|
params: unquote(params),
|
224
|
+ subqueries: unquote(acc.subqueries),
|
206
225
|
file: unquote(env.file),
|
207
226
|
line: unquote(env.line)
|
208
227
|
}
|
changed
lib/ecto/query/builder/preload.ex
|
@@ -302,7 +302,7 @@ defmodule Ecto.Query.Builder.Preload do
|
302
302
|
raise ArgumentError,
|
303
303
|
"invalid preload for key `#{inspect(key)}`: #{inspect(other)}. " <>
|
304
304
|
"Preloads can be a query, a function (arity 1), or a dynamic that " <>
|
305
|
- "evalutes to a single binding."
|
305
|
+ "evaluates to a single binding."
|
306
306
|
end
|
307
307
|
|
308
308
|
defp assert_assoc!(mode, _atom) when mode in [:both, :assoc], do: :ok
|
changed
lib/ecto/query/builder/select.ex
|
@@ -74,9 +74,9 @@ defmodule Ecto.Query.Builder.Select do
|
74
74
|
|
75
75
|
# Map
|
76
76
|
defp escape({:%{}, _, [{:|, _, [data, pairs]}]}, params_acc, vars, env) do
|
77
|
- {data, params_acc} = escape(data, params_acc, vars, env)
|
78
|
- {pairs, params_acc} = escape_pairs(pairs, params_acc, vars, env)
|
79
|
- {{:{}, [], [:%{}, [], [{:{}, [], [:|, [], [data, pairs]]}]]}, params_acc}
|
77
|
+ {escaped_data, params_acc} = escape(data, params_acc, vars, env)
|
78
|
+ {pairs, params_acc} = escape_pairs(pairs, data, params_acc, vars, env)
|
79
|
+ {{:{}, [], [:%{}, [], [{:{}, [], [:|, [], [escaped_data, pairs]]}]]}, params_acc}
|
80
80
|
end
|
81
81
|
|
82
82
|
# Merge
|
|
@@ -93,7 +93,7 @@ defmodule Ecto.Query.Builder.Select do
|
93
93
|
|
94
94
|
# Map
|
95
95
|
defp escape({:%{}, _, pairs}, params_acc, vars, env) do
|
96
|
- {pairs, params_acc} = escape_pairs(pairs, params_acc, vars, env)
|
96
|
+ {pairs, params_acc} = escape_pairs(pairs, nil, params_acc, vars, env)
|
97
97
|
{{:{}, [], [:%{}, [], pairs]}, params_acc}
|
98
98
|
end
|
99
99
|
|
|
@@ -128,14 +128,21 @@ defmodule Ecto.Query.Builder.Select do
|
128
128
|
escape(expr, params_acc, vars, env)
|
129
129
|
end
|
130
130
|
|
131
|
- defp escape_pairs(pairs, params_acc, vars, env) do
|
131
|
+ defp escape_pairs(pairs, update_data, params_acc, vars, env) do
|
132
132
|
Enum.map_reduce(pairs, params_acc, fn {k, v}, acc ->
|
133
|
+ v = tag_update_param(update_data, k, v)
|
133
134
|
{k, acc} = escape_key(k, acc, vars, env)
|
134
135
|
{v, acc} = escape(v, acc, vars, env)
|
135
136
|
{{k, v}, acc}
|
136
137
|
end)
|
137
138
|
end
|
138
139
|
|
140
|
+ defp tag_update_param({var, _, context}, field, {:^, _,[_]} = param) when is_atom(var) and is_atom(context) do
|
141
|
+ {:type, [], [param, {{:., [], [{var, [], context}, field]}, [], []}]}
|
142
|
+ end
|
143
|
+
|
144
|
+ defp tag_update_param(_, _, value), do: value
|
145
|
+
|
139
146
|
defp escape_key(k, params_acc, _vars, _env) when is_atom(k) do
|
140
147
|
{k, params_acc}
|
141
148
|
end
|
|
@@ -392,7 +399,7 @@ defmodule Ecto.Query.Builder.Select do
|
392
399
|
{:merge, [], [old_expr, new_expr]}
|
393
400
|
end
|
394
401
|
|
395
|
- {{:map, meta, old_fields}, {:map, _, new_fields}} when old_params == [] ->
|
402
|
+ {{:map, meta, old_fields}, {:map, _, new_fields}} ->
|
396
403
|
cond do
|
397
404
|
old_fields == [] ->
|
398
405
|
new_expr
|
|
@@ -400,11 +407,16 @@ defmodule Ecto.Query.Builder.Select do
|
400
407
|
new_fields == [] ->
|
401
408
|
old_expr
|
402
409
|
|
403
|
- Keyword.keyword?(old_fields) and Keyword.keyword?(new_fields) ->
|
404
|
- {:%{}, meta, Keyword.merge(old_fields, new_fields)}
|
405
|
-
|
406
410
|
true ->
|
407
|
- {:merge, [], [old_expr, new_expr]}
|
411
|
+ require_distinct_keys? = old_params != []
|
412
|
+
|
413
|
+ case merge_map_fields(old_fields, new_fields, require_distinct_keys?) do
|
414
|
+ fields when is_list(fields) ->
|
415
|
+ {:%{}, meta, fields}
|
416
|
+
|
417
|
+ :error ->
|
418
|
+ {:merge, [], [old_expr, new_expr]}
|
419
|
+ end
|
408
420
|
end
|
409
421
|
|
410
422
|
{_, {:map, _, _}} ->
|
|
@@ -463,6 +475,35 @@ defmodule Ecto.Query.Builder.Select do
|
463
475
|
:error
|
464
476
|
end
|
465
477
|
|
478
|
+ defp merge_map_fields(old_fields, new_fields, false) do
|
479
|
+ if Keyword.keyword?(old_fields) and Keyword.keyword?(new_fields) do
|
480
|
+ Keyword.merge(old_fields, new_fields)
|
481
|
+ else
|
482
|
+ :error
|
483
|
+ end
|
484
|
+ end
|
485
|
+
|
486
|
+ defp merge_map_fields(old_fields, new_fields, true) when is_list(old_fields) do
|
487
|
+ if Keyword.keyword?(new_fields) do
|
488
|
+ valid? =
|
489
|
+ Enum.reduce_while(old_fields, true, fn
|
490
|
+ {k, _v}, _ when is_atom(k) ->
|
491
|
+ if Keyword.has_key?(new_fields, k),
|
492
|
+ do: {:halt, false},
|
493
|
+ else: {:cont, true}
|
494
|
+
|
495
|
+ _, _ ->
|
496
|
+ {:halt, false}
|
497
|
+ end)
|
498
|
+
|
499
|
+ if valid?, do: old_fields ++ new_fields, else: :error
|
500
|
+ else
|
501
|
+ :error
|
502
|
+ end
|
503
|
+ end
|
504
|
+
|
505
|
+ defp merge_map_fields(_, _, true), do: :error
|
506
|
+
|
466
507
|
defp merge_argument_to_error({:&, _, [0]}, %{from: %{source: {source, alias}}}) do
|
467
508
|
"source #{inspect(source || alias)}"
|
468
509
|
end
|
changed
lib/ecto/query/builder/update.ex
|
@@ -92,7 +92,7 @@ defmodule Ecto.Query.Builder.Update do
|
92
92
|
|
93
93
|
defp compile_error!(expr) do
|
94
94
|
Builder.error! "malformed update `#{Macro.to_string(expr)}` in query expression, " <>
|
95
|
- "expected a keyword list with set/push/pop as keys with field-value " <>
|
95
|
+ "expected a keyword list with set/push/pull as keys with field-value " <>
|
96
96
|
"pairs as values"
|
97
97
|
end
|
98
98
|
|
|
@@ -186,7 +186,7 @@ defmodule Ecto.Query.Builder.Update do
|
186
186
|
defp runtime_error!(value) do
|
187
187
|
raise ArgumentError,
|
188
188
|
"malformed update `#{inspect(value)}` in query expression, " <>
|
189
|
- "expected a keyword list with set/push/pop as keys with field-value pairs as values"
|
189
|
+ "expected a keyword list with set/push/pull as keys with field-value pairs as values"
|
190
190
|
end
|
191
191
|
|
192
192
|
defp validate_op!(key) when key in @keys, do: :ok
|
changed
lib/ecto/query/builder/windows.ex
|
@@ -125,24 +125,25 @@ defmodule Ecto.Query.Builder.Windows do
|
125
125
|
end
|
126
126
|
|
127
127
|
defp escape_window(vars, {name, expr}, env) do
|
128
|
- {compile_acc, runtime_acc, {params, _acc}} = escape(expr, {[], %{}}, vars, env)
|
129
|
- {name, compile_acc, runtime_acc, Builder.escape_params(params)}
|
128
|
+ {compile_acc, runtime_acc, {params, acc}} = escape(expr, {[], %{subqueries: []}}, vars, env)
|
129
|
+ {name, compile_acc, runtime_acc, Builder.escape_params(params), acc}
|
130
130
|
end
|
131
131
|
|
132
|
- defp build_compile_window({name, compile_acc, _, params}, env) do
|
132
|
+ defp build_compile_window({name, compile_acc, _, params, acc}, env) do
|
133
133
|
{name,
|
134
134
|
quote do
|
135
|
- %Ecto.Query.QueryExpr{
|
135
|
+ %Ecto.Query.ByExpr{
|
136
136
|
expr: unquote(compile_acc),
|
137
137
|
params: unquote(params),
|
138
|
+ subqueries: unquote(acc.subqueries),
|
138
139
|
file: unquote(env.file),
|
139
140
|
line: unquote(env.line)
|
140
141
|
}
|
141
142
|
end}
|
142
143
|
end
|
143
144
|
|
144
|
- defp build_runtime_window({name, compile_acc, runtime_acc, params}, _env) do
|
145
|
- {:{}, [], [name, Enum.reverse(compile_acc), runtime_acc, Enum.reverse(params)]}
|
145
|
+ defp build_runtime_window({name, compile_acc, runtime_acc, params, acc}, _env) do
|
146
|
+ {:{}, [], [name, Enum.reverse(compile_acc), runtime_acc, Enum.reverse(params), {:%{}, [], Map.to_list(acc)}]}
|
146
147
|
end
|
147
148
|
|
148
149
|
@doc """
|
|
@@ -150,30 +151,32 @@ defmodule Ecto.Query.Builder.Windows do
|
150
151
|
"""
|
151
152
|
def runtime!(query, runtime, file, line) do
|
152
153
|
windows =
|
153
|
- Enum.map(runtime, fn {name, compile_acc, runtime_acc, params} ->
|
154
|
- {acc, params} = do_runtime_window!(runtime_acc, query, compile_acc, params)
|
155
|
- expr = %Ecto.Query.QueryExpr{expr: Enum.reverse(acc), params: Enum.reverse(params), file: file, line: line}
|
154
|
+ Enum.map(runtime, fn {name, compile_acc, runtime_acc, params, escape_acc} ->
|
155
|
+ {{acc, subqueries}, params} = do_runtime_window!(runtime_acc, query, {compile_acc, escape_acc.subqueries}, params)
|
156
|
+ expr = %Ecto.Query.ByExpr{expr: Enum.reverse(acc), params: Enum.reverse(params), file: file, line: line, subqueries: subqueries}
|
156
157
|
{name, expr}
|
157
158
|
end)
|
158
159
|
|
159
160
|
apply(query, windows)
|
160
161
|
end
|
161
162
|
|
162
|
- defp do_runtime_window!([{:order_by, order_by} | kw], query, acc, params) do
|
163
|
- {order_by, params} = OrderBy.order_by_or_distinct!(:order_by, query, order_by, params)
|
164
|
- do_runtime_window!(kw, query, [{:order_by, order_by} | acc], params)
|
163
|
+ defp do_runtime_window!([{:order_by, order_by} | kw], query, {acc, subqueries_acc}, params) do
|
164
|
+ {order_by, params, subqueries} = OrderBy.order_by_or_distinct!(:order_by, query, order_by, params)
|
165
|
+
|
166
|
+ do_runtime_window!(kw, query, {[{:order_by, order_by} | acc], subqueries_acc ++ subqueries}, params)
|
165
167
|
end
|
166
168
|
|
167
|
- defp do_runtime_window!([{:partition_by, partition_by} | kw], query, acc, params) do
|
168
|
- {partition_by, params} = GroupBy.group_or_partition_by!(:partition_by, query, partition_by, params)
|
169
|
- do_runtime_window!(kw, query, [{:partition_by, partition_by} | acc], params)
|
169
|
+ defp do_runtime_window!([{:partition_by, partition_by} | kw], query, {acc, subqueries_acc}, params) do
|
170
|
+ {partition_by, params, subqueries} = GroupBy.group_or_partition_by!(:partition_by, query, partition_by, params)
|
171
|
+
|
172
|
+ do_runtime_window!(kw, query, {[{:partition_by, partition_by} | acc], subqueries_acc ++ subqueries}, params)
|
170
173
|
end
|
171
174
|
|
172
|
- defp do_runtime_window!([{:frame, frame} | kw], query, acc, params) do
|
175
|
+ defp do_runtime_window!([{:frame, frame} | kw], query, {acc, subqueries_acc}, params) do
|
173
176
|
case frame do
|
174
177
|
%Ecto.Query.DynamicExpr{} ->
|
175
178
|
{frame, params, _count} = Builder.Dynamic.partially_expand(:windows, query, frame, params, length(params))
|
176
|
- do_runtime_window!(kw, query, [{:frame, frame} | acc], params)
|
179
|
+ do_runtime_window!(kw, query, {[{:frame, frame} | acc], subqueries_acc}, params)
|
177
180
|
|
178
181
|
_ ->
|
179
182
|
raise ArgumentError,
|
changed
lib/ecto/query/inspect.ex
|
@@ -14,7 +14,7 @@ defimpl Inspect, for: Ecto.Query.DynamicExpr do
|
14
14
|
aliases =
|
15
15
|
for({as, _} when is_atom(as) <- binding, do: as)
|
16
16
|
|> Enum.with_index()
|
17
|
- |> Map.new
|
17
|
+ |> Map.new()
|
18
18
|
|
19
19
|
query = %Ecto.Query{joins: joins, aliases: aliases}
|
20
20
|
|
|
@@ -54,12 +54,19 @@ defimpl Inspect, for: Ecto.Query do
|
54
54
|
%WithExpr{recursive: recursive, queries: [_ | _] = queries} ->
|
55
55
|
with_ctes =
|
56
56
|
Enum.map(queries, fn {name, cte_opts, query} ->
|
57
|
- cte = case query do
|
58
|
- %Ecto.Query{} -> __MODULE__.inspect(query, opts)
|
59
|
- %Ecto.Query.QueryExpr{} -> expr(query, {})
|
60
|
- end
|
57
|
+ cte =
|
58
|
+ case query do
|
59
|
+ %Ecto.Query{} -> __MODULE__.inspect(query, opts)
|
60
|
+ %Ecto.Query.QueryExpr{} -> expr(query, {})
|
61
|
+ end
|
61
62
|
|
62
|
- concat(["|> with_cte(\"" <> name <> "\", materialized: ", inspect(cte_opts[:materialized]), ", as: ", cte, ")"])
|
63
|
+ concat([
|
64
|
+ "|> with_cte(\"" <> name <> "\", materialized: ",
|
65
|
+ inspect(cte_opts[:materialized]),
|
66
|
+ ", as: ",
|
67
|
+ cte,
|
68
|
+ ")"
|
69
|
+ ])
|
63
70
|
end)
|
64
71
|
|
65
72
|
result = if recursive, do: glue(result, "\n", "|> recursive_ctes(true)"), else: result
|
|
@@ -135,10 +142,15 @@ defimpl Inspect, for: Ecto.Query do
|
135
142
|
end
|
136
143
|
|
137
144
|
defp inspect_source(%{source: %Ecto.Query{} = query}, _names), do: "^" <> inspect(query)
|
138
|
- defp inspect_source(%{source: %Ecto.SubQuery{query: query}}, _names), do: "subquery(#{to_string(query)})"
|
145
|
+
|
146
|
+ defp inspect_source(%{source: %Ecto.SubQuery{query: query}}, _names),
|
147
|
+ do: "subquery(#{to_string(query)})"
|
148
|
+
|
139
149
|
defp inspect_source(%{source: {source, nil}}, _names), do: inspect(source)
|
140
150
|
defp inspect_source(%{source: {nil, schema}}, _names), do: inspect(schema)
|
141
|
- defp inspect_source(%{source: {:fragment, _, _} = source} = part, names), do: "#{expr(source, names, part)}"
|
151
|
+
|
152
|
+ defp inspect_source(%{source: {:fragment, _, _} = source} = part, names),
|
153
|
+ do: "#{expr(source, names, part)}"
|
142
154
|
|
143
155
|
defp inspect_source(%{source: {:values, _, [types | _]}}, _names) do
|
144
156
|
fields = Keyword.keys(types)
|
|
@@ -233,40 +245,27 @@ defimpl Inspect, for: Ecto.Query do
|
233
245
|
@doc false
|
234
246
|
def expr(expr, names, part) do
|
235
247
|
expr
|
236
|
- |> Macro.traverse(:ok, &{prewalk(&1), &2}, &{postwalk(&1, names, part), &2})
|
248
|
+ |> Macro.traverse(:ok, &{prewalk(&1, names), &2}, &{postwalk(&1, names, part), &2})
|
237
249
|
|> elem(0)
|
238
250
|
|> macro_to_string()
|
239
251
|
end
|
240
252
|
|
241
|
- if Version.match?(System.version(), ">= 1.11.0") do
|
242
|
- defp macro_to_string(expr), do: Macro.to_string(expr)
|
243
|
- else
|
244
|
- defp macro_to_string(expr) do
|
245
|
- Macro.to_string(expr, fn
|
246
|
- {{:., _, [_, _]}, _, []}, string -> String.replace_suffix(string, "()", "")
|
247
|
- _other, string -> string
|
248
|
- end)
|
249
|
- end
|
250
|
- end
|
253
|
+ defp macro_to_string(expr), do: Macro.to_string(expr)
|
251
254
|
|
252
255
|
# Tagged values
|
253
|
- defp prewalk(%Ecto.Query.Tagged{value: value, tag: nil}) do
|
256
|
+ defp prewalk(%Ecto.Query.Tagged{value: value, tag: nil}, _) do
|
254
257
|
value
|
255
258
|
end
|
256
259
|
|
257
|
- defp prewalk(%Ecto.Query.Tagged{value: value, tag: {:parameterized, type, opts}}) do
|
258
|
- {:type, [], [value, {:{}, [], [:parameterized, type, opts]}]}
|
259
|
- end
|
260
|
-
|
261
|
- defp prewalk(%Ecto.Query.Tagged{value: value, tag: tag}) do
|
260
|
+ defp prewalk(%Ecto.Query.Tagged{value: value, tag: tag}, _) do
|
262
261
|
{:type, [], [value, tag]}
|
263
262
|
end
|
264
263
|
|
265
|
- defp prewalk({:type, _, [value, {:parameterized, type, opts}]}) do
|
266
|
- {:type, [], [value, {:{}, [], [:parameterized, type, opts]}]}
|
264
|
+ defp prewalk({{:., dot_meta, [{:&, _, [ix]}, field]}, meta, []}, names) do
|
265
|
+ {{:., dot_meta, [binding(names, ix), field]}, meta, []}
|
267
266
|
end
|
268
267
|
|
269
|
- defp prewalk(node) do
|
268
|
+ defp prewalk(node, _) do
|
270
269
|
node
|
271
270
|
end
|
272
271
|
|
|
@@ -337,10 +336,6 @@ defimpl Inspect, for: Ecto.Query do
|
337
336
|
end
|
338
337
|
end
|
339
338
|
|
340
|
- defp type_to_expr({:parameterized, type, opts}, _names, _part) do
|
341
|
- {:{}, [], [:parameterized, type, opts]}
|
342
|
- end
|
343
|
-
|
344
339
|
defp type_to_expr({ix, type}, names, part) when is_integer(ix) do
|
345
340
|
{{:., [], [binding_to_expr(ix, names, part), type]}, [no_parens: true], []}
|
346
341
|
end
|
|
@@ -369,8 +364,6 @@ defimpl Inspect, for: Ecto.Query do
|
369
364
|
defp join_qual(:full), do: :full_join
|
370
365
|
defp join_qual(:cross), do: :cross_join
|
371
366
|
defp join_qual(:cross_lateral), do: :cross_lateral_join
|
372
|
- defp join_qual(:array), do: :array_join
|
373
|
- defp join_qual(:left_array), do: :left_array_join
|
374
367
|
|
375
368
|
defp collect_sources(%{from: nil, joins: joins}) do
|
376
369
|
["query" | join_sources(joins)]
|
changed
lib/ecto/query/planner.ex
|
@@ -2,7 +2,16 @@ defmodule Ecto.Query.Planner do
|
2
2
|
# Normalizes a query and its parameters.
|
3
3
|
@moduledoc false
|
4
4
|
|
5
|
- alias Ecto.Query.{BooleanExpr, DynamicExpr, FromExpr, JoinExpr, QueryExpr, SelectExpr, LimitExpr}
|
5
|
+ alias Ecto.Query.{
|
6
|
+ BooleanExpr,
|
7
|
+ ByExpr,
|
8
|
+ DynamicExpr,
|
9
|
+ FromExpr,
|
10
|
+ JoinExpr,
|
11
|
+ QueryExpr,
|
12
|
+ SelectExpr,
|
13
|
+ LimitExpr
|
14
|
+ }
|
6
15
|
|
7
16
|
if map_size(%Ecto.Query{}) != 21 do
|
8
17
|
raise "Ecto.Query match out of date in builder"
|
|
@@ -29,7 +38,7 @@ defmodule Ecto.Query.Planner do
|
29
38
|
last = length(joins) + position
|
30
39
|
|
31
40
|
mapping = fn
|
32
|
- 0 -> last
|
41
|
+ 0 -> last
|
33
42
|
ix -> ix + position - 1
|
34
43
|
end
|
35
44
|
|
|
@@ -38,8 +47,12 @@ defmodule Ecto.Query.Planner do
|
38
47
|
end
|
39
48
|
end
|
40
49
|
|
41
|
- defp merge_expr_and_params(op, %QueryExpr{expr: left_expr, params: left_params} = struct,
|
42
|
- right_expr, right_params) do
|
50
|
+ defp merge_expr_and_params(
|
51
|
+ op,
|
52
|
+ %QueryExpr{expr: left_expr, params: left_params} = struct,
|
53
|
+ right_expr,
|
54
|
+ right_params
|
55
|
+ ) do
|
43
56
|
right_expr = Ecto.Query.Builder.bump_interpolations(right_expr, left_params)
|
44
57
|
%{struct | expr: merge_expr(op, left_expr, right_expr), params: left_params ++ right_params}
|
45
58
|
end
|
|
@@ -53,22 +66,25 @@ defmodule Ecto.Query.Planner do
|
53
66
|
"""
|
54
67
|
def rewrite_sources(%{expr: expr, params: params} = part, mapping) do
|
55
68
|
expr =
|
56
|
- Macro.prewalk expr, fn
|
69
|
+ Macro.prewalk(expr, fn
|
57
70
|
%Ecto.Query.Tagged{type: type, tag: tag} = tagged ->
|
58
71
|
%{tagged | type: rewrite_type(type, mapping), tag: rewrite_type(tag, mapping)}
|
72
|
+
|
59
73
|
{:&, meta, [ix]} ->
|
60
74
|
{:&, meta, [mapping.(ix)]}
|
75
|
+
|
61
76
|
other ->
|
62
77
|
other
|
63
|
- end
|
78
|
+ end)
|
64
79
|
|
65
80
|
params =
|
66
|
- Enum.map params, fn
|
81
|
+ Enum.map(params, fn
|
67
82
|
{val, type} ->
|
68
83
|
{val, rewrite_type(type, mapping)}
|
84
|
+
|
69
85
|
val ->
|
70
86
|
val
|
71
|
- end
|
87
|
+ end)
|
72
88
|
|
73
89
|
%{part | expr: expr, params: params}
|
74
90
|
end
|
|
@@ -127,10 +143,12 @@ defmodule Ecto.Query.Planner do
|
127
143
|
case query_lookup(key, query, operation, cache, adapter, counter) do
|
128
144
|
{_, select, prepared} ->
|
129
145
|
{build_meta(query, select), {:nocache, prepared}, cast_params, dump_params}
|
146
|
+
|
130
147
|
{_key, :cached, select, cached} ->
|
131
148
|
update = &cache_update(cache, key, &1)
|
132
149
|
reset = &cache_reset(cache, key, &1)
|
133
150
|
{build_meta(query, select), {:cached, update, reset, cached}, cast_params, dump_params}
|
151
|
+
|
134
152
|
{_key, :cache, select, prepared} ->
|
135
153
|
update = &cache_update(cache, key, &1)
|
136
154
|
{build_meta(query, select), {:cache, update, prepared}, cast_params, dump_params}
|
|
@@ -152,6 +170,7 @@ defmodule Ecto.Query.Planner do
|
152
170
|
case query_without_cache(query, operation, adapter, counter) do
|
153
171
|
{:cache, select, prepared} ->
|
154
172
|
cache_insert(cache, key, {key, :cache, select, prepared})
|
173
|
+
|
155
174
|
{:nocache, _, _} = nocache ->
|
156
175
|
nocache
|
157
176
|
end
|
|
@@ -161,6 +180,7 @@ defmodule Ecto.Query.Planner do
|
161
180
|
case :ets.insert_new(cache, elem) do
|
162
181
|
true ->
|
163
182
|
elem
|
183
|
+
|
164
184
|
false ->
|
165
185
|
[elem] = :ets.lookup(cache, key)
|
166
186
|
elem
|
|
@@ -197,77 +217,117 @@ defmodule Ecto.Query.Planner do
|
197
217
|
This function is called by the backend before invoking
|
198
218
|
any cache mechanism.
|
199
219
|
"""
|
200
|
- @spec plan(Ecto.Query.t, atom, module) :: {planned_query :: Ecto.Query.t, parameters :: list, cache_key :: any}
|
201
|
- def plan(query, operation, adapter) do
|
220
|
+ @spec plan(Ecto.Query.t(), atom(), module, map()) ::
|
221
|
+ {planned_query :: Ecto.Query.t(), parameters :: list(), cache_key :: any()}
|
222
|
+ def plan(query, operation, adapter, cte_names \\ %{}) do
|
223
|
+ {query, cte_names} = plan_ctes(query, adapter, cte_names)
|
224
|
+ plan_subquery = &plan_subquery(&1, query, nil, adapter, false, cte_names)
|
225
|
+
|
202
226
|
query
|
203
|
- |> plan_sources(adapter)
|
227
|
+ |> plan_sources(adapter, cte_names)
|
204
228
|
|> plan_assocs()
|
205
|
- |> plan_combinations(adapter)
|
206
|
- |> plan_ctes(adapter)
|
207
|
- |> plan_wheres(adapter)
|
208
|
- |> plan_select(adapter)
|
229
|
+ |> plan_combinations(adapter, cte_names)
|
230
|
+ |> plan_expr_subqueries(:wheres, plan_subquery)
|
231
|
+ |> plan_expr_subqueries(:havings, plan_subquery)
|
232
|
+ |> plan_expr_subqueries(:order_bys, plan_subquery)
|
233
|
+ |> plan_expr_subqueries(:group_bys, plan_subquery)
|
234
|
+ |> plan_expr_subquery(:distinct, plan_subquery)
|
235
|
+ |> plan_expr_subquery(:select, plan_subquery)
|
236
|
+ |> plan_windows(plan_subquery)
|
209
237
|
|> plan_cache(operation, adapter)
|
210
238
|
rescue
|
211
239
|
e ->
|
212
240
|
# Reraise errors so we ignore the planner inner stacktrace
|
213
|
- filter_and_reraise e, __STACKTRACE__
|
241
|
+ filter_and_reraise(e, __STACKTRACE__)
|
214
242
|
end
|
215
243
|
|
216
244
|
@doc """
|
217
245
|
Prepare all sources, by traversing and expanding from, joins, subqueries.
|
218
246
|
"""
|
219
|
- def plan_sources(query, adapter) do
|
220
|
- {from, source} = plan_from(query, adapter)
|
247
|
+ def plan_sources(query, adapter, cte_names) do
|
248
|
+ {from, source} = plan_from(query, adapter, cte_names)
|
221
249
|
|
222
250
|
# Set up the initial source so we can refer
|
223
251
|
# to the parent in subqueries in joins
|
224
252
|
query = %{query | sources: {source}}
|
225
253
|
|
226
|
- {joins, sources, tail_sources} = plan_joins(query, [source], length(query.joins), adapter)
|
254
|
+ {joins, sources, tail_sources} =
|
255
|
+ plan_joins(query, [source], length(query.joins), adapter, cte_names)
|
227
256
|
|
228
|
- %{query | from: from,
|
229
|
- joins: joins |> Enum.reverse,
|
230
|
- sources: (tail_sources ++ sources) |> Enum.reverse |> List.to_tuple()}
|
257
|
+ %{
|
258
|
+ query
|
259
|
+ | from: from,
|
260
|
+ joins: joins |> Enum.reverse(),
|
261
|
+ sources: (tail_sources ++ sources) |> Enum.reverse() |> List.to_tuple()
|
262
|
+ }
|
231
263
|
end
|
232
264
|
|
233
|
- defp plan_from(%{from: nil} = query, _adapter) do
|
265
|
+ defp plan_from(%{from: nil} = query, _adapter, _cte_names) do
|
234
266
|
error!(query, "query must have a from expression")
|
235
267
|
end
|
236
268
|
|
237
|
- defp plan_from(%{from: %{source: {kind, _, _}}, preloads: preloads, assocs: assocs} = query, _adapter)
|
269
|
+ defp plan_from(
|
270
|
+ %{from: %{source: {kind, _, _}}, preloads: preloads, assocs: assocs} = query,
|
271
|
+ _adapter,
|
272
|
+ _cte_names
|
273
|
+ )
|
238
274
|
when kind in [:fragment, :values] and (assocs != [] or preloads != []) do
|
239
275
|
error!(query, "cannot preload associations with a #{kind} source")
|
240
276
|
end
|
241
277
|
|
242
|
- defp plan_from(%{from: from} = query, adapter) do
|
243
|
- plan_source(query, from, adapter)
|
278
|
+ defp plan_from(%{from: from} = query, adapter, cte_names) do
|
279
|
+ plan_source(query, from, adapter, cte_names)
|
244
280
|
end
|
245
281
|
|
246
|
- defp plan_source(query, %{source: %Ecto.SubQuery{} = subquery, prefix: prefix} = expr, adapter) do
|
247
|
- subquery = plan_subquery(subquery, query, prefix, adapter, true)
|
282
|
+ defp plan_source(
|
283
|
+ query,
|
284
|
+ %{source: %Ecto.SubQuery{} = subquery, prefix: prefix} = expr,
|
285
|
+ adapter,
|
286
|
+ cte_names
|
287
|
+ ) do
|
288
|
+ subquery = plan_subquery(subquery, query, prefix, adapter, true, cte_names)
|
248
289
|
{%{expr | source: subquery}, subquery}
|
249
290
|
end
|
250
291
|
|
251
|
- defp plan_source(query, %{source: {nil, schema}} = expr, _adapter)
|
292
|
+ defp plan_source(query, %{source: {nil, schema}} = expr, _adapter, cte_names)
|
252
293
|
when is_atom(schema) and schema != nil do
|
253
294
|
source = schema.__schema__(:source)
|
254
|
- prefix = plan_source_schema_prefix(expr, schema) || query.prefix
|
295
|
+ source_prefix = plan_source_schema_prefix(expr, schema)
|
296
|
+
|
297
|
+ prefix =
|
298
|
+ case cte_names do
|
299
|
+ %{^source => _} -> source_prefix
|
300
|
+ _ -> source_prefix || query.prefix
|
301
|
+ end
|
302
|
+
|
255
303
|
{%{expr | source: {source, schema}}, {source, schema, prefix}}
|
256
304
|
end
|
257
305
|
|
258
|
- defp plan_source(query, %{source: {source, schema}, prefix: prefix} = expr, _adapter)
|
259
|
- when is_binary(source) and is_atom(schema),
|
260
|
- do: {expr, {source, schema, prefix || query.prefix}}
|
306
|
+ defp plan_source(query, %{source: {source, schema}, prefix: prefix} = expr, _adapter, cte_names)
|
307
|
+ when is_binary(source) and is_atom(schema) do
|
308
|
+ prefix =
|
309
|
+ case cte_names do
|
310
|
+ %{^source => _} -> prefix
|
311
|
+ _ -> prefix || query.prefix
|
312
|
+ end
|
261
313
|
|
262
|
- defp plan_source(_query, %{source: {kind, _, _} = source, prefix: nil} = expr, _adapter)
|
314
|
+ {expr, {source, schema, prefix}}
|
315
|
+ end
|
316
|
+
|
317
|
+ defp plan_source(
|
318
|
+ _query,
|
319
|
+ %{source: {kind, _, _} = source, prefix: nil} = expr,
|
320
|
+ _adapter,
|
321
|
+ _cte_names
|
322
|
+ )
|
263
323
|
when kind in [:fragment, :values],
|
264
324
|
do: {expr, source}
|
265
325
|
|
266
|
- defp plan_source(query, %{source: {kind, _, _}, prefix: prefix} = expr, _adapter)
|
326
|
+ defp plan_source(query, %{source: {kind, _, _}, prefix: prefix} = expr, _adapter, _cte_names)
|
267
327
|
when kind in [:fragment, :values],
|
268
328
|
do: error!(query, expr, "cannot set prefix: #{inspect(prefix)} option for #{kind} sources")
|
269
329
|
|
270
|
- defp plan_subquery(subquery, query, prefix, adapter, source?) do
|
330
|
+ defp plan_subquery(subquery, query, prefix, adapter, source?, cte_names) do
|
271
331
|
%{query: inner_query} = subquery
|
272
332
|
|
273
333
|
inner_query = %{
|
|
@@ -276,7 +336,7 @@ defmodule Ecto.Query.Planner do
|
276
336
|
aliases: Map.put(inner_query.aliases, @parent_as, query)
|
277
337
|
}
|
278
338
|
|
279
|
- {inner_query, params, key} = plan(inner_query, :all, adapter)
|
339
|
+ {inner_query, params, key} = plan(inner_query, :all, adapter, cte_names)
|
280
340
|
assert_no_subquery_assocs!(inner_query)
|
281
341
|
|
282
342
|
{inner_query, select} =
|
|
@@ -307,16 +367,26 @@ defmodule Ecto.Query.Planner do
|
307
367
|
end
|
308
368
|
|
309
369
|
defp normalize_subquery_select(query, adapter, source?) do
|
310
|
- {schema_or_source, expr, %{select: select} = query} = rewrite_subquery_select_expr(query, source?)
|
311
|
- {expr, _} = prewalk(expr, :select, query, select, 0, adapter)
|
312
|
- {{:map, types}, fields, _from} = collect_fields(expr, [], :none, query, select.take, true, %{})
|
370
|
+ {schema_or_source, expr, %{select: select} = query} =
|
371
|
+ rewrite_subquery_select_expr(query, source?)
|
372
|
+
|
373
|
+ {expr, counter} = prewalk(expr, :select, query, select, 0, adapter)
|
374
|
+
|
375
|
+ if counter != length(select.params) do
|
376
|
+ error!(query, "subqueries cannot select_merge onto an existing field that has an interpolation")
|
377
|
+ end
|
378
|
+
|
379
|
+ {{:map, types}, fields, _from} =
|
380
|
+ collect_fields(expr, [], :none, query, select.take, true, %{})
|
381
|
+
|
313
382
|
# types must take into account selected_as/2 aliases so that the correct fields are
|
314
383
|
# referenced when the outer query selects the entire subquery
|
315
384
|
types = normalize_subquery_types(types, Enum.reverse(fields), query.select.aliases, [])
|
316
385
|
{query, subquery_source(schema_or_source, types)}
|
317
386
|
end
|
318
387
|
|
319
|
- defp normalize_subquery_types(types, _fields, select_aliases, _acc) when select_aliases == %{} do
|
388
|
+ defp normalize_subquery_types(types, _fields, select_aliases, _acc)
|
389
|
+ when select_aliases == %{} do
|
320
390
|
types
|
321
391
|
end
|
322
392
|
|
|
@@ -324,11 +394,21 @@ defmodule Ecto.Query.Planner do
|
324
394
|
Enum.reverse(acc)
|
325
395
|
end
|
326
396
|
|
327
|
- defp normalize_subquery_types([{alias, _} = type | types], [{alias, _} | fields], select_aliases, acc) do
|
397
|
+ defp normalize_subquery_types(
|
398
|
+ [{alias, _} = type | types],
|
399
|
+ [{alias, _} | fields],
|
400
|
+ select_aliases,
|
401
|
+ acc
|
402
|
+ ) do
|
328
403
|
normalize_subquery_types(types, fields, select_aliases, [type | acc])
|
329
404
|
end
|
330
405
|
|
331
|
- defp normalize_subquery_types([{source_alias, type_value} | types], [field | fields], select_aliases, acc) do
|
406
|
+ defp normalize_subquery_types(
|
407
|
+ [{source_alias, type_value} | types],
|
408
|
+ [field | fields],
|
409
|
+ select_aliases,
|
410
|
+ acc
|
411
|
+ ) do
|
332
412
|
if Map.has_key?(select_aliases, source_alias) do
|
333
413
|
raise ArgumentError, """
|
334
414
|
the alias, #{inspect(source_alias)}, provided to `selected_as/2` conflicts
|
|
@@ -354,8 +434,11 @@ defmodule Ecto.Query.Planner do
|
354
434
|
|
355
435
|
defp subquery_source(nil, types), do: {:map, types}
|
356
436
|
defp subquery_source(name, types) when is_atom(name), do: {:struct, name, types}
|
437
|
+
|
357
438
|
defp subquery_source({:source, schema, prefix, types}, only) do
|
358
|
- types = Enum.map(only, fn {field, {:value, type}} -> {field, Keyword.get(types, field, type)} end)
|
439
|
+ types =
|
440
|
+ Enum.map(only, fn {field, {:value, type}} -> {field, Keyword.get(types, field, type)} end)
|
441
|
+
|
359
442
|
{:source, schema, prefix, types}
|
360
443
|
end
|
361
444
|
|
|
@@ -368,7 +451,10 @@ defmodule Ecto.Query.Planner do
|
368
451
|
{schema_or_source, expr, put_in(query.select.expr, expr)}
|
369
452
|
|
370
453
|
:error when source? ->
|
371
|
- error!(query, "subquery/cte must select a source (t), a field (t.field) or a map, got: `#{Macro.to_string(expr)}`")
|
454
|
+ error!(
|
455
|
+ query,
|
456
|
+ "subquery/cte must select a source (t), a field (t.field) or a map, got: `#{Macro.to_string(expr)}`"
|
457
|
+ )
|
372
458
|
|
373
459
|
:error ->
|
374
460
|
expr = {:%{}, [], [result: expr]}
|
|
@@ -381,10 +467,12 @@ defmodule Ecto.Query.Planner do
|
381
467
|
{right_struct, right_fields} = subquery_select(right, take, query)
|
382
468
|
{left_struct || right_struct, Keyword.merge(left_fields, right_fields)}
|
383
469
|
end
|
470
|
+
|
384
471
|
defp subquery_select({:%, _, [name, map]}, take, query) do
|
385
472
|
{_, fields} = subquery_select(map, take, query)
|
386
473
|
{name, fields}
|
387
474
|
end
|
475
|
+
|
388
476
|
defp subquery_select({:%{}, _, [{:|, _, [{:&, [], [ix]}, pairs]}]} = expr, take, query) do
|
389
477
|
assert_subquery_fields!(query, expr, pairs)
|
390
478
|
drop = Map.new(pairs, fn {key, _} -> {key, nil} end)
|
|
@@ -396,18 +484,22 @@ defmodule Ecto.Query.Planner do
|
396
484
|
kept_keys = subquery_source_fields(source) -- Keyword.keys(pairs)
|
397
485
|
{keep_source_or_struct(source), subquery_fields(kept_keys, ix) ++ pairs}
|
398
486
|
end
|
487
|
+
|
399
488
|
defp subquery_select({:%{}, _, pairs} = expr, _take, query) do
|
400
489
|
assert_subquery_fields!(query, expr, pairs)
|
401
490
|
{nil, pairs}
|
402
491
|
end
|
492
|
+
|
403
493
|
defp subquery_select({:&, _, [ix]}, take, query) do
|
404
494
|
{source, _} = source_take!(:select, query, take, ix, ix, %{})
|
405
495
|
fields = subquery_source_fields(source)
|
406
496
|
{keep_source_or_struct(source), subquery_fields(fields, ix)}
|
407
497
|
end
|
498
|
+
|
408
499
|
defp subquery_select({{:., _, [{:&, _, [_]}, field]}, _, []} = expr, _take, _query) do
|
409
500
|
{nil, [{field, expr}]}
|
410
501
|
end
|
502
|
+
|
411
503
|
defp subquery_select(_expr, _take, _query) do
|
412
504
|
:error
|
413
505
|
end
|
|
@@ -427,7 +519,10 @@ defmodule Ecto.Query.Planner do
|
427
519
|
defp subquery_source_fields({:map, types}), do: Keyword.keys(types)
|
428
520
|
|
429
521
|
defp subquery_type_for({:source, _, _, fields}, field), do: Keyword.fetch(fields, field)
|
430
|
- defp subquery_type_for({:struct, _name, types}, field), do: subquery_type_for_value(types, field)
|
522
|
+
|
523
|
+ defp subquery_type_for({:struct, _name, types}, field),
|
524
|
+ do: subquery_type_for_value(types, field)
|
525
|
+
|
431
526
|
defp subquery_type_for({:map, types}, field), do: subquery_type_for_value(types, field)
|
432
527
|
|
433
528
|
defp subquery_type_for_value(types, field) do
|
|
@@ -441,37 +536,55 @@ defmodule Ecto.Query.Planner do
|
441
536
|
defp assert_subquery_fields!(query, expr, pairs) do
|
442
537
|
Enum.each(pairs, fn
|
443
538
|
{key, _} when not is_atom(key) ->
|
444
|
- error!(query, "only atom keys are allowed when selecting a map in subquery, got: `#{Macro.to_string(expr)}`")
|
539
|
+ error!(
|
540
|
+ query,
|
541
|
+ "only atom keys are allowed when selecting a map in subquery, got: `#{Macro.to_string(expr)}`"
|
542
|
+ )
|
445
543
|
|
446
544
|
{key, value} ->
|
447
545
|
if valid_subquery_value?(value) do
|
448
546
|
{key, value}
|
449
547
|
else
|
450
|
- error!(query, "atoms, structs, maps, lists, tuples and sources are not allowed as map values in subquery, got: `#{Macro.to_string(expr)}`")
|
548
|
+ error!(
|
549
|
+ query,
|
550
|
+ "atoms, structs, maps, lists, tuples and sources are not allowed as map values in subquery, got: `#{Macro.to_string(expr)}`"
|
551
|
+ )
|
451
552
|
end
|
452
553
|
end)
|
453
554
|
end
|
454
555
|
|
455
556
|
defp valid_subquery_value?({_, _}), do: false
|
456
557
|
defp valid_subquery_value?(args) when is_list(args), do: false
|
558
|
+
|
457
559
|
defp valid_subquery_value?({container, _, args})
|
458
|
- when container in [:{}, :%{}, :&, :%] and is_list(args), do: false
|
560
|
+ when container in [:{}, :%{}, :&, :%] and is_list(args),
|
561
|
+ do: false
|
562
|
+
|
459
563
|
defp valid_subquery_value?(nil), do: true
|
460
564
|
defp valid_subquery_value?(arg) when is_atom(arg), do: is_boolean(arg)
|
461
565
|
defp valid_subquery_value?(_), do: true
|
462
566
|
|
463
|
- defp plan_joins(query, sources, offset, adapter) do
|
464
|
- plan_joins(query.joins, query, [], sources, [], 1, offset, adapter)
|
567
|
+ defp plan_joins(query, sources, offset, adapter, cte_names) do
|
568
|
+ plan_joins(query.joins, query, [], sources, [], 1, offset, adapter, cte_names)
|
465
569
|
end
|
466
570
|
|
467
|
- defp plan_joins([%JoinExpr{assoc: {ix, assoc}, qual: qual, on: on, prefix: prefix} = join|t],
|
468
|
- query, joins, sources, tail_sources, counter, offset, adapter) do
|
571
|
+ defp plan_joins(
|
572
|
+ [%JoinExpr{assoc: {ix, assoc}, qual: qual, on: on, prefix: prefix} = join | t],
|
573
|
+ query,
|
574
|
+ joins,
|
575
|
+ sources,
|
576
|
+ tail_sources,
|
577
|
+ counter,
|
578
|
+ offset,
|
579
|
+ adapter,
|
580
|
+ cte_names
|
581
|
+ ) do
|
469
582
|
source = fetch_source!(sources, ix)
|
470
583
|
schema = schema_for_association_join!(query, join, source)
|
471
584
|
refl = schema.__schema__(:association, assoc)
|
472
585
|
|
473
586
|
unless refl do
|
474
|
- error! query, join, "could not find association `#{assoc}` on schema #{inspect schema}"
|
587
|
+ error!(query, join, "could not find association `#{assoc}` on schema #{inspect(schema)}")
|
475
588
|
end
|
476
589
|
|
477
590
|
# If we have the following join:
|
|
@@ -499,16 +612,16 @@ defmodule Ecto.Query.Planner do
|
499
612
|
# 2. from and joins can have their prefixes explicitly
|
500
613
|
# overwritten by the join prefix
|
501
614
|
child = rewrite_prefix(child, query.prefix)
|
502
|
- child = update_in child.from, &rewrite_prefix(&1, prefix)
|
503
|
- child = update_in child.joins, &Enum.map(&1, fn join -> rewrite_prefix(join, prefix) end)
|
615
|
+ child = update_in(child.from, &rewrite_prefix(&1, prefix))
|
616
|
+ child = update_in(child.joins, &Enum.map(&1, fn join -> rewrite_prefix(join, prefix) end))
|
504
617
|
|
505
618
|
last_ix = length(child.joins)
|
506
619
|
source_ix = counter
|
507
620
|
|
508
|
- {_, child_from_source} = plan_source(child, child.from, adapter)
|
621
|
+ {_, child_from_source} = plan_source(child, child.from, adapter, cte_names)
|
509
622
|
|
510
623
|
{child_joins, child_sources, child_tail} =
|
511
|
- plan_joins(child, [child_from_source], offset + last_ix - 1, adapter)
|
624
|
+ plan_joins(child, [child_from_source], offset + last_ix - 1, adapter, cte_names)
|
512
625
|
|
513
626
|
# Rewrite joins indexes as mentioned above
|
514
627
|
child_joins = Enum.map(child_joins, &rewrite_join(&1, qual, ix, last_ix, source_ix, offset))
|
|
@@ -516,41 +629,115 @@ defmodule Ecto.Query.Planner do
|
516
629
|
# Drop the last resource which is the association owner (it is reversed)
|
517
630
|
child_sources = Enum.drop(child_sources, -1)
|
518
631
|
|
519
|
- [current_source|child_sources] = child_sources
|
632
|
+ [current_source | child_sources] = child_sources
|
520
633
|
child_sources = child_tail ++ child_sources
|
521
634
|
|
522
|
- plan_joins(t, query, attach_on(child_joins, on) ++ joins, [current_source|sources],
|
523
|
- child_sources ++ tail_sources, counter + 1, offset + length(child_sources), adapter)
|
635
|
+ plan_joins(
|
636
|
+ t,
|
637
|
+ query,
|
638
|
+ attach_on(child_joins, on) ++ joins,
|
639
|
+ [current_source | sources],
|
640
|
+ child_sources ++ tail_sources,
|
641
|
+ counter + 1,
|
642
|
+ offset + length(child_sources),
|
643
|
+ adapter,
|
644
|
+ cte_names
|
645
|
+ )
|
524
646
|
end
|
525
647
|
|
526
|
- defp plan_joins([%JoinExpr{source: %Ecto.Query{} = join_query, qual: qual, on: on, prefix: prefix} = join|t],
|
527
|
- query, joins, sources, tail_sources, counter, offset, adapter) do
|
648
|
+ defp plan_joins(
|
649
|
+ [
|
650
|
+ %JoinExpr{source: %Ecto.Query{} = join_query, qual: qual, on: on, prefix: prefix} =
|
651
|
+ join
|
652
|
+ | t
|
653
|
+ ],
|
654
|
+ query,
|
655
|
+ joins,
|
656
|
+ sources,
|
657
|
+ tail_sources,
|
658
|
+ counter,
|
659
|
+ offset,
|
660
|
+ adapter,
|
661
|
+ cte_names
|
662
|
+ ) do
|
528
663
|
case join_query do
|
529
|
- %{order_bys: [], limit: nil, offset: nil, group_bys: [], joins: [],
|
530
|
- havings: [], preloads: [], assocs: [], distinct: nil, lock: nil} ->
|
664
|
+ %{
|
665
|
+ order_bys: [],
|
666
|
+ limit: nil,
|
667
|
+ offset: nil,
|
668
|
+ group_bys: [],
|
669
|
+ joins: [],
|
670
|
+ havings: [],
|
671
|
+ preloads: [],
|
672
|
+ assocs: [],
|
673
|
+ distinct: nil,
|
674
|
+ lock: nil
|
675
|
+ } ->
|
531
676
|
join_query = rewrite_prefix(join_query, query.prefix)
|
532
677
|
from = rewrite_prefix(join_query.from, prefix)
|
533
|
- {from, source} = plan_source(join_query, from, adapter)
|
678
|
+ {from, source} = plan_source(join_query, from, adapter, cte_names)
|
534
679
|
[join] = attach_on(query_to_joins(qual, from.source, join_query, counter), on)
|
535
|
- plan_joins(t, query, [join|joins], [source|sources], tail_sources, counter + 1, offset, adapter)
|
680
|
+
|
681
|
+ plan_joins(
|
682
|
+ t,
|
683
|
+ query,
|
684
|
+ [join | joins],
|
685
|
+ [source | sources],
|
686
|
+ tail_sources,
|
687
|
+ counter + 1,
|
688
|
+ offset,
|
689
|
+ adapter,
|
690
|
+ cte_names
|
691
|
+ )
|
692
|
+
|
536
693
|
_ ->
|
537
|
- error! query, join, """
|
694
|
+ error!(query, join, """
|
538
695
|
invalid query was interpolated in a join.
|
539
696
|
If you want to pass a query to a join, you must either:
|
540
697
|
|
541
698
|
1. Make sure the query only has `where` conditions (which will be converted to ON clauses)
|
542
699
|
2. Or wrap the query in a subquery by calling subquery(query)
|
543
|
- """
|
700
|
+ """)
|
544
701
|
end
|
545
702
|
end
|
546
703
|
|
547
|
- defp plan_joins([%JoinExpr{} = join|t],
|
548
|
- query, joins, sources, tail_sources, counter, offset, adapter) do
|
549
|
- {join, source} = plan_source(query, %{join | ix: counter}, adapter)
|
550
|
- plan_joins(t, query, [join|joins], [source|sources], tail_sources, counter + 1, offset, adapter)
|
704
|
+ defp plan_joins(
|
705
|
+ [%JoinExpr{} = join | t],
|
706
|
+ query,
|
707
|
+ joins,
|
708
|
+ sources,
|
709
|
+ tail_sources,
|
710
|
+ counter,
|
711
|
+ offset,
|
712
|
+ adapter,
|
713
|
+ cte_names
|
714
|
+ ) do
|
715
|
+ {join, source} = plan_source(query, %{join | ix: counter}, adapter, cte_names)
|
716
|
+
|
717
|
+ plan_joins(
|
718
|
+ t,
|
719
|
+ query,
|
720
|
+ [join | joins],
|
721
|
+ [source | sources],
|
722
|
+ tail_sources,
|
723
|
+ counter + 1,
|
724
|
+ offset,
|
725
|
+ adapter,
|
726
|
+ cte_names
|
727
|
+ )
|
551
728
|
end
|
552
729
|
|
553
|
- defp plan_joins([], _query, joins, sources, tail_sources, _counter, _offset, _adapter) do
|
730
|
+ defp plan_joins(
|
731
|
+ [],
|
732
|
+ _query,
|
733
|
+ joins,
|
734
|
+ sources,
|
735
|
+ tail_sources,
|
736
|
+ _counter,
|
737
|
+ _offset,
|
738
|
+ _adapter,
|
739
|
+ _cte_names
|
740
|
+ ) do
|
554
741
|
{joins, sources, tail_sources}
|
555
742
|
end
|
556
743
|
|
|
@@ -563,19 +750,26 @@ defmodule Ecto.Query.Planner do
|
563
750
|
defp rewrite_prefix(expr, _prefix), do: expr
|
564
751
|
|
565
752
|
defp rewrite_join(%{on: on, ix: join_ix} = join, qual, ix, last_ix, source_ix, inc_ix) do
|
566
|
- expr = Macro.prewalk on.expr, fn
|
753
|
+ expr =
|
754
|
+ Macro.prewalk(on.expr, fn
|
567
755
|
{:&, meta, [join_ix]} ->
|
568
756
|
{:&, meta, [rewrite_ix(join_ix, ix, last_ix, source_ix, inc_ix)]}
|
757
|
+
|
569
758
|
expr = %Ecto.Query.Tagged{type: {type_ix, type}} when is_integer(type_ix) ->
|
570
759
|
%{expr | type: {rewrite_ix(type_ix, ix, last_ix, source_ix, inc_ix), type}}
|
760
|
+
|
571
761
|
other ->
|
572
762
|
other
|
573
|
- end
|
763
|
+ end)
|
574
764
|
|
575
765
|
params = Enum.map(on.params, &rewrite_param_ix(&1, ix, last_ix, source_ix, inc_ix))
|
576
766
|
|
577
|
- %{join | on: %{on | expr: expr, params: params}, qual: qual,
|
578
|
- ix: rewrite_ix(join_ix, ix, last_ix, source_ix, inc_ix)}
|
767
|
+ %{
|
768
|
+ join
|
769
|
+ | on: %{on | expr: expr, params: params},
|
770
|
+ qual: qual,
|
771
|
+ ix: rewrite_ix(join_ix, ix, last_ix, source_ix, inc_ix)
|
772
|
+ }
|
579
773
|
end
|
580
774
|
|
581
775
|
# We need to replace the source by the one from the assoc
|
|
@@ -590,11 +784,13 @@ defmodule Ecto.Query.Planner do
|
590
784
|
# All others need to be incremented by the offset sources
|
591
785
|
defp rewrite_ix(join_ix, _ix, _last_ix, _source_ix, inc_ix), do: join_ix + inc_ix
|
592
786
|
|
593
|
- defp rewrite_param_ix({value, {upper, {type_ix, field}}}, ix, last_ix, source_ix, inc_ix) when is_integer(type_ix) do
|
787
|
+ defp rewrite_param_ix({value, {upper, {type_ix, field}}}, ix, last_ix, source_ix, inc_ix)
|
788
|
+ when is_integer(type_ix) do
|
594
789
|
{value, {upper, {rewrite_ix(type_ix, ix, last_ix, source_ix, inc_ix), field}}}
|
595
790
|
end
|
596
791
|
|
597
|
- defp rewrite_param_ix({value, {type_ix, field}}, ix, last_ix, source_ix, inc_ix) when is_integer(type_ix) do
|
792
|
+ defp rewrite_param_ix({value, {type_ix, field}}, ix, last_ix, source_ix, inc_ix)
|
793
|
+ when is_integer(type_ix) do
|
598
794
|
{value, {rewrite_ix(type_ix, ix, last_ix, source_ix, inc_ix), field}}
|
599
795
|
end
|
600
796
|
|
|
@@ -602,23 +798,31 @@ defmodule Ecto.Query.Planner do
|
602
798
|
|
603
799
|
defp fetch_source!(sources, ix) when is_integer(ix) do
|
604
800
|
case Enum.reverse(sources) |> Enum.fetch(ix) do
|
605
|
- {:ok, source} -> source
|
606
|
- :error -> raise ArgumentError, "could not find a source with index `#{ix}` in `#{inspect sources}"
|
801
|
+ {:ok, source} ->
|
802
|
+ source
|
803
|
+
|
804
|
+ :error ->
|
805
|
+ raise ArgumentError, "could not find a source with index `#{ix}` in `#{inspect(sources)}"
|
607
806
|
end
|
608
807
|
end
|
609
808
|
|
610
809
|
defp fetch_source!(_, ix) do
|
611
|
- raise ArgumentError, "invalid binding index: `#{inspect ix}` (check if you're binding using a valid :as atom)"
|
810
|
+ raise ArgumentError,
|
811
|
+ "invalid binding index: `#{inspect(ix)}` (check if you're binding using a valid :as atom)"
|
612
812
|
end
|
613
813
|
|
614
814
|
defp schema_for_association_join!(query, join, source) do
|
615
815
|
case source do
|
616
816
|
{:fragment, _, _} ->
|
617
|
- error! query, join, "cannot perform association joins on fragment sources"
|
817
|
+ error!(query, join, "cannot perform association joins on fragment sources")
|
618
818
|
|
619
819
|
{source, nil, _} ->
|
620
|
- error! query, join, "cannot perform association join on #{inspect source} " <>
|
621
|
- "because it does not have a schema"
|
820
|
+ error!(
|
821
|
+ query,
|
822
|
+ join,
|
823
|
+ "cannot perform association join on #{inspect(source)} " <>
|
824
|
+ "because it does not have a schema"
|
825
|
+ )
|
622
826
|
|
623
827
|
{_, schema, _} ->
|
624
828
|
schema
|
|
@@ -630,48 +834,67 @@ defmodule Ecto.Query.Planner do
|
630
834
|
schema
|
631
835
|
|
632
836
|
%Ecto.SubQuery{} ->
|
633
|
- error! query, join, "can only perform association joins on subqueries " <>
|
634
|
- "that return a source with schema in select"
|
837
|
+ error!(
|
838
|
+ query,
|
839
|
+ join,
|
840
|
+ "can only perform association joins on subqueries " <>
|
841
|
+ "that return a source with schema in select"
|
842
|
+ )
|
635
843
|
|
636
844
|
_ ->
|
637
|
- error! query, join, "can only perform association joins on sources with a schema"
|
845
|
+ error!(query, join, "can only perform association joins on sources with a schema")
|
638
846
|
end
|
639
847
|
end
|
640
848
|
|
641
|
- @spec plan_wheres(Ecto.Query.t, module) :: Ecto.Query.t
|
642
|
- defp plan_wheres(query, adapter) do
|
643
|
- wheres =
|
644
|
- Enum.map(query.wheres, fn
|
645
|
- %{subqueries: []} = where ->
|
646
|
- where
|
647
|
-
|
648
|
- %{subqueries: subqueries} = where ->
|
649
|
- %{where | subqueries: Enum.map(subqueries, &plan_subquery(&1, query, nil, adapter, false))}
|
650
|
- end)
|
651
|
-
|
652
|
- havings =
|
653
|
- Enum.map(query.havings, fn
|
654
|
- %{subqueries: []} = having ->
|
655
|
- having
|
656
|
-
|
657
|
- %{subqueries: subqueries} = having ->
|
658
|
- %{having | subqueries: Enum.map(subqueries, &plan_subquery(&1, query, nil, adapter, false))}
|
659
|
- end)
|
660
|
-
|
661
|
- %{query | wheres: wheres, havings: havings}
|
849
|
+ # An optimized version of plan subqueries that only modifies the query when necessary.
|
850
|
+ defp plan_expr_subqueries(query, key, fun) do
|
851
|
+ query
|
852
|
+ |> Map.fetch!(key)
|
853
|
+ |> plan_expr_subqueries([], query, key, fun)
|
662
854
|
end
|
663
855
|
|
664
|
- @spec plan_select(Ecto.Query.t, module) :: Ecto.Query.t
|
665
|
- defp plan_select(query, adapter) do
|
666
|
- case query do
|
667
|
- %{select: %{subqueries: [_ | _] = subqueries}} ->
|
668
|
- subqueries = Enum.map(subqueries, &plan_subquery(&1, query, nil, adapter, false))
|
669
|
- put_in(query.select.subqueries, subqueries)
|
856
|
+ defp plan_expr_subqueries([%{subqueries: []} = head | tail], acc, query, key, fun) do
|
857
|
+ plan_expr_subqueries(tail, [head | acc], query, key, fun)
|
858
|
+ end
|
670
859
|
|
671
|
- query -> query
|
860
|
+ defp plan_expr_subqueries([head | tail], acc, query, key, fun) do
|
861
|
+ exprs =
|
862
|
+ Enum.reduce([head | tail], acc, fn
|
863
|
+ %{subqueries: []} = expr, acc ->
|
864
|
+ [expr | acc]
|
865
|
+
|
866
|
+ %{subqueries: subqueries} = expr, acc ->
|
867
|
+ [%{expr | subqueries: Enum.map(subqueries, fun)} | acc]
|
868
|
+ end)
|
869
|
+
|
870
|
+ %{query | key => Enum.reverse(exprs)}
|
871
|
+ end
|
872
|
+
|
873
|
+ defp plan_expr_subqueries([], _acc, query, _key, _fun) do
|
874
|
+ query
|
875
|
+ end
|
876
|
+
|
877
|
+ defp plan_expr_subquery(query, key, fun) do
|
878
|
+ with %{^key => %{subqueries: [_ | _] = subqueries} = expr} <- query do
|
879
|
+ %{query | key => %{expr | subqueries: Enum.map(subqueries, fun)}}
|
672
880
|
end
|
673
881
|
end
|
674
882
|
|
883
|
+ defp plan_windows(%{windows: []} = query, _fun), do: query
|
884
|
+
|
885
|
+ defp plan_windows(query, fun) do
|
886
|
+ windows =
|
887
|
+ Enum.map(query.windows, fn
|
888
|
+ {key, %{subqueries: []} = window} ->
|
889
|
+ {key, window}
|
890
|
+
|
891
|
+ {key, %{subqueries: subqueries} = window} ->
|
892
|
+ {key, %{window | subqueries: Enum.map(subqueries, fun)}}
|
893
|
+ end)
|
894
|
+
|
895
|
+ %{query | windows: windows}
|
896
|
+ end
|
897
|
+
|
675
898
|
@doc """
|
676
899
|
Prepare the parameters by merging and casting them according to sources.
|
677
900
|
"""
|
|
@@ -693,7 +916,7 @@ defmodule Ecto.Query.Planner do
|
693
916
|
end
|
694
917
|
|
695
918
|
defp merge_cache(kind, query, expr, {cache, params}, _operation, adapter)
|
696
|
- when kind in ~w(select distinct limit offset)a do
|
919
|
+ when kind in ~w(select distinct limit offset)a do
|
697
920
|
if expr do
|
698
921
|
{params, cacheable?} = cast_and_merge_params(kind, query, expr, params, adapter)
|
699
922
|
{merge_cache({kind, expr_to_cache(expr)}, cache, cacheable?), params}
|
|
@@ -703,56 +926,59 @@ defmodule Ecto.Query.Planner do
|
703
926
|
end
|
704
927
|
|
705
928
|
defp merge_cache(kind, query, exprs, {cache, params}, _operation, adapter)
|
706
|
- when kind in ~w(where update group_by having order_by)a do
|
929
|
+ when kind in ~w(where update group_by having order_by)a do
|
707
930
|
{expr_cache, {params, cacheable?}} =
|
708
|
- Enum.map_reduce exprs, {params, true}, fn expr, {params, cacheable?} ->
|
931
|
+ Enum.map_reduce(exprs, {params, true}, fn expr, {params, cacheable?} ->
|
709
932
|
{params, current_cacheable?} = cast_and_merge_params(kind, query, expr, params, adapter)
|
710
933
|
{expr_to_cache(expr), {params, cacheable? and current_cacheable?}}
|
711
|
- end
|
934
|
+ end)
|
712
935
|
|
713
936
|
case expr_cache do
|
714
937
|
[] -> {cache, params}
|
715
|
- _ -> {merge_cache({kind, expr_cache}, cache, cacheable?), params}
|
938
|
+ _ -> {merge_cache({kind, expr_cache}, cache, cacheable?), params}
|
716
939
|
end
|
717
940
|
end
|
718
941
|
|
719
942
|
defp merge_cache(:join, query, exprs, {cache, params}, _operation, adapter) do
|
720
943
|
{expr_cache, {params, cacheable?}} =
|
721
|
- Enum.map_reduce exprs, {params, true}, fn
|
944
|
+ Enum.map_reduce(exprs, {params, true}, fn
|
722
945
|
%JoinExpr{on: on, qual: qual, hints: hints} = join, {params, cacheable?} ->
|
723
946
|
{key, params} = source_cache(join, params)
|
724
947
|
{params, join_cacheable?} = cast_and_merge_params(:join, query, join, params, adapter)
|
725
948
|
{params, on_cacheable?} = cast_and_merge_params(:join, query, on, params, adapter)
|
949
|
+
|
726
950
|
{{qual, key, on.expr, hints},
|
727
951
|
{params, cacheable? and join_cacheable? and on_cacheable? and key != :nocache}}
|
728
|
- end
|
952
|
+ end)
|
729
953
|
|
730
954
|
case expr_cache do
|
731
955
|
[] -> {cache, params}
|
732
|
- _ -> {merge_cache({:join, expr_cache}, cache, cacheable?), params}
|
956
|
+ _ -> {merge_cache({:join, expr_cache}, cache, cacheable?), params}
|
733
957
|
end
|
734
958
|
end
|
735
959
|
|
736
960
|
defp merge_cache(:windows, query, exprs, {cache, params}, _operation, adapter) do
|
737
961
|
{expr_cache, {params, cacheable?}} =
|
738
|
- Enum.map_reduce exprs, {params, true}, fn {key, expr}, {params, cacheable?} ->
|
739
|
- {params, current_cacheable?} = cast_and_merge_params(:windows, query, expr, params, adapter)
|
962
|
+ Enum.map_reduce(exprs, {params, true}, fn {key, expr}, {params, cacheable?} ->
|
963
|
+ {params, current_cacheable?} =
|
964
|
+ cast_and_merge_params(:windows, query, expr, params, adapter)
|
965
|
+
|
740
966
|
{{key, expr_to_cache(expr)}, {params, cacheable? and current_cacheable?}}
|
741
|
- end
|
967
|
+ end)
|
742
968
|
|
743
969
|
case expr_cache do
|
744
970
|
[] -> {cache, params}
|
745
|
- _ -> {merge_cache({:windows, expr_cache}, cache, cacheable?), params}
|
971
|
+ _ -> {merge_cache({:windows, expr_cache}, cache, cacheable?), params}
|
746
972
|
end
|
747
973
|
end
|
748
974
|
|
749
975
|
defp merge_cache(:combination, _query, combinations, cache_and_params, operation, adapter) do
|
750
976
|
# In here we add each combination as its own entry in the cache key.
|
751
977
|
# We could group them to avoid multiple keys, but since they are uncommon, we keep it simple.
|
752
|
- Enum.reduce combinations, cache_and_params, fn {modifier, query}, {cache, params} ->
|
978
|
+ Enum.reduce(combinations, cache_and_params, fn {modifier, query}, {cache, params} ->
|
753
979
|
{_, params, inner_cache} = traverse_cache(query, operation, {[], params}, adapter)
|
754
980
|
{merge_cache({modifier, inner_cache}, cache, inner_cache != :nocache), params}
|
755
|
- end
|
981
|
+ end)
|
756
982
|
end
|
757
983
|
|
758
984
|
defp merge_cache(:with_cte, _query, nil, cache_and_params, _operation, _adapter) do
|
|
@@ -765,33 +991,56 @@ defmodule Ecto.Query.Planner do
|
765
991
|
|
766
992
|
# In here we add each cte as its own entry in the cache key.
|
767
993
|
# We could group them to avoid multiple keys, but since they are uncommon, we keep it simple.
|
768
|
- Enum.reduce queries, cache_and_params, fn
|
994
|
+ Enum.reduce(queries, cache_and_params, fn
|
769
995
|
{name, opts, %Ecto.Query{} = query}, {cache, params} ->
|
770
996
|
{_, params, inner_cache} = traverse_cache(query, :all, {[], params}, adapter)
|
771
|
- {merge_cache({key, name, opts[:materialized], opts[:operation], inner_cache}, cache, inner_cache != :nocache), params}
|
997
|
+
|
998
|
+ {merge_cache(
|
999
|
+ {key, name, opts[:materialized], opts[:operation], inner_cache},
|
1000
|
+ cache,
|
1001
|
+ inner_cache != :nocache
|
1002
|
+ ), params}
|
772
1003
|
|
773
1004
|
{name, opts, %Ecto.Query.QueryExpr{} = query_expr}, {cache, params} ->
|
774
|
- {params, cacheable?} = cast_and_merge_params(:with_cte, query, query_expr, params, adapter)
|
775
|
- {merge_cache({key, name, opts[:materialized], opts[:operation], expr_to_cache(query_expr)}, cache, cacheable?), params}
|
776
|
- end
|
1005
|
+ {params, cacheable?} =
|
1006
|
+ cast_and_merge_params(:with_cte, query, query_expr, params, adapter)
|
1007
|
+
|
1008
|
+ {merge_cache(
|
1009
|
+ {key, name, opts[:materialized], opts[:operation], expr_to_cache(query_expr)},
|
1010
|
+ cache,
|
1011
|
+ cacheable?
|
1012
|
+ ), params}
|
1013
|
+ end)
|
777
1014
|
end
|
778
1015
|
|
779
1016
|
defp expr_to_cache(%QueryExpr{expr: expr}), do: expr
|
1017
|
+
|
780
1018
|
defp expr_to_cache(%SelectExpr{expr: expr, subqueries: []}), do: expr
|
1019
|
+
|
781
1020
|
defp expr_to_cache(%SelectExpr{expr: expr, subqueries: subqueries}) do
|
782
1021
|
{expr, Enum.map(subqueries, fn %{cache: cache} -> {:subquery, cache} end)}
|
783
1022
|
end
|
1023
|
+
|
1024
|
+ defp expr_to_cache(%ByExpr{expr: expr, subqueries: []}), do: expr
|
1025
|
+
|
1026
|
+ defp expr_to_cache(%ByExpr{expr: expr, subqueries: subqueries}) do
|
1027
|
+ {expr, Enum.map(subqueries, fn %{cache: cache} -> {:subquery, cache} end)}
|
1028
|
+ end
|
1029
|
+
|
784
1030
|
defp expr_to_cache(%BooleanExpr{op: op, expr: expr, subqueries: []}), do: {op, expr}
|
1031
|
+
|
785
1032
|
defp expr_to_cache(%BooleanExpr{op: op, expr: expr, subqueries: subqueries}) do
|
786
1033
|
# Alternate implementation could be replace {:subquery, i} expression in expr.
|
787
1034
|
# Current strategy appends [{:subquery, i, cache}], where cache is the cache key for this subquery.
|
788
1035
|
{op, expr, Enum.map(subqueries, fn %{cache: cache} -> {:subquery, cache} end)}
|
789
1036
|
end
|
1037
|
+
|
790
1038
|
defp expr_to_cache(%LimitExpr{expr: expr, with_ties: with_ties}), do: {with_ties, expr}
|
791
1039
|
|
792
|
- @spec cast_and_merge_params(atom, Ecto.Query.t, any, list, module) :: {params :: list, cacheable? :: boolean}
|
1040
|
+ @spec cast_and_merge_params(atom, Ecto.Query.t(), any, list, module) ::
|
1041
|
+ {params :: list, cacheable? :: boolean}
|
793
1042
|
defp cast_and_merge_params(kind, query, expr, params, adapter) do
|
794
|
- Enum.reduce expr.params, {params, true}, fn
|
1043
|
+ Enum.reduce(expr.params, {params, true}, fn
|
795
1044
|
{:subquery, i}, {acc, cacheable?} ->
|
796
1045
|
# This is the place holder to intersperse subquery parameters.
|
797
1046
|
%Ecto.SubQuery{params: subparams, cache: cache} = Enum.fetch!(expr.subqueries, i)
|
|
@@ -803,16 +1052,16 @@ defmodule Ecto.Query.Planner do
|
803
1052
|
{cast_v, {:splice, dump_v}} -> {split_variadic_params(cast_v, dump_v, acc), cacheable?}
|
804
1053
|
cast_v_and_dump_v -> {[cast_v_and_dump_v | acc], cacheable?}
|
805
1054
|
end
|
806
|
- end
|
1055
|
+ end)
|
807
1056
|
end
|
808
1057
|
|
809
1058
|
defp split_variadic_params(cast_v, dump_v, acc) do
|
810
1059
|
Enum.zip(cast_v, dump_v) |> Enum.reverse(acc)
|
811
1060
|
end
|
812
1061
|
|
813
|
- defp merge_cache(_left, _right, false), do: :nocache
|
1062
|
+ defp merge_cache(_left, _right, false), do: :nocache
|
814
1063
|
defp merge_cache(_left, :nocache, true), do: :nocache
|
815
|
- defp merge_cache(left, right, true), do: [left|right]
|
1064
|
+ defp merge_cache(left, right, true), do: [left | right]
|
816
1065
|
|
817
1066
|
defp finalize_cache(_query, _operation, :nocache) do
|
818
1067
|
:nocache
|
|
@@ -826,16 +1075,17 @@ defmodule Ecto.Query.Planner do
|
826
1075
|
case select do
|
827
1076
|
%{take: take} when take != %{} ->
|
828
1077
|
[take: take] ++ cache
|
1078
|
+
|
829
1079
|
_ ->
|
830
1080
|
cache
|
831
1081
|
end
|
832
1082
|
|
833
1083
|
cache =
|
834
1084
|
cache
|
835
|
- |> prepend_if(assocs != [], [assocs: assocs])
|
836
|
- |> prepend_if(prefix != nil, [prefix: prefix])
|
837
|
- |> prepend_if(lock != nil, [lock: lock])
|
838
|
- |> prepend_if(aliases != %{}, [aliases: aliases])
|
1085
|
+ |> prepend_if(assocs != [], assocs: assocs)
|
1086
|
+ |> prepend_if(prefix != nil, prefix: prefix)
|
1087
|
+ |> prepend_if(lock != nil, lock: lock)
|
1088
|
+ |> prepend_if(aliases != %{}, aliases: aliases)
|
839
1089
|
|
840
1090
|
[operation | cache]
|
841
1091
|
end
|
|
@@ -845,26 +1095,47 @@ defmodule Ecto.Query.Planner do
|
845
1095
|
|
846
1096
|
defp source_cache(%{source: {_, nil} = source, prefix: prefix}, params),
|
847
1097
|
do: {{source, prefix}, params}
|
1098
|
+
|
848
1099
|
defp source_cache(%{source: {bin, schema}, prefix: prefix}, params),
|
849
1100
|
do: {{bin, schema, schema.__schema__(:hash), prefix}, params}
|
850
|
- defp source_cache(%{source: {kind, _, _} = source, prefix: prefix}, params)
|
851
|
- when kind in [:fragment, :values],
|
852
|
- do: {{source, prefix}, params}
|
1101
|
+
|
1102
|
+ defp source_cache(%{source: {:fragment, _, _} = source, prefix: prefix}, params),
|
1103
|
+ do: {{source, prefix}, params}
|
1104
|
+
|
1105
|
+ defp source_cache(%{source: {:values, _, _}}, params),
|
1106
|
+ do: {:nocache, params}
|
1107
|
+
|
853
1108
|
defp source_cache(%{source: %Ecto.SubQuery{params: inner, cache: key}}, params),
|
854
1109
|
do: {key, Enum.reverse(inner, params)}
|
855
1110
|
|
856
1111
|
defp cast_param(_kind, query, expr, %DynamicExpr{}, _type, _value) do
|
857
|
- error! query, expr, "invalid dynamic expression",
|
858
|
- "dynamic expressions can only be interpolated at the top level of where, having, group_by, order_by, select, update or a join's on"
|
1112
|
+ error!(
|
1113
|
+ query,
|
1114
|
+ expr,
|
1115
|
+ "invalid dynamic expression",
|
1116
|
+ "dynamic expressions can only be interpolated at the top level of where, having, group_by, order_by, select, update or a join's on"
|
1117
|
+ )
|
859
1118
|
end
|
1119
|
+
|
860
1120
|
defp cast_param(_kind, query, expr, [{key, _} | _], _type, _value) when is_atom(key) do
|
861
|
- error! query, expr, "invalid keyword list",
|
862
|
- "keyword lists are only allowed at the top level of where, having, distinct, order_by, update or a join's on"
|
1121
|
+ error!(
|
1122
|
+ query,
|
1123
|
+ expr,
|
1124
|
+ "invalid keyword list",
|
1125
|
+ "keyword lists are only allowed at the top level of where, having, distinct, order_by, update or a join's on"
|
1126
|
+ )
|
863
1127
|
end
|
864
|
- defp cast_param(_kind, query, expr, %x{}, {:in, _type}, _value) when x in [Ecto.Query, Ecto.SubQuery] do
|
865
|
- error! query, expr, "an #{inspect(x)} struct is not supported as right-side value of `in` operator",
|
866
|
- "Did you mean to write `expr in subquery(query)` instead?"
|
1128
|
+
|
1129
|
+ defp cast_param(_kind, query, expr, %x{}, {:in, _type}, _value)
|
1130
|
+ when x in [Ecto.Query, Ecto.SubQuery] do
|
1131
|
+ error!(
|
1132
|
+ query,
|
1133
|
+ expr,
|
1134
|
+ "an #{inspect(x)} struct is not supported as right-side value of `in` operator",
|
1135
|
+ "Did you mean to write `expr in subquery(query)` instead?"
|
1136
|
+ )
|
867
1137
|
end
|
1138
|
+
|
868
1139
|
defp cast_param(kind, query, expr, v, type, adapter) do
|
869
1140
|
type = field_type!(kind, query, expr, type)
|
870
1141
|
|
|
@@ -895,57 +1166,80 @@ defmodule Ecto.Query.Planner do
|
895
1166
|
end
|
896
1167
|
|
897
1168
|
defp plan_assocs(_query, _ix, []), do: :ok
|
898
|
- defp plan_assocs(query, ix, assocs) do
|
899
|
- # We validate the schema exists when preparing joins above
|
900
|
- {_, parent_schema, _} = get_preload_source!(query, ix)
|
901
1169
|
|
902
|
- Enum.each assocs, fn {assoc, {child_ix, child_assocs}} ->
|
1170
|
+ defp plan_assocs(query, ix, assocs) do
|
1171
|
+ # We validate the schema exists when preparing joins.
|
1172
|
+ parent_schema =
|
1173
|
+ case get_preload_source!(query, ix) do
|
1174
|
+ {_, schema, _} ->
|
1175
|
+ schema
|
1176
|
+
|
1177
|
+ %Ecto.SubQuery{select: {:source, {_, schema}, _, _}} ->
|
1178
|
+ schema
|
1179
|
+ end
|
1180
|
+
|
1181
|
+ Enum.each(assocs, fn {assoc, {child_ix, child_assocs}} ->
|
903
1182
|
refl = parent_schema.__schema__(:association, assoc)
|
904
1183
|
|
905
1184
|
unless refl do
|
906
|
- error! query, "field `#{inspect parent_schema}.#{assoc}` " <>
|
907
|
- "in preload is not an association"
|
1185
|
+ error!(
|
1186
|
+ query,
|
1187
|
+ "field `#{inspect(parent_schema)}.#{assoc}` " <>
|
1188
|
+ "in preload is not an association"
|
1189
|
+ )
|
908
1190
|
end
|
909
1191
|
|
910
1192
|
case find_source_expr(query, child_ix) do
|
911
1193
|
%JoinExpr{qual: qual} when qual in [:inner, :left, :inner_lateral, :left_lateral] ->
|
912
1194
|
:ok
|
1195
|
+
|
913
1196
|
%JoinExpr{qual: qual} ->
|
914
|
- error! query, "association `#{inspect parent_schema}.#{assoc}` " <>
|
915
|
- "in preload requires an inner, left or lateral join, got #{qual} join"
|
1197
|
+ error!(
|
1198
|
+ query,
|
1199
|
+ "association `#{inspect(parent_schema)}.#{assoc}` " <>
|
1200
|
+ "in preload requires an inner, left or lateral join, got #{qual} join"
|
1201
|
+ )
|
1202
|
+
|
916
1203
|
_ ->
|
917
1204
|
:ok
|
918
1205
|
end
|
919
1206
|
|
920
1207
|
plan_assocs(query, child_ix, child_assocs)
|
921
|
- end
|
1208
|
+ end)
|
922
1209
|
end
|
923
1210
|
|
924
|
- defp plan_combinations(query, adapter) do
|
1211
|
+ defp plan_combinations(query, adapter, cte_names) do
|
925
1212
|
combinations =
|
926
|
- Enum.map query.combinations, fn {type, combination_query} ->
|
927
|
- {prepared_query, _params, _key} = combination_query |> attach_prefix(query) |> plan(:all, adapter)
|
1213
|
+ Enum.map(query.combinations, fn {type, combination_query} ->
|
1214
|
+ {prepared_query, _params, _key} =
|
1215
|
+ combination_query |> attach_prefix(query) |> plan(:all, adapter, cte_names)
|
1216
|
+
|
928
1217
|
prepared_query = prepared_query |> ensure_select(true)
|
929
1218
|
{type, prepared_query}
|
930
|
- end
|
1219
|
+ end)
|
931
1220
|
|
932
1221
|
%{query | combinations: combinations}
|
933
1222
|
end
|
934
1223
|
|
935
|
- defp plan_ctes(%Ecto.Query{with_ctes: nil} = query, _adapter), do: query
|
936
|
- defp plan_ctes(%Ecto.Query{with_ctes: %{queries: queries}} = query, adapter) do
|
937
|
- queries =
|
938
|
- Enum.map queries, fn
|
939
|
- {name, opts, %Ecto.Query{} = cte_query} ->
|
940
|
- {planned_query, _params, _key} = cte_query |> attach_prefix(query) |> plan(:all, adapter)
|
1224
|
+ defp plan_ctes(%Ecto.Query{with_ctes: nil} = query, _adapter, cte_names), do: {query, cte_names}
|
1225
|
+
|
1226
|
+ defp plan_ctes(%Ecto.Query{with_ctes: %{queries: queries}} = query, adapter, cte_names) do
|
1227
|
+ {queries, cte_names} =
|
1228
|
+ Enum.map_reduce(queries, cte_names, fn
|
1229
|
+ {name, opts, %Ecto.Query{} = cte_query}, cte_names ->
|
1230
|
+ cte_names = Map.put(cte_names, name, [])
|
1231
|
+
|
1232
|
+ {planned_query, _params, _key} =
|
1233
|
+ cte_query |> attach_prefix(query) |> plan(:all, adapter, cte_names)
|
1234
|
+
|
941
1235
|
planned_query = planned_query |> ensure_select(true)
|
942
|
- {name, opts, planned_query}
|
1236
|
+ {{name, opts, planned_query}, cte_names}
|
943
1237
|
|
944
|
- {name, opts, other} ->
|
945
|
- {name, opts, other}
|
946
|
- end
|
1238
|
+ {name, opts, other}, cte_names ->
|
1239
|
+ {{name, opts, other}, cte_names}
|
1240
|
+ end)
|
947
1241
|
|
948
|
- put_in(query.with_ctes.queries, queries)
|
1242
|
+ {put_in(query.with_ctes.queries, queries), cte_names}
|
949
1243
|
end
|
950
1244
|
|
951
1245
|
defp find_source_expr(query, 0) do
|
|
@@ -953,7 +1247,7 @@ defmodule Ecto.Query.Planner do
|
953
1247
|
end
|
954
1248
|
|
955
1249
|
defp find_source_expr(query, ix) do
|
956
|
- Enum.find(query.joins, & &1.ix == ix)
|
1250
|
+ Enum.find(query.joins, &(&1.ix == ix))
|
957
1251
|
end
|
958
1252
|
|
959
1253
|
@doc """
|
|
@@ -962,22 +1256,35 @@ defmodule Ecto.Query.Planner do
|
962
1256
|
def ensure_select(%{select: select} = query, _fields) when select != nil do
|
963
1257
|
query
|
964
1258
|
end
|
1259
|
+
|
965
1260
|
def ensure_select(%{select: nil}, []) do
|
966
1261
|
raise ArgumentError, ":returning expects at least one field to be given, got an empty list"
|
967
1262
|
end
|
1263
|
+
|
968
1264
|
def ensure_select(%{select: nil} = query, fields) when is_list(fields) do
|
969
|
- %{query | select: %SelectExpr{expr: {:&, [], [0]}, take: %{0 => {:any, fields}},
|
970
|
- line: __ENV__.line, file: __ENV__.file}}
|
1265
|
+ %{
|
1266
|
+ query
|
1267
|
+ | select: %SelectExpr{
|
1268
|
+ expr: {:&, [], [0]},
|
1269
|
+ take: %{0 => {:any, fields}},
|
1270
|
+ line: __ENV__.line,
|
1271
|
+ file: __ENV__.file
|
1272
|
+ }
|
1273
|
+ }
|
971
1274
|
end
|
1275
|
+
|
972
1276
|
def ensure_select(%{select: nil, from: %{source: {_, nil}}} = query, true) do
|
973
|
- error! query, "queries that do not have a schema need to explicitly pass a :select clause"
|
1277
|
+ error!(query, "queries that do not have a schema need to explicitly pass a :select clause")
|
974
1278
|
end
|
1279
|
+
|
975
1280
|
def ensure_select(%{select: nil, from: %{source: {:fragment, _, _}}} = query, true) do
|
976
|
- error! query, "queries from a fragment need to explicitly pass a :select clause"
|
1281
|
+ error!(query, "queries from a fragment need to explicitly pass a :select clause")
|
977
1282
|
end
|
1283
|
+
|
978
1284
|
def ensure_select(%{select: nil} = query, true) do
|
979
1285
|
%{query | select: %SelectExpr{expr: {:&, [], [0]}, line: __ENV__.line, file: __ENV__.file}}
|
980
1286
|
end
|
1287
|
+
|
981
1288
|
def ensure_select(%{select: nil} = query, false) do
|
982
1289
|
query
|
983
1290
|
end
|
|
@@ -997,7 +1304,7 @@ defmodule Ecto.Query.Planner do
|
997
1304
|
rescue
|
998
1305
|
e ->
|
999
1306
|
# Reraise errors so we ignore the planner inner stacktrace
|
1000
|
- filter_and_reraise e, __STACKTRACE__
|
1307
|
+ filter_and_reraise(e, __STACKTRACE__)
|
1001
1308
|
end
|
1002
1309
|
|
1003
1310
|
defp keep_literals?(:insert_all, _), do: true
|
|
@@ -1007,30 +1314,39 @@ defmodule Ecto.Query.Planner do
|
1007
1314
|
case operation do
|
1008
1315
|
:all ->
|
1009
1316
|
assert_no_update!(query, operation)
|
1317
|
+
|
1010
1318
|
:insert_all ->
|
1011
1319
|
assert_no_update!(query, operation)
|
1320
|
+
|
1012
1321
|
:update_all ->
|
1013
1322
|
assert_update!(query, operation)
|
1014
1323
|
assert_only_filter_expressions!(query, operation)
|
1324
|
+
|
1015
1325
|
:delete_all ->
|
1016
1326
|
assert_no_update!(query, operation)
|
1017
1327
|
assert_only_filter_expressions!(query, operation)
|
1018
1328
|
end
|
1019
1329
|
|
1020
|
- traverse_exprs(query, operation, counter,
|
1021
|
- &validate_and_increment(&1, &2, &3, &4, operation, adapter))
|
1330
|
+ traverse_exprs(
|
1331
|
+ query,
|
1332
|
+ operation,
|
1333
|
+ counter,
|
1334
|
+ &validate_and_increment(&1, &2, &3, &4, operation, adapter)
|
1335
|
+ )
|
1022
1336
|
end
|
1023
1337
|
|
1024
|
- defp validate_and_increment(:from, query, %{source: %Ecto.SubQuery{}}, _counter, kind, _adapter) when kind not in ~w(all insert_all)a do
|
1025
|
- error! query, "`#{kind}` does not allow subqueries in `from`"
|
1338
|
+ defp validate_and_increment(:from, query, %{source: %Ecto.SubQuery{}}, _counter, kind, _adapter)
|
1339
|
+ when kind not in ~w(all insert_all)a do
|
1340
|
+ error!(query, "`#{kind}` does not allow subqueries in `from`")
|
1026
1341
|
end
|
1342
|
+
|
1027
1343
|
defp validate_and_increment(:from, query, %{source: source} = expr, counter, _kind, adapter) do
|
1028
1344
|
{source, acc} = prewalk_source(source, :from, query, expr, counter, adapter)
|
1029
1345
|
{%{expr | source: source}, acc}
|
1030
1346
|
end
|
1031
1347
|
|
1032
1348
|
defp validate_and_increment(kind, query, expr, counter, _operation, adapter)
|
1033
|
- when kind in ~w(select distinct limit offset)a do
|
1349
|
+ when kind in ~w(select distinct limit offset)a do
|
1034
1350
|
if expr do
|
1035
1351
|
prewalk(kind, query, expr, counter, adapter)
|
1036
1352
|
else
|
|
@@ -1040,15 +1356,16 @@ defmodule Ecto.Query.Planner do
|
1040
1356
|
|
1041
1357
|
defp validate_and_increment(kind, query, exprs, counter, _operation, adapter)
|
1042
1358
|
when kind in ~w(where group_by having order_by update)a do
|
1043
|
-
|
1044
1359
|
{exprs, counter} =
|
1045
1360
|
Enum.reduce(exprs, {[], counter}, fn
|
1046
1361
|
%{expr: []}, {list, acc} ->
|
1047
1362
|
{list, acc}
|
1363
|
+
|
1048
1364
|
expr, {list, acc} ->
|
1049
1365
|
{expr, acc} = prewalk(kind, query, expr, acc, adapter)
|
1050
|
- {[expr|list], acc}
|
1366
|
+ {[expr | list], acc}
|
1051
1367
|
end)
|
1368
|
+
|
1052
1369
|
{Enum.reverse(exprs), counter}
|
1053
1370
|
end
|
1054
1371
|
|
|
@@ -1060,7 +1377,7 @@ defmodule Ecto.Query.Planner do
|
1060
1377
|
fun = &validate_and_increment(&1, &2, &3, &4, :all, adapter)
|
1061
1378
|
|
1062
1379
|
{queries, counter} =
|
1063
|
- Enum.reduce with_expr.queries, {[], counter}, fn
|
1380
|
+ Enum.reduce(with_expr.queries, {[], counter}, fn
|
1064
1381
|
{name, opts, %Ecto.Query{} = inner_query}, {queries, counter} ->
|
1065
1382
|
inner_query = put_in(inner_query.aliases[@parent_as], query)
|
1066
1383
|
|
|
@@ -1071,35 +1388,41 @@ defmodule Ecto.Query.Planner do
|
1071
1388
|
|
1072
1389
|
# Now compute the fields as keyword lists so we emit AS in Ecto query.
|
1073
1390
|
%{select: %{expr: expr, take: take, aliases: aliases}} = inner_query
|
1074
|
- {{:map, types}, fields, _from} = collect_fields(expr, [], :none, inner_query, take, true, %{})
|
1391
|
+
|
1392
|
+ {{:map, types}, fields, _from} =
|
1393
|
+ collect_fields(expr, [], :none, inner_query, take, true, %{})
|
1394
|
+
|
1075
1395
|
fields = cte_fields(Keyword.keys(types), Enum.reverse(fields), aliases)
|
1076
1396
|
inner_query = put_in(inner_query.select.fields, fields)
|
1077
1397
|
{_, inner_query} = pop_in(inner_query.aliases[@parent_as])
|
1078
1398
|
|
1079
1399
|
{[{name, opts, inner_query} | queries], counter}
|
1080
1400
|
|
1081
|
- {name, opts, %QueryExpr{expr: {:fragment, _, _} = fragment} = query_expr}, {queries, counter} ->
|
1082
|
- {fragment, counter} = prewalk_source(fragment, :with_cte, query, with_expr, counter, adapter)
|
1401
|
+ {name, opts, %QueryExpr{expr: {:fragment, _, _} = fragment} = query_expr},
|
1402
|
+ {queries, counter} ->
|
1403
|
+ {fragment, counter} =
|
1404
|
+ prewalk_source(fragment, :with_cte, query, with_expr, counter, adapter)
|
1405
|
+
|
1083
1406
|
query_expr = %{query_expr | expr: fragment}
|
1084
1407
|
{[{name, opts, query_expr} | queries], counter}
|
1085
|
- end
|
1408
|
+ end)
|
1086
1409
|
|
1087
1410
|
{%{with_expr | queries: Enum.reverse(queries)}, counter}
|
1088
1411
|
end
|
1089
1412
|
|
1090
1413
|
defp validate_and_increment(:join, query, exprs, counter, _operation, adapter) do
|
1091
|
- Enum.map_reduce exprs, counter, fn join, acc ->
|
1414
|
+ Enum.map_reduce(exprs, counter, fn join, acc ->
|
1092
1415
|
{source, acc} = prewalk_source(join.source, :join, query, join, acc, adapter)
|
1093
1416
|
{on, acc} = prewalk(:join, query, join.on, acc, adapter)
|
1094
1417
|
{%{join | on: on, source: source, params: nil}, acc}
|
1095
|
- end
|
1418
|
+ end)
|
1096
1419
|
end
|
1097
1420
|
|
1098
1421
|
defp validate_and_increment(:windows, query, exprs, counter, _operation, adapter) do
|
1099
1422
|
{exprs, counter} =
|
1100
1423
|
Enum.reduce(exprs, {[], counter}, fn {name, expr}, {list, acc} ->
|
1101
1424
|
{expr, acc} = prewalk(:windows, query, expr, acc, adapter)
|
1102
|
- {[{name, expr}|list], acc}
|
1425
|
+ {[{name, expr} | list], acc}
|
1103
1426
|
end)
|
1104
1427
|
|
1105
1428
|
{Enum.reverse(exprs), counter}
|
|
@@ -1110,21 +1433,22 @@ defmodule Ecto.Query.Planner do
|
1110
1433
|
parent_aliases = query.aliases[@parent_as]
|
1111
1434
|
|
1112
1435
|
{combinations, counter} =
|
1113
|
- Enum.reduce combinations, {[], counter}, fn {type, combination_query}, {combinations, counter} ->
|
1436
|
+ Enum.reduce(combinations, {[], counter}, fn {type, combination_query},
|
1437
|
+ {combinations, counter} ->
|
1114
1438
|
combination_query = put_in(combination_query.aliases[@parent_as], parent_aliases)
|
1115
1439
|
{combination_query, counter} = traverse_exprs(combination_query, operation, counter, fun)
|
1116
1440
|
{combination_query, _} = combination_query |> normalize_select(true)
|
1117
1441
|
{_, combination_query} = pop_in(combination_query.aliases[@parent_as])
|
1118
1442
|
{[{type, combination_query} | combinations], counter}
|
1119
|
- end
|
1443
|
+ end)
|
1120
1444
|
|
1121
1445
|
{Enum.reverse(combinations), counter}
|
1122
1446
|
end
|
1123
1447
|
|
1124
|
- defp validate_json_path!([path_field | rest], field, {:parameterized, Ecto.Embedded, embed}) do
|
1448
|
+ defp validate_json_path!([path_field | rest], field, {:parameterized, {Ecto.Embedded, embed}}) do
|
1125
1449
|
case embed do
|
1126
1450
|
%{related: related, cardinality: :one} ->
|
1127
|
- unless Enum.any?(related.__schema__(:fields), &Atom.to_string(&1) == path_field) do
|
1451
|
+ unless Enum.any?(related.__schema__(:fields), &(Atom.to_string(&1) == path_field)) do
|
1128
1452
|
raise "field `#{path_field}` does not exist in #{inspect(related)}"
|
1129
1453
|
end
|
1130
1454
|
|
|
@@ -1137,7 +1461,7 @@ defmodule Ecto.Query.Planner do
|
1137
1461
|
end
|
1138
1462
|
|
1139
1463
|
updated_embed = %{embed | cardinality: :one}
|
1140
|
- validate_json_path!(rest, path_field, {:parameterized, Ecto.Embedded, updated_embed})
|
1464
|
+ validate_json_path!(rest, path_field, {:parameterized, {Ecto.Embedded, updated_embed}})
|
1141
1465
|
|
1142
1466
|
other ->
|
1143
1467
|
raise "expected field `#{field}` to be of type embed, got: `#{inspect(other)}`"
|
|
@@ -1155,7 +1479,7 @@ defmodule Ecto.Query.Planner do
|
1155
1479
|
{:map, _} ->
|
1156
1480
|
:ok
|
1157
1481
|
|
1158
|
- {:parameterized, type, _} ->
|
1482
|
+ {:parameterized, {type, _}} ->
|
1159
1483
|
validate_json_path!(path, field, type)
|
1160
1484
|
|
1161
1485
|
type ->
|
|
@@ -1171,21 +1495,30 @@ defmodule Ecto.Query.Planner do
|
1171
1495
|
{fragments, acc} = prewalk(fragments, kind, query, expr, acc, adapter)
|
1172
1496
|
{{:fragment, meta, fragments}, acc}
|
1173
1497
|
end
|
1498
|
+
|
1174
1499
|
defp prewalk_source({:values, meta, [types, num_rows]}, _kind, _query, _expr, acc, _adapter) do
|
1175
1500
|
length = num_rows * length(types)
|
1176
1501
|
# Adapters will use the schema types to cast the values
|
1177
1502
|
schema_types = Enum.map(types, fn {field, type} -> {field, Ecto.Type.type(type)} end)
|
1178
1503
|
{{:values, meta, [schema_types, acc, num_rows]}, acc + length}
|
1179
1504
|
end
|
1180
|
- defp prewalk_source(%Ecto.SubQuery{query: inner_query} = subquery, kind, query, _expr, counter, adapter) do
|
1505
|
+
|
1506
|
+ defp prewalk_source(
|
1507
|
+ %Ecto.SubQuery{query: inner_query} = subquery,
|
1508
|
+ kind,
|
1509
|
+ query,
|
1510
|
+ _expr,
|
1511
|
+ counter,
|
1512
|
+ adapter
|
1513
|
+ ) do
|
1181
1514
|
try do
|
1182
1515
|
inner_query = put_in(inner_query.aliases[@parent_as], query)
|
1183
1516
|
{inner_query, counter} = normalize_query(inner_query, :all, adapter, counter)
|
1184
1517
|
{inner_query, _} = normalize_select(inner_query, true)
|
1185
1518
|
{_, inner_query} = pop_in(inner_query.aliases[@parent_as])
|
1186
1519
|
|
1520
|
+ # If the subquery comes from a select, we are not really interested on the fields
|
1187
1521
|
inner_query =
|
1188
|
- # If the subquery comes from a select, we are not really interested on the fields
|
1189
1522
|
if kind == :where do
|
1190
1523
|
inner_query
|
1191
1524
|
else
|
|
@@ -1207,6 +1540,7 @@ defmodule Ecto.Query.Planner do
|
1207
1540
|
e -> raise Ecto.SubQueryError, query: query, exception: e
|
1208
1541
|
end
|
1209
1542
|
end
|
1543
|
+
|
1210
1544
|
defp prewalk_source(source, _kind, _query, _expr, acc, _adapter) do
|
1211
1545
|
{source, acc}
|
1212
1546
|
end
|
|
@@ -1215,17 +1549,19 @@ defmodule Ecto.Query.Planner do
|
1215
1549
|
source = get_source!(:update, query, 0)
|
1216
1550
|
|
1217
1551
|
{inner, acc} =
|
1218
|
- Enum.map_reduce expr.expr, counter, fn {op, kw}, counter ->
|
1552
|
+ Enum.map_reduce(expr.expr, counter, fn {op, kw}, counter ->
|
1219
1553
|
{kw, acc} =
|
1220
|
- Enum.map_reduce kw, counter, fn {field, value}, counter ->
|
1554
|
+ Enum.map_reduce(kw, counter, fn {field, value}, counter ->
|
1221
1555
|
{value, acc} = prewalk(value, :update, query, expr, counter, adapter)
|
1222
1556
|
{{field_source(source, field), value}, acc}
|
1223
|
- end
|
1557
|
+ end)
|
1558
|
+
|
1224
1559
|
{{op, kw}, acc}
|
1225
|
- end
|
1560
|
+ end)
|
1226
1561
|
|
1227
1562
|
{%{expr | expr: inner, params: nil}, acc}
|
1228
1563
|
end
|
1564
|
+
|
1229
1565
|
defp prewalk(kind, query, expr, counter, adapter) do
|
1230
1566
|
{inner, acc} = prewalk(expr.expr, kind, query, expr, counter, adapter)
|
1231
1567
|
{%{expr | expr: inner, params: nil}, acc}
|
|
@@ -1246,14 +1582,21 @@ defmodule Ecto.Query.Planner do
|
1246
1582
|
{right, acc} = prewalk(right, kind, query, expr, acc, adapter)
|
1247
1583
|
|
1248
1584
|
case right.query.select.fields do
|
1249
|
- [_] -> :ok
|
1250
|
- _ -> error!(query, "subquery must return a single field in order to be used on the right-side of `in`")
|
1585
|
+ [_] ->
|
1586
|
+ :ok
|
1587
|
+
|
1588
|
+ _ ->
|
1589
|
+ error!(
|
1590
|
+ query,
|
1591
|
+ "subquery must return a single field in order to be used on the right-side of `in`"
|
1592
|
+ )
|
1251
1593
|
end
|
1252
1594
|
|
1253
1595
|
{{:in, in_meta, [left, right]}, acc}
|
1254
1596
|
end
|
1255
1597
|
|
1256
|
- defp prewalk({quantifier, meta, [{:subquery, _} = subquery]}, kind, query, expr, acc, adapter) when quantifier in [:exists, :any, :all] do
|
1598
|
+ defp prewalk({quantifier, meta, [{:subquery, _} = subquery]}, kind, query, expr, acc, adapter)
|
1599
|
+ when quantifier in [:exists, :any, :all] do
|
1257
1600
|
{subquery, acc} = prewalk(subquery, kind, query, expr, acc, adapter)
|
1258
1601
|
|
1259
1602
|
case {quantifier, subquery.query.select.fields} do
|
|
@@ -1273,13 +1616,19 @@ defmodule Ecto.Query.Planner do
|
1273
1616
|
{{quantifier, meta, [subquery]}, acc}
|
1274
1617
|
end
|
1275
1618
|
|
1276
|
- defp prewalk({:splice, splice_meta, [{:^, meta, [_]}, length]}, _kind, _query, _expr, acc, _adapter) do
|
1619
|
+ defp prewalk(
|
1620
|
+ {:splice, splice_meta, [{:^, meta, [_]}, length]},
|
1621
|
+ _kind,
|
1622
|
+ _query,
|
1623
|
+ _expr,
|
1624
|
+ acc,
|
1625
|
+ _adapter
|
1626
|
+ ) do
|
1277
1627
|
param = {:^, meta, [acc, length]}
|
1278
1628
|
{{:splice, splice_meta, [param]}, acc + length}
|
1279
1629
|
end
|
1280
1630
|
|
1281
|
- defp prewalk({{:., dot_meta, [left, field]}, meta, []},
|
1282
|
- kind, query, expr, acc, _adapter) do
|
1631
|
+ defp prewalk({{:., dot_meta, [left, field]}, meta, []}, kind, query, expr, acc, _adapter) do
|
1283
1632
|
{ix, ix_expr, ix_query} = get_ix!(left, kind, query)
|
1284
1633
|
extra = if kind == :select, do: [type: type!(kind, ix_query, expr, ix, field)], else: []
|
1285
1634
|
field = field_source(get_source!(kind, ix_query, ix), field)
|
|
@@ -1329,16 +1678,21 @@ defmodule Ecto.Query.Planner do
|
1329
1678
|
# So it is best to be consistent and not support query-dumping of
|
1330
1679
|
# non-base types.
|
1331
1680
|
if is_binary(value) and Ecto.Type.type(type) in [:binary_id, :binary, :uuid] do
|
1332
|
- error = "cannot encode value `#{inspect v}` of type `#{inspect(type)}` within a query, please interpolate (using ^) instead"
|
1333
|
- error! query, expr, error
|
1681
|
+ error =
|
1682
|
+ "cannot encode value `#{inspect(v)}` of type `#{inspect(type)}` within a query, please interpolate (using ^) instead"
|
1683
|
+
|
1684
|
+ error!(query, expr, error)
|
1334
1685
|
else
|
1335
1686
|
{value, acc}
|
1336
1687
|
end
|
1337
1688
|
else
|
1338
1689
|
{:error, error} ->
|
1339
|
- error = error <> ". Or the value is incompatible or it must be " <>
|
1340
|
- "interpolated (using ^) so it may be cast accordingly"
|
1341
|
- error! query, expr, error
|
1690
|
+ error =
|
1691
|
+ error <>
|
1692
|
+ ". Or the value is incompatible or it must be " <>
|
1693
|
+ "interpolated (using ^) so it may be cast accordingly"
|
1694
|
+
|
1695
|
+ error!(query, expr, error)
|
1342
1696
|
end
|
1343
1697
|
end
|
1344
1698
|
end
|
|
@@ -1419,7 +1773,10 @@ defmodule Ecto.Query.Planner do
|
1419
1773
|
{fields, preprocess, {from_tag, from_source}}
|
1420
1774
|
|
1421
1775
|
:none when preloads != [] or assocs != [] ->
|
1422
|
- error! query, "the binding used in `from` must be selected in `select` when using `preload`"
|
1776
|
+ error!(
|
1777
|
+ query,
|
1778
|
+ "the binding used in `from` must be selected in `select` when using `preload`"
|
1779
|
+ )
|
1423
1780
|
|
1424
1781
|
:none ->
|
1425
1782
|
{Enum.reverse(fields), [], :none}
|
|
@@ -1444,14 +1801,25 @@ defmodule Ecto.Query.Planner do
|
1444
1801
|
# collected in the `from` information. Then, everything else refers to
|
1445
1802
|
# the preprocessed `from` as `{:source, :from}`.
|
1446
1803
|
|
1447
|
- defp collect_fields({:merge, _, [left, right]}, fields, from, query, take, keep_literals?, _drop) do
|
1804
|
+ defp collect_fields(
|
1805
|
+ {:merge, _, [left, right]},
|
1806
|
+ fields,
|
1807
|
+ from,
|
1808
|
+ query,
|
1809
|
+ take,
|
1810
|
+ keep_literals?,
|
1811
|
+ _drop
|
1812
|
+ ) do
|
1448
1813
|
case collect_fields(left, fields, from, query, take, keep_literals?, %{}) do
|
1449
1814
|
{{:source, :from}, fields, left_from} ->
|
1450
1815
|
{right, right_fields, _} =
|
1451
1816
|
collect_fields(right, [], left_from, query, take, keep_literals?, %{})
|
1452
1817
|
|
1453
1818
|
{from_expr, from_source, from_fields} = left_from
|
1454
|
- from = {{:merge, from_expr, right}, from_source, from_fields ++ Enum.reverse(right_fields)}
|
1819
|
+
|
1820
|
+ from =
|
1821
|
+ {{:merge, from_expr, right}, from_source, from_fields ++ Enum.reverse(right_fields)}
|
1822
|
+
|
1455
1823
|
{{:source, :from}, fields, from}
|
1456
1824
|
|
1457
1825
|
{left, left_fields, left_from} ->
|
|
@@ -1478,8 +1846,15 @@ defmodule Ecto.Query.Planner do
|
1478
1846
|
|
1479
1847
|
# Expression handling
|
1480
1848
|
|
1481
|
- defp collect_fields({agg, _, [{{:., dot_meta, [{:&, _, [_]}, _]}, _, []} | _]} = expr,
|
1482
|
- fields, from, _query, _take, _keep_literals?, _drop)
|
1849
|
+ defp collect_fields(
|
1850
|
+ {agg, _, [{{:., dot_meta, [{:&, _, [_]}, _]}, _, []} | _]} = expr,
|
1851
|
+ fields,
|
1852
|
+ from,
|
1853
|
+ _query,
|
1854
|
+ _take,
|
1855
|
+ _keep_literals?,
|
1856
|
+ _drop
|
1857
|
+ )
|
1483
1858
|
when agg in @aggs do
|
1484
1859
|
type =
|
1485
1860
|
case agg do
|
|
@@ -1491,24 +1866,48 @@ defmodule Ecto.Query.Planner do
|
1491
1866
|
# If it is possible to upcast, we do it, otherwise keep the DB value.
|
1492
1867
|
# For example, an average of integers will return a decimal, which can't be cast
|
1493
1868
|
# as an integer. But an average of "moneys" should be upcast.
|
1494
|
- _ -> {:maybe, Keyword.fetch!(dot_meta, :type)}
|
1869
|
+ _ -> {:try, Keyword.fetch!(dot_meta, :type)}
|
1495
1870
|
end
|
1496
1871
|
|
1497
1872
|
{{:value, type}, [expr | fields], from}
|
1498
1873
|
end
|
1499
1874
|
|
1500
|
- defp collect_fields({:filter, _, [call, _]} = expr, fields, from, query, take, keep_literals?, _drop) do
|
1875
|
+ defp collect_fields(
|
1876
|
+ {:filter, _, [call, _]} = expr,
|
1877
|
+ fields,
|
1878
|
+ from,
|
1879
|
+ query,
|
1880
|
+ take,
|
1881
|
+ keep_literals?,
|
1882
|
+ _drop
|
1883
|
+ ) do
|
1501
1884
|
case call do
|
1502
|
- {agg, _, _} when agg in @aggs -> :ok
|
1503
|
- {:fragment, _, [_ | _]} -> :ok
|
1504
|
- _ -> error!(query, "filter(...) expects the first argument to be an aggregate expression, got: `#{Macro.to_string(expr)}`")
|
1885
|
+ {agg, _, _} when agg in @aggs ->
|
1886
|
+ :ok
|
1887
|
+
|
1888
|
+ {:fragment, _, [_ | _]} ->
|
1889
|
+ :ok
|
1890
|
+
|
1891
|
+ _ ->
|
1892
|
+ error!(
|
1893
|
+ query,
|
1894
|
+ "filter(...) expects the first argument to be an aggregate expression, got: `#{Macro.to_string(expr)}`"
|
1895
|
+ )
|
1505
1896
|
end
|
1506
1897
|
|
1507
1898
|
{type, _, _} = collect_fields(call, fields, from, query, take, keep_literals?, %{})
|
1508
1899
|
{type, [expr | fields], from}
|
1509
1900
|
end
|
1510
1901
|
|
1511
|
- defp collect_fields({:coalesce, _, [left, right]} = expr, fields, from, query, take, _keep_literals?, _drop) do
|
1902
|
+ defp collect_fields(
|
1903
|
+ {:coalesce, _, [left, right]} = expr,
|
1904
|
+ fields,
|
1905
|
+ from,
|
1906
|
+ query,
|
1907
|
+ take,
|
1908
|
+ _keep_literals?,
|
1909
|
+ _drop
|
1910
|
+ ) do
|
1512
1911
|
{left_type, _, _} = collect_fields(left, fields, from, query, take, true, %{})
|
1513
1912
|
{right_type, _, _} = collect_fields(right, fields, from, query, take, true, %{})
|
1514
1913
|
|
|
@@ -1516,22 +1915,39 @@ defmodule Ecto.Query.Planner do
|
1516
1915
|
{type, [expr | fields], from}
|
1517
1916
|
end
|
1518
1917
|
|
1519
|
- defp collect_fields({:over, _, [call, window]} = expr, fields, from, query, take, keep_literals?, _drop) do
|
1918
|
+ defp collect_fields(
|
1919
|
+ {:over, _, [call, window]} = expr,
|
1920
|
+ fields,
|
1921
|
+ from,
|
1922
|
+ query,
|
1923
|
+ take,
|
1924
|
+ keep_literals?,
|
1925
|
+ _drop
|
1926
|
+ ) do
|
1520
1927
|
if is_atom(window) and not Keyword.has_key?(query.windows, window) do
|
1521
|
- error!(query, "unknown window #{inspect window} given to over/2")
|
1928
|
+ error!(query, "unknown window #{inspect(window)} given to over/2")
|
1522
1929
|
end
|
1523
1930
|
|
1524
1931
|
{type, _, _} = collect_fields(call, fields, from, query, take, keep_literals?, %{})
|
1525
1932
|
{type, [expr | fields], from}
|
1526
1933
|
end
|
1527
1934
|
|
1528
|
- defp collect_fields({{:., dot_meta, [{:&, _, [_]}, _]}, _, []} = expr,
|
1529
|
- fields, from, _query, _take, _keep_literals?, _drop) do
|
1935
|
+ defp collect_fields(
|
1936
|
+ {{:., dot_meta, [{:&, _, [_]}, _]}, _, []} = expr,
|
1937
|
+ fields,
|
1938
|
+ from,
|
1939
|
+ _query,
|
1940
|
+ _take,
|
1941
|
+ _keep_literals?,
|
1942
|
+ _drop
|
1943
|
+ ) do
|
1530
1944
|
{{:value, Keyword.fetch!(dot_meta, :type)}, [expr | fields], from}
|
1531
1945
|
end
|
1532
1946
|
|
1533
1947
|
defp collect_fields({left, right}, fields, from, query, take, keep_literals?, _drop) do
|
1534
|
- {args, fields, from} = collect_args([left, right], fields, from, query, take, keep_literals?, [])
|
1948
|
+ {args, fields, from} =
|
1949
|
+ collect_args([left, right], fields, from, query, take, keep_literals?, [])
|
1950
|
+
|
1535
1951
|
{{:tuple, args}, fields, from}
|
1536
1952
|
end
|
1537
1953
|
|
|
@@ -1540,7 +1956,15 @@ defmodule Ecto.Query.Planner do
|
1540
1956
|
{{:tuple, args}, fields, from}
|
1541
1957
|
end
|
1542
1958
|
|
1543
|
- defp collect_fields({:%{}, _, [{:|, _, [data, args]}]}, fields, from, query, take, keep_literals?, _drop) do
|
1959
|
+ defp collect_fields(
|
1960
|
+ {:%{}, _, [{:|, _, [data, args]}]},
|
1961
|
+ fields,
|
1962
|
+ from,
|
1963
|
+ query,
|
1964
|
+ take,
|
1965
|
+ keep_literals?,
|
1966
|
+ _drop
|
1967
|
+ ) do
|
1544
1968
|
drop = Map.new(args, fn {key, _} -> {key, nil} end)
|
1545
1969
|
{data, fields, from} = collect_fields(data, fields, from, query, take, keep_literals?, drop)
|
1546
1970
|
{args, fields, from} = collect_kv(args, fields, from, query, take, keep_literals?, [])
|
|
@@ -1552,8 +1976,15 @@ defmodule Ecto.Query.Planner do
|
1552
1976
|
{{:map, args}, fields, from}
|
1553
1977
|
end
|
1554
1978
|
|
1555
|
- defp collect_fields({:%, _, [name, {:%{}, _, [{:|, _, [data, args]}]}]},
|
1556
|
- fields, from, query, take, keep_literals?, _drop) do
|
1979
|
+ defp collect_fields(
|
1980
|
+ {:%, _, [name, {:%{}, _, [{:|, _, [data, args]}]}]},
|
1981
|
+ fields,
|
1982
|
+ from,
|
1983
|
+ query,
|
1984
|
+ take,
|
1985
|
+ keep_literals?,
|
1986
|
+ _drop
|
1987
|
+ ) do
|
1557
1988
|
drop = Map.new(args, fn {key, _} -> {key, nil} end)
|
1558
1989
|
{data, fields, from} = collect_fields(data, fields, from, query, take, keep_literals?, drop)
|
1559
1990
|
{args, fields, from} = collect_kv(args, fields, from, query, take, keep_literals?, [])
|
|
@@ -1561,27 +1992,52 @@ defmodule Ecto.Query.Planner do
|
1561
1992
|
{{:struct, name, data, args}, fields, from}
|
1562
1993
|
end
|
1563
1994
|
|
1564
|
- defp collect_fields({:%, _, [name, {:%{}, _, args}]}, fields, from, query, take, keep_literals?, _drop) do
|
1995
|
+ defp collect_fields(
|
1996
|
+ {:%, _, [name, {:%{}, _, args}]},
|
1997
|
+ fields,
|
1998
|
+ from,
|
1999
|
+ query,
|
2000
|
+ take,
|
2001
|
+ keep_literals?,
|
2002
|
+ _drop
|
2003
|
+ ) do
|
1565
2004
|
{args, fields, from} = collect_kv(args, fields, from, query, take, keep_literals?, [])
|
1566
2005
|
struct!(name, args)
|
1567
2006
|
{{:struct, name, args}, fields, from}
|
1568
2007
|
end
|
1569
2008
|
|
1570
|
- defp collect_fields({:date_add, _, [arg | _]} = expr, fields, from, query, take, keep_literals?, _drop) do
|
2009
|
+ defp collect_fields(
|
2010
|
+ {:date_add, _, [arg | _]} = expr,
|
2011
|
+ fields,
|
2012
|
+ from,
|
2013
|
+ query,
|
2014
|
+ take,
|
2015
|
+ keep_literals?,
|
2016
|
+ _drop
|
2017
|
+ ) do
|
1571
2018
|
case collect_fields(arg, fields, from, query, take, keep_literals?, %{}) do
|
1572
2019
|
{{:value, :any}, _, _} -> {{:value, :date}, [expr | fields], from}
|
1573
2020
|
{type, _, _} -> {type, [expr | fields], from}
|
1574
2021
|
end
|
1575
2022
|
end
|
1576
2023
|
|
1577
|
- defp collect_fields({:datetime_add, _, [arg | _]} = expr, fields, from, query, take, keep_literals?, _drop) do
|
2024
|
+ defp collect_fields(
|
2025
|
+ {:datetime_add, _, [arg | _]} = expr,
|
2026
|
+ fields,
|
2027
|
+ from,
|
2028
|
+ query,
|
2029
|
+ take,
|
2030
|
+ keep_literals?,
|
2031
|
+ _drop
|
2032
|
+ ) do
|
1578
2033
|
case collect_fields(arg, fields, from, query, take, keep_literals?, %{}) do
|
1579
2034
|
{{:value, :any}, _, _} -> {{:value, :naive_datetime}, [expr | fields], from}
|
1580
2035
|
{type, _, _} -> {type, [expr | fields], from}
|
1581
2036
|
end
|
1582
2037
|
end
|
1583
2038
|
|
1584
|
- defp collect_fields(args, fields, from, query, take, keep_literals?, _drop) when is_list(args) do
|
2039
|
+ defp collect_fields(args, fields, from, query, take, keep_literals?, _drop)
|
2040
|
+ when is_list(args) do
|
1585
2041
|
{args, fields, from} = collect_args(args, fields, from, query, take, keep_literals?, [])
|
1586
2042
|
{{:list, args}, fields, from}
|
1587
2043
|
end
|
|
@@ -1606,7 +2062,8 @@ defmodule Ecto.Query.Planner do
|
1606
2062
|
{{:value, :any}, [nil | fields], from}
|
1607
2063
|
end
|
1608
2064
|
|
1609
|
- defp collect_fields(expr, fields, from, _query, _take, _keep_literals?, _drop) when is_atom(expr) do
|
2065
|
+ defp collect_fields(expr, fields, from, _query, _take, _keep_literals?, _drop)
|
2066
|
+ when is_atom(expr) do
|
1610
2067
|
{expr, fields, from}
|
1611
2068
|
end
|
1612
2069
|
|
|
@@ -1615,7 +2072,15 @@ defmodule Ecto.Query.Planner do
|
1615
2072
|
{expr, fields, from}
|
1616
2073
|
end
|
1617
2074
|
|
1618
|
- defp collect_fields(%Ecto.Query.Tagged{tag: tag} = expr, fields, from, _query, _take, _keep_literals?, _drop) do
|
2075
|
+ defp collect_fields(
|
2076
|
+ %Ecto.Query.Tagged{tag: tag} = expr,
|
2077
|
+ fields,
|
2078
|
+ from,
|
2079
|
+ _query,
|
2080
|
+ _take,
|
2081
|
+ _keep_literals?,
|
2082
|
+ _drop
|
2083
|
+ ) do
|
1619
2084
|
{{:value, tag}, [expr | fields], from}
|
1620
2085
|
end
|
1621
2086
|
|
|
@@ -1629,7 +2094,15 @@ defmodule Ecto.Query.Planner do
|
1629
2094
|
{{:value, :boolean}, [expr | fields], from}
|
1630
2095
|
end
|
1631
2096
|
|
1632
|
- defp collect_fields({:selected_as, _, [select_expr, name]}, fields, from, query, take, keep_literals?, _drop) do
|
2097
|
+ defp collect_fields(
|
2098
|
+ {:selected_as, _, [select_expr, name]},
|
2099
|
+ fields,
|
2100
|
+ from,
|
2101
|
+ query,
|
2102
|
+ take,
|
2103
|
+ keep_literals?,
|
2104
|
+ _drop
|
2105
|
+ ) do
|
1633
2106
|
{type, _, _} = collect_fields(select_expr, fields, from, query, take, keep_literals?, %{})
|
1634
2107
|
{type, [{name, select_expr} | fields], from}
|
1635
2108
|
end
|
|
@@ -1652,6 +2125,7 @@ defmodule Ecto.Query.Planner do
|
1652
2125
|
{elem, fields, from} = collect_fields(elem, fields, from, query, take, keep_literals?, %{})
|
1653
2126
|
collect_args(elems, fields, from, query, take, keep_literals?, [elem | acc])
|
1654
2127
|
end
|
2128
|
+
|
1655
2129
|
defp collect_args([], fields, from, _query, _take, _keep_literals?, acc) do
|
1656
2130
|
{Enum.reverse(acc), fields, from}
|
1657
2131
|
end
|
|
@@ -1664,15 +2138,19 @@ defmodule Ecto.Query.Planner do
|
1664
2138
|
Map.update(acc, field, {index, children}, fn
|
1665
2139
|
{^index, current_children} ->
|
1666
2140
|
{index, merge_assocs(children ++ current_children, query)}
|
2141
|
+
|
1667
2142
|
{other_index, _} ->
|
1668
|
- error! query, "association `#{field}` is being set to binding at position #{index} " <>
|
1669
|
- "and at position #{other_index} at the same time"
|
2143
|
+ error!(
|
2144
|
+ query,
|
2145
|
+ "association `#{field}` is being set to binding at position #{index} " <>
|
2146
|
+ "and at position #{other_index} at the same time"
|
2147
|
+ )
|
1670
2148
|
end)
|
1671
2149
|
end)
|
1672
2150
|
|> Map.to_list()
|
1673
2151
|
end
|
1674
2152
|
|
1675
|
- defp collect_assocs(exprs, fields, query, tag, take, [{assoc, {ix, children}}|tail]) do
|
2153
|
+ defp collect_assocs(exprs, fields, query, tag, take, [{assoc, {ix, children}} | tail]) do
|
1676
2154
|
to_take = get_preload_source!(query, ix)
|
1677
2155
|
{fetch, take_children} = fetch_assoc(tag, take, assoc)
|
1678
2156
|
{expr, taken} = take!(to_take, query, fetch, assoc, ix, %{})
|
|
@@ -1682,6 +2160,7 @@ defmodule Ecto.Query.Planner do
|
1682
2160
|
{exprs, fields} = collect_assocs(exprs, fields, query, tag, take, tail)
|
1683
2161
|
{exprs, fields}
|
1684
2162
|
end
|
2163
|
+
|
1685
2164
|
defp collect_assocs(exprs, fields, _query, _tag, _take, []) do
|
1686
2165
|
{exprs, fields}
|
1687
2166
|
end
|
|
@@ -1701,16 +2180,19 @@ defmodule Ecto.Query.Planner do
|
1701
2180
|
defp take!(source, query, fetched, field, ix, drop) do
|
1702
2181
|
case {fetched, source} do
|
1703
2182
|
{{:ok, {:struct, _}}, {:fragment, _, _}} ->
|
1704
|
- error! query, "it is not possible to return a struct subset of a fragment"
|
2183
|
+ error!(query, "it is not possible to return a struct subset of a fragment")
|
1705
2184
|
|
1706
2185
|
{{:ok, {:struct, _}}, %Ecto.SubQuery{}} ->
|
1707
|
- error! query, "it is not possible to return a struct subset of a subquery"
|
2186
|
+ error!(query, "it is not possible to return a struct subset of a subquery")
|
1708
2187
|
|
1709
2188
|
{{:ok, {_, []}}, {_, _, _}} ->
|
1710
|
- error! query, "at least one field must be selected for binding `#{field}`, got an empty list"
|
2189
|
+ error!(
|
2190
|
+ query,
|
2191
|
+ "at least one field must be selected for binding `#{field}`, got an empty list"
|
2192
|
+ )
|
1711
2193
|
|
1712
2194
|
{{:ok, {:struct, _}}, {_, nil, _}} ->
|
1713
|
- error! query, "struct/2 in select expects a source with a schema"
|
2195
|
+ error!(query, "struct/2 in select expects a source with a schema")
|
1714
2196
|
|
1715
2197
|
{{:ok, {kind, fields}}, {source, schema, prefix}} when is_binary(source) ->
|
1716
2198
|
dumper = if schema, do: schema.__schema__(:dump), else: %{}
|
|
@@ -1719,14 +2201,19 @@ defmodule Ecto.Query.Planner do
|
1719
2201
|
{{:source, {source, schema}, prefix || query.prefix, types}, fields}
|
1720
2202
|
|
1721
2203
|
{{:ok, {_, fields}}, _} ->
|
1722
|
- {{:map, Enum.map(fields, &{&1, {:value, :any}})}, Enum.map(fields, &select_field(&1, ix))}
|
2204
|
+ {{:map, Enum.map(fields, &{&1, {:value, :any}})}, Enum.map(fields, &select_field(&1, ix, :always))}
|
1723
2205
|
|
1724
2206
|
{:error, {:fragment, _, _}} ->
|
1725
2207
|
{{:value, :map}, [{:&, [], [ix]}]}
|
1726
2208
|
|
1727
2209
|
{:error, {:values, _, [types, _]}} ->
|
1728
2210
|
fields = Keyword.keys(types)
|
1729
|
- dumper = types |> Enum.map(fn {field, type} -> {field, {field, type}} end) |> Enum.into(%{})
|
2211
|
+
|
2212
|
+ dumper =
|
2213
|
+ types
|
2214
|
+ |> Enum.map(fn {field, type} -> {field, {field, type, :always}} end)
|
2215
|
+ |> Enum.into(%{})
|
2216
|
+
|
1730
2217
|
{types, fields} = select_dump(fields, dumper, ix, drop)
|
1731
2218
|
{{:source, :values, nil, types}, fields}
|
1732
2219
|
|
|
@@ -1734,30 +2221,32 @@ defmodule Ecto.Query.Planner do
|
1734
2221
|
{{:value, :map}, [{:&, [], [ix]}]}
|
1735
2222
|
|
1736
2223
|
{:error, {source, schema, prefix}} ->
|
1737
|
- {types, fields} = select_dump(schema.__schema__(:query_fields), schema.__schema__(:dump), ix, drop)
|
2224
|
+ {types, fields} =
|
2225
|
+ select_dump(schema.__schema__(:query_fields), schema.__schema__(:dump), ix, drop)
|
1738
2226
|
|
1739
2227
|
{{:source, {source, schema}, prefix || query.prefix, types}, fields}
|
1740
2228
|
|
1741
2229
|
{:error, %Ecto.SubQuery{select: select}} ->
|
1742
2230
|
fields = subquery_source_fields(select)
|
1743
|
- {select, Enum.map(fields, &select_field(&1, ix))}
|
2231
|
+ {select, Enum.map(fields, &select_field(&1, ix, :always))}
|
1744
2232
|
end
|
1745
2233
|
end
|
1746
2234
|
|
1747
2235
|
defp select_dump(fields, dumper, ix, drop) do
|
1748
2236
|
fields
|
1749
|
- |> Enum.reverse
|
2237
|
+ |> Enum.reverse()
|
1750
2238
|
|> Enum.reduce({[], []}, fn
|
1751
2239
|
field, {types, exprs} when is_atom(field) and not is_map_key(drop, field) ->
|
1752
|
- {source, type} = Map.get(dumper, field, {field, :any})
|
1753
|
- {[{field, type} | types], [select_field(source, ix) | exprs]}
|
2240
|
+ {source, type, writable} = Map.get(dumper, field, {field, :any, :always})
|
2241
|
+ {[{field, type} | types], [select_field(source, ix, writable) | exprs]}
|
2242
|
+
|
1754
2243
|
_field, acc ->
|
1755
2244
|
acc
|
1756
2245
|
end)
|
1757
2246
|
end
|
1758
2247
|
|
1759
|
- defp select_field(field, ix) do
|
1760
|
- {{:., [], [{:&, [], [ix]}, field]}, [], []}
|
2248
|
+ defp select_field(field, ix, writable) do
|
2249
|
+ {{:., [writable: writable], [{:&, [], [ix]}, field]}, [], []}
|
1761
2250
|
end
|
1762
2251
|
|
1763
2252
|
defp get_ix!({:&, _, [ix]} = expr, _kind, query) do
|
|
@@ -1775,7 +2264,10 @@ defmodule Ecto.Query.Planner do
|
1775
2264
|
case query.aliases[@parent_as] do
|
1776
2265
|
%{aliases: %{^as => ix}, sources: sources} = query ->
|
1777
2266
|
if kind == :select and not (ix < tuple_size(sources)) do
|
1778
|
- error!(query, "the parent_as in a subquery select used as a join can only access the `from` binding")
|
2267
|
+ error!(
|
2268
|
+ query,
|
2269
|
+ "the parent_as in a subquery select used as a join can only access the `from` binding"
|
2270
|
+ )
|
1779
2271
|
else
|
1780
2272
|
{ix, {:parent_as, [], [as]}, query}
|
1781
2273
|
end
|
|
@@ -1792,17 +2284,28 @@ defmodule Ecto.Query.Planner do
|
1792
2284
|
elem(sources, ix)
|
1793
2285
|
rescue
|
1794
2286
|
ArgumentError ->
|
1795
|
- error! query, "invalid query has specified more bindings than bindings available " <>
|
1796
|
- "in `#{where}` (look for `unknown_binding!` in the printed query below)"
|
2287
|
+ error!(
|
2288
|
+ query,
|
2289
|
+ "invalid query has specified more bindings than bindings available " <>
|
2290
|
+ "in `#{where}` (look for `unknown_binding!` in the printed query below)"
|
2291
|
+ )
|
1797
2292
|
end
|
1798
2293
|
|
1799
2294
|
defp get_preload_source!(query, ix) do
|
1800
2295
|
case get_source!(:preload, query, ix) do
|
1801
2296
|
{source, schema, _} = all when is_binary(source) and schema != nil ->
|
1802
2297
|
all
|
2298
|
+
|
2299
|
+ %Ecto.SubQuery{select: {:source, {source, schema}, _, _}} = subquery
|
2300
|
+ when is_binary(source) and schema != nil ->
|
2301
|
+ subquery
|
2302
|
+
|
1803
2303
|
_ ->
|
1804
|
- error! query, "can only preload sources with a schema " <>
|
1805
|
- "(fragments, binary and subqueries are not supported)"
|
2304
|
+ error!(
|
2305
|
+ query,
|
2306
|
+ "can only preload sources with a schema" <>
|
2307
|
+ "(fragments, binaries and subqueries that do not select a schema are not supported)"
|
2308
|
+ )
|
1806
2309
|
end
|
1807
2310
|
end
|
1808
2311
|
|
|
@@ -1811,8 +2314,15 @@ defmodule Ecto.Query.Planner do
|
1811
2314
|
"""
|
1812
2315
|
def attach_prefix(%{prefix: nil} = query, opts) when is_list(opts) do
|
1813
2316
|
case Keyword.fetch(opts, :prefix) do
|
1814
|
- {:ok, prefix} -> %{query | prefix: prefix}
|
1815
|
- :error -> query
|
2317
|
+ {:ok, prefix} when is_binary(prefix) or is_nil(prefix) ->
|
2318
|
+ %{query | prefix: prefix}
|
2319
|
+
|
2320
|
+ {:ok, prefix} when is_atom(prefix) ->
|
2321
|
+ IO.warn("atom prefixes are deprecated. Please use a string instead.")
|
2322
|
+ %{query | prefix: prefix}
|
2323
|
+
|
2324
|
+ :error ->
|
2325
|
+ query
|
1816
2326
|
end
|
1817
2327
|
end
|
1818
2328
|
|
|
@@ -1824,9 +2334,21 @@ defmodule Ecto.Query.Planner do
|
1824
2334
|
|
1825
2335
|
## Helpers
|
1826
2336
|
|
1827
|
- @all_exprs [with_cte: :with_ctes, distinct: :distinct, select: :select, from: :from, join: :joins,
|
1828
|
- where: :wheres, group_by: :group_bys, having: :havings, windows: :windows,
|
1829
|
- combination: :combinations, order_by: :order_bys, limit: :limit, offset: :offset]
|
2337
|
+ @all_exprs [
|
2338
|
+ with_cte: :with_ctes,
|
2339
|
+ distinct: :distinct,
|
2340
|
+ select: :select,
|
2341
|
+ from: :from,
|
2342
|
+ join: :joins,
|
2343
|
+ where: :wheres,
|
2344
|
+ group_by: :group_bys,
|
2345
|
+ having: :havings,
|
2346
|
+ windows: :windows,
|
2347
|
+ combination: :combinations,
|
2348
|
+ order_by: :order_bys,
|
2349
|
+ limit: :limit,
|
2350
|
+ offset: :offset
|
2351
|
+ ]
|
1830
2352
|
|
1831
2353
|
# Although joins come before updates in the actual query,
|
1832
2354
|
# the on fields are moved to where, so they effectively
|
|
@@ -1834,11 +2356,22 @@ defmodule Ecto.Query.Planner do
|
1834
2356
|
# with parameters are not supported as a join on MySQL.
|
1835
2357
|
# The only way to address it is by splitting how join
|
1836
2358
|
# and their on expressions are processed.
|
1837
|
- @update_all_exprs [with_cte: :with_ctes, from: :from, update: :updates,
|
1838
|
- join: :joins, where: :wheres, select: :select]
|
2359
|
+ @update_all_exprs [
|
2360
|
+ with_cte: :with_ctes,
|
2361
|
+ from: :from,
|
2362
|
+ update: :updates,
|
2363
|
+ join: :joins,
|
2364
|
+ where: :wheres,
|
2365
|
+ select: :select
|
2366
|
+ ]
|
1839
2367
|
|
1840
|
- @delete_all_exprs [with_cte: :with_ctes, from: :from, join: :joins,
|
1841
|
- where: :wheres, select: :select]
|
2368
|
+ @delete_all_exprs [
|
2369
|
+ with_cte: :with_ctes,
|
2370
|
+ from: :from,
|
2371
|
+ join: :joins,
|
2372
|
+ where: :wheres,
|
2373
|
+ select: :select
|
2374
|
+ ]
|
1842
2375
|
|
1843
2376
|
# Traverse all query components with expressions.
|
1844
2377
|
# Therefore from, preload, assocs and lock are not traversed.
|
|
@@ -1851,19 +2384,26 @@ defmodule Ecto.Query.Planner do
|
1851
2384
|
:delete_all -> @delete_all_exprs
|
1852
2385
|
end
|
1853
2386
|
|
1854
|
- Enum.reduce exprs, {query, acc}, fn {kind, key}, {query, acc} ->
|
2387
|
+ Enum.reduce(exprs, {query, acc}, fn {kind, key}, {query, acc} ->
|
1855
2388
|
{traversed, acc} = fun.(kind, query, Map.fetch!(query, key), acc)
|
1856
2389
|
{%{query | key => traversed}, acc}
|
1857
|
- end
|
2390
|
+ end)
|
1858
2391
|
end
|
1859
2392
|
|
1860
2393
|
defp field_type!(kind, query, expr, type, allow_virtuals? \\ false)
|
1861
2394
|
|
1862
|
- defp field_type!(kind, query, expr, {composite, {ix, field}}, allow_virtuals?) when is_integer(ix) do
|
2395
|
+ defp field_type!(kind, query, expr, {composite, {ix, field}}, allow_virtuals?)
|
2396
|
+ when is_integer(ix) do
|
1863
2397
|
{composite, type!(kind, query, expr, ix, field, allow_virtuals?)}
|
1864
2398
|
end
|
1865
2399
|
|
1866
|
- defp field_type!(kind, query, expr, {composite, {{bind_kind, _, [_]} = bind_expr, field}}, allow_virtuals?)
|
2400
|
+ defp field_type!(
|
2401
|
+ kind,
|
2402
|
+ query,
|
2403
|
+ expr,
|
2404
|
+ {composite, {{bind_kind, _, [_]} = bind_expr, field}},
|
2405
|
+ allow_virtuals?
|
2406
|
+ )
|
1867
2407
|
when bind_kind in [:as, :parent_as] do
|
1868
2408
|
{ix, _, ix_query} = get_ix!(bind_expr, kind, query)
|
1869
2409
|
{composite, type!(kind, ix_query, expr, ix, field, allow_virtuals?)}
|
|
@@ -1898,7 +2438,7 @@ defmodule Ecto.Query.Planner do
|
1898
2438
|
type
|
1899
2439
|
|
1900
2440
|
:error ->
|
1901
|
- error! query, expr, "field `#{field}` in `#{kind}` does not exist in values list"
|
2441
|
+ error!(query, expr, "field `#{field}` in `#{kind}` does not exist in values list")
|
1902
2442
|
end
|
1903
2443
|
|
1904
2444
|
{_, schema, _} ->
|
|
@@ -1923,18 +2463,37 @@ defmodule Ecto.Query.Planner do
|
1923
2463
|
Map.has_key?(schema.__struct__(), field) ->
|
1924
2464
|
case schema.__schema__(:association, field) do
|
1925
2465
|
%Ecto.Association.BelongsTo{owner_key: owner_key} ->
|
1926
|
- error! query, expr, "field `#{field}` in `#{kind}` is an association in schema #{inspect schema}. " <>
|
1927
|
- "Did you mean to use `#{owner_key}`?"
|
2466
|
+ error!(
|
2467
|
+ query,
|
2468
|
+ expr,
|
2469
|
+ "field `#{field}` in `#{kind}` is an association in schema #{inspect(schema)}. " <>
|
2470
|
+ "Did you mean to use `#{owner_key}`?"
|
2471
|
+ )
|
2472
|
+
|
1928
2473
|
%_{} ->
|
1929
|
- error! query, expr, "field `#{field}` in `#{kind}` is an association in schema #{inspect schema}"
|
2474
|
+ error!(
|
2475
|
+ query,
|
2476
|
+ expr,
|
2477
|
+ "field `#{field}` in `#{kind}` is an association in schema #{inspect(schema)}"
|
2478
|
+ )
|
1930
2479
|
|
1931
2480
|
_ ->
|
1932
|
- error! query, expr, "field `#{field}` in `#{kind}` is a virtual field in schema #{inspect schema}"
|
2481
|
+ error!(
|
2482
|
+ query,
|
2483
|
+ expr,
|
2484
|
+ "field `#{field}` in `#{kind}` is a virtual field in schema #{inspect(schema)}"
|
2485
|
+ )
|
1933
2486
|
end
|
1934
2487
|
|
1935
2488
|
true ->
|
1936
2489
|
hint = closest_fields_hint(field, schema)
|
1937
|
- error! query, expr, "field `#{field}` in `#{kind}` does not exist in schema #{inspect schema}", hint
|
2490
|
+
|
2491
|
+ error!(
|
2492
|
+ query,
|
2493
|
+ expr,
|
2494
|
+ "field `#{field}` in `#{kind}` does not exist in schema #{inspect(schema)}",
|
2495
|
+ hint
|
2496
|
+ )
|
1938
2497
|
end
|
1939
2498
|
end
|
1940
2499
|
|
|
@@ -1944,7 +2503,7 @@ defmodule Ecto.Query.Planner do
|
1944
2503
|
schema.__schema__(:fields)
|
1945
2504
|
|> Enum.map(fn field -> {field, String.jaro_distance(input_string, Atom.to_string(field))} end)
|
1946
2505
|
|> Enum.filter(fn {_field, score} -> score >= 0.77 end)
|
1947
|
- |> Enum.sort(& elem(&1, 0) >= elem(&2, 0))
|
2506
|
+ |> Enum.sort(&(elem(&1, 0) >= elem(&2, 0)))
|
1948
2507
|
|> Enum.take(5)
|
1949
2508
|
|> Enum.map(&elem(&1, 0))
|
1950
2509
|
|> case do
|
|
@@ -1964,13 +2523,17 @@ defmodule Ecto.Query.Planner do
|
1964
2523
|
defp normalize_param(_kind, {:out, {:array, type}}, _value) do
|
1965
2524
|
{:ok, type}
|
1966
2525
|
end
|
2526
|
+
|
1967
2527
|
defp normalize_param(_kind, {:out, :any}, _value) do
|
1968
2528
|
{:ok, :any}
|
1969
2529
|
end
|
2530
|
+
|
1970
2531
|
defp normalize_param(kind, {:out, other}, value) do
|
1971
|
- {:error, "value `#{inspect value}` in `#{kind}` expected to be part of an array " <>
|
1972
|
- "but matched type is #{inspect other}"}
|
2532
|
+ {:error,
|
2533
|
+ "value `#{inspect(value)}` in `#{kind}` expected to be part of an array " <>
|
2534
|
+ "but matched type is #{inspect(other)}"}
|
1973
2535
|
end
|
2536
|
+
|
1974
2537
|
defp normalize_param(_kind, type, _value) do
|
1975
2538
|
{:ok, type}
|
1976
2539
|
end
|
|
@@ -1979,8 +2542,18 @@ defmodule Ecto.Query.Planner do
|
1979
2542
|
case Ecto.Type.cast(type, v) do
|
1980
2543
|
{:ok, v} ->
|
1981
2544
|
{:ok, v}
|
1982
|
- _ ->
|
1983
|
- {:error, "value `#{inspect v}` in `#{kind}` cannot be cast to type #{Ecto.Type.format(type)}"}
|
2545
|
+
|
2546
|
+ :error ->
|
2547
|
+ {:error,
|
2548
|
+ "value `#{inspect(v)}` in `#{kind}` cannot be cast to type #{Ecto.Type.format(type)}"}
|
2549
|
+
|
2550
|
+ {:error, _meta} ->
|
2551
|
+ {:error,
|
2552
|
+ "value `#{inspect(v)}` in `#{kind}` cannot be cast to type #{Ecto.Type.format(type)}"}
|
2553
|
+
|
2554
|
+ other ->
|
2555
|
+ raise "expected #{inspect(type)}.cast/1 to return {:ok, v}, :error, or {:error, meta}" <>
|
2556
|
+ ", got: #{inspect(other)}"
|
1984
2557
|
end
|
1985
2558
|
end
|
1986
2559
|
|
|
@@ -1988,8 +2561,9 @@ defmodule Ecto.Query.Planner do
|
1988
2561
|
case Ecto.Type.adapter_dump(adapter, type, v) do
|
1989
2562
|
{:ok, v} ->
|
1990
2563
|
{:ok, v}
|
2564
|
+
|
1991
2565
|
:error ->
|
1992
|
- {:error, "value `#{inspect v}` cannot be dumped to type #{Ecto.Type.format(type)}"}
|
2566
|
+ {:error, "value `#{inspect(v)}` cannot be dumped to type #{Ecto.Type.format(type)}"}
|
1993
2567
|
end
|
1994
2568
|
end
|
1995
2569
|
|
|
@@ -1998,6 +2572,7 @@ defmodule Ecto.Query.Planner do
|
1998
2572
|
# which will be checked and raise later.
|
1999
2573
|
schema.__schema__(:field_source, field) || field
|
2000
2574
|
end
|
2575
|
+
|
2001
2576
|
defp field_source(_, field) do
|
2002
2577
|
field
|
2003
2578
|
end
|
|
@@ -2027,46 +2602,84 @@ defmodule Ecto.Query.Planner do
|
2027
2602
|
defp cte_fields([], [], _aliases), do: []
|
2028
2603
|
|
2029
2604
|
defp assert_update!(%Ecto.Query{updates: updates} = query, operation) do
|
2605
|
+ dumper = dumper_for_update(query)
|
2606
|
+
|
2030
2607
|
changes =
|
2031
2608
|
Enum.reduce(updates, %{}, fn update, acc ->
|
2032
2609
|
Enum.reduce(update.expr, acc, fn {_op, kw}, acc ->
|
2033
2610
|
Enum.reduce(kw, acc, fn {k, v}, acc ->
|
2034
2611
|
if Map.has_key?(acc, k) do
|
2035
|
- error! query, "duplicate field `#{k}` for `#{operation}`"
|
2036
|
- else
|
2037
|
- Map.put(acc, k, v)
|
2612
|
+ error!(query, "duplicate field `#{k}` for `#{operation}`")
|
2038
2613
|
end
|
2614
|
+
|
2615
|
+ case dumper do
|
2616
|
+ %{^k => {_, _, :always}} -> :ok
|
2617
|
+ %{} -> error!(query, "cannot update non-updatable field `#{inspect(k)}`")
|
2618
|
+ nil -> :ok
|
2619
|
+ end
|
2620
|
+
|
2621
|
+ Map.put(acc, k, v)
|
2039
2622
|
end)
|
2040
2623
|
end)
|
2041
2624
|
end)
|
2042
2625
|
|
2043
2626
|
if changes == %{} do
|
2044
|
- error! query, "`#{operation}` requires at least one field to be updated"
|
2627
|
+ error!(query, "`#{operation}` requires at least one field to be updated")
|
2045
2628
|
end
|
2046
2629
|
end
|
2047
2630
|
|
2048
2631
|
defp assert_no_update!(query, operation) do
|
2049
2632
|
case query do
|
2050
|
- %Ecto.Query{updates: []} -> query
|
2633
|
+ %Ecto.Query{updates: []} ->
|
2634
|
+ query
|
2635
|
+
|
2051
2636
|
_ ->
|
2052
|
- error! query, "`#{operation}` does not allow `update` expressions"
|
2637
|
+ error!(query, "`#{operation}` does not allow `update` expressions")
|
2053
2638
|
end
|
2054
2639
|
end
|
2055
2640
|
|
2056
2641
|
defp assert_only_filter_expressions!(query, operation) do
|
2057
2642
|
case query do
|
2058
|
- %Ecto.Query{order_bys: [], limit: nil, offset: nil, group_bys: [],
|
2059
|
- havings: [], preloads: [], assocs: [], distinct: nil, lock: nil,
|
2060
|
- windows: [], combinations: []} ->
|
2643
|
+ %Ecto.Query{
|
2644
|
+ order_bys: [],
|
2645
|
+ limit: nil,
|
2646
|
+ offset: nil,
|
2647
|
+ group_bys: [],
|
2648
|
+ havings: [],
|
2649
|
+ preloads: [],
|
2650
|
+ assocs: [],
|
2651
|
+ distinct: nil,
|
2652
|
+ lock: nil,
|
2653
|
+ windows: [],
|
2654
|
+ combinations: []
|
2655
|
+ } ->
|
2061
2656
|
query
|
2657
|
+
|
2062
2658
|
_ when operation == :delete_all ->
|
2063
|
- error! query, "`#{operation}` allows only `with_cte`, `where`, `select`, and `join` expressions. " <>
|
2064
|
- "You can exclude unwanted expressions from a query by using " <>
|
2065
|
- "Ecto.Query.exclude/2. Error found"
|
2659
|
+ error!(
|
2660
|
+ query,
|
2661
|
+ "`#{operation}` allows only `with_cte`, `where`, `select`, and `join` expressions. " <>
|
2662
|
+ "You can exclude unwanted expressions from a query by using " <>
|
2663
|
+ "Ecto.Query.exclude/2. Error found"
|
2664
|
+ )
|
2665
|
+
|
2066
2666
|
_ ->
|
2067
|
- error! query, "`#{operation}` allows only `with_cte`, `where` and `join` expressions. " <>
|
2068
|
- "You can exclude unwanted expressions from a query by using " <>
|
2069
|
- "Ecto.Query.exclude/2. Error found"
|
2667
|
+ error!(
|
2668
|
+ query,
|
2669
|
+ "`#{operation}` allows only `with_cte`, `where` and `join` expressions. " <>
|
2670
|
+ "You can exclude unwanted expressions from a query by using " <>
|
2671
|
+ "Ecto.Query.exclude/2. Error found"
|
2672
|
+ )
|
2673
|
+ end
|
2674
|
+ end
|
2675
|
+
|
2676
|
+ defp dumper_for_update(query) do
|
2677
|
+ case get_source!(:updates, query, 0) do
|
2678
|
+ {source, schema, _} when is_binary(source) and schema != nil ->
|
2679
|
+ schema.__schema__(:dump)
|
2680
|
+
|
2681
|
+ _ ->
|
2682
|
+ nil
|
2070
2683
|
end
|
2071
2684
|
end
|
2072
2685
|
|
|
@@ -2083,6 +2696,11 @@ defmodule Ecto.Query.Planner do
|
2083
2696
|
end
|
2084
2697
|
|
2085
2698
|
defp error!(query, expr, message, hint) do
|
2086
|
- raise Ecto.QueryError, message: message, query: query, file: expr.file, line: expr.line, hint: hint
|
2699
|
+ raise Ecto.QueryError,
|
2700
|
+ message: message,
|
2701
|
+ query: query,
|
2702
|
+ file: expr.file,
|
2703
|
+ line: expr.line,
|
2704
|
+ hint: hint
|
2087
2705
|
end
|
2088
2706
|
end
|
changed
lib/ecto/queryable.ex
|
@@ -1,6 +1,102 @@
|
1
1
|
defprotocol Ecto.Queryable do
|
2
2
|
@moduledoc """
|
3
3
|
Converts a data structure into an `Ecto.Query`.
|
4
|
+
|
5
|
+ This is used by `Ecto.Repo` and also `from` macro. For example, `Repo.all`
|
6
|
+ expects any queryable as argument, which is why you can do `Repo.all(MySchema)`
|
7
|
+ or `Repo.all(query)`. Furthermore, when you write `from ALIAS in QUERYABLE`,
|
8
|
+ `QUERYABLE` accepts any data structure that implements `Ecto.Queryable`.
|
9
|
+
|
10
|
+ This module defines a few default implementations so let us go over each and
|
11
|
+ how to use them.
|
12
|
+
|
13
|
+ ## Atom
|
14
|
+
|
15
|
+ The most common use case for this protocol is to convert atoms representing
|
16
|
+ an `Ecto.Schema` module into a query. This is what happens when you write:
|
17
|
+
|
18
|
+ query = from(p in Person)
|
19
|
+
|
20
|
+ Or when you directly pass a schema to a repository:
|
21
|
+
|
22
|
+ Repo.all(Person)
|
23
|
+
|
24
|
+ In case you did not know, Elixir modules are just atoms. This implementation
|
25
|
+ takes the provided module name and then tries to load the associated schema.
|
26
|
+ If no schema exists, it will raise `Protocol.UndefinedError`.
|
27
|
+
|
28
|
+ ## BitString
|
29
|
+
|
30
|
+ This implementation allows you to directly specify a table that you would like
|
31
|
+ to query from:
|
32
|
+
|
33
|
+ from(
|
34
|
+ p in "people",
|
35
|
+ select: {p.first_name, p.last_name}
|
36
|
+ )
|
37
|
+
|
38
|
+ Or:
|
39
|
+
|
40
|
+ Repo.delete_all("people")
|
41
|
+
|
42
|
+ While this is quite simple to use, some repository operations, such as
|
43
|
+ `Repo.all`, require a `select` clause. When you query a schema, the
|
44
|
+ select is automatically defined for you based on the schema fields,
|
45
|
+ but when you pass a table directly, you need to explicitly list them.
|
46
|
+ This limitation now brings us to our next implementation!
|
47
|
+
|
48
|
+ ## Tuple
|
49
|
+
|
50
|
+ Similar to the `BitString` implementation, this allows you to specify the
|
51
|
+ underlying table that you would like to query; however, this additionally
|
52
|
+ allows you to specify the schema you would like to use:
|
53
|
+
|
54
|
+ from(p in {"filtered_people", Person})
|
55
|
+
|
56
|
+ This can be particularly useful if you have database views that filter or
|
57
|
+ aggregate the underlying data of a table but share the same schema. This means
|
58
|
+ that you can reuse the same schema while specifying a separate "source" for
|
59
|
+ the data.
|
60
|
+
|
61
|
+ ## Ecto.Query
|
62
|
+
|
63
|
+ This is a simple pass through. After all, all `Ecto.Query` instances
|
64
|
+ can be converted into `Ecto.Query`:
|
65
|
+
|
66
|
+ Repo.all(from u in User, where: u.active)
|
67
|
+
|
68
|
+ This also enables Ecto queries to compose, since we can pass one query
|
69
|
+ as the source of another:
|
70
|
+
|
71
|
+ active_users = from u in User, where: u.active
|
72
|
+ ordered_active_users = from u in active_users, order_by: u.created_at
|
73
|
+
|
74
|
+ ## Ecto.SubQuery
|
75
|
+
|
76
|
+ Ecto also allows you to compose queries using subqueries. Imagine you
|
77
|
+ have a table of "people". Now imagine that you want to do something with
|
78
|
+ people with the most common last names. To get that list, you could write
|
79
|
+ something like:
|
80
|
+
|
81
|
+ sub = from(
|
82
|
+ p in Person,
|
83
|
+ group_by: p.last_name,
|
84
|
+ having: count(p.last_name) > 1,
|
85
|
+ select: %{last_name: p.last_name, count: count(p.last_name)}
|
86
|
+ )
|
87
|
+
|
88
|
+ Now if you want to do something else with this data, perhaps join on
|
89
|
+ additional tables and perform some calculations, you can do that as so:
|
90
|
+
|
91
|
+ from(
|
92
|
+ p in subquery(sub),
|
93
|
+ # other filtering etc here
|
94
|
+ )
|
95
|
+
|
96
|
+ Please note that the `Ecto.Query.subquery/2` is needed here to convert the
|
97
|
+ `Ecto.Query` into an instance of `Ecto.SubQuery`. This protocol then wraps
|
98
|
+ it into an `Ecto.Query`, but using the provided subquery in the FROM clause.
|
99
|
+ Please see `Ecto.Query.subquery/2` for more information.
|
4
100
|
"""
|
5
101
|
|
6
102
|
@doc """
|
|
@@ -39,6 +135,9 @@ defimpl Ecto.Queryable, for: Atom do
|
39
135
|
end
|
40
136
|
|
41
137
|
raise Protocol.UndefinedError, protocol: @protocol, value: module, description: message
|
138
|
+
|
139
|
+ FunctionClauseError ->
|
140
|
+ raise Protocol.UndefinedError, protocol: @protocol, value: module, description: "the given module is an embedded schema"
|
42
141
|
end
|
43
142
|
end
|
44
143
|
end
|
changed
lib/ecto/repo.ex
|
@@ -50,7 +50,7 @@ defmodule Ecto.Repo do
|
50
50
|
using the [Telemetry](`:telemetry`) library. By default, the telemetry prefix
|
51
51
|
is based on the module name, so if your module is called
|
52
52
|
`MyApp.Repo`, the prefix will be `[:my_app, :repo]`. See the
|
53
|
- "Telemetry Events" section to see which events we recommend
|
53
|
+ ["Telemetry Events"](#module-telemetry-events) section to see which events we recommend
|
54
54
|
adapters to publish. Note that if you have multiple databases, you
|
55
55
|
should keep the `:telemetry_prefix` consistent for each repo and
|
56
56
|
use the `:repo` property in the event metadata for distinguishing
|
|
@@ -79,14 +79,6 @@ defmodule Ecto.Repo do
|
79
79
|
config :my_app, Repo,
|
80
80
|
url: "ecto:https://postgres:postgres@localhost/ecto_simple?ssl=true&pool_size=10"
|
81
81
|
|
82
|
- In case the URL needs to be dynamically configured, for example by
|
83
|
- reading a system environment variable, such can be done via the
|
84
|
- `c:init/2` repository callback:
|
85
|
-
|
86
|
- def init(_type, config) do
|
87
|
- {:ok, Keyword.put(config, :url, System.get_env("DATABASE_URL"))}
|
88
|
- end
|
89
|
-
|
90
82
|
## Shared options
|
91
83
|
|
92
84
|
Almost all of the repository functions outlined in this module accept the following
|
|
@@ -217,7 +209,7 @@ defmodule Ecto.Repo do
|
217
209
|
@aggregates [:count, :avg, :max, :min, :sum]
|
218
210
|
|
219
211
|
def config do
|
220
|
- {:ok, config} = Ecto.Repo.Supervisor.runtime_config(:runtime, __MODULE__, @otp_app, [])
|
212
|
+ {:ok, config} = Ecto.Repo.Supervisor.init_config(:runtime, __MODULE__, @otp_app, [])
|
221
213
|
config
|
222
214
|
end
|
223
215
|
|
|
@@ -608,6 +600,9 @@ defmodule Ecto.Repo do
|
608
600
|
@doc """
|
609
601
|
A callback executed when the repo starts or when configuration is read.
|
610
602
|
|
603
|
+ This callback is available for backwards compatibility purposes. Most
|
604
|
+ runtime configuration in Elixir today can be done via config/runtime.exs.
|
605
|
+
|
611
606
|
The first argument is the context the callback is being invoked. If it
|
612
607
|
is called because the Repo supervisor is starting, it will be `:supervisor`.
|
613
608
|
It will be `:runtime` if it is called for reading configuration without
|
|
@@ -626,7 +621,7 @@ defmodule Ecto.Repo do
|
626
621
|
@doc """
|
627
622
|
Returns the adapter tied to the repository.
|
628
623
|
"""
|
629
|
- @doc group: "Runtime API"
|
624
|
+ @doc group: "Config API"
|
630
625
|
@callback __adapter__ :: Ecto.Adapter.t()
|
631
626
|
|
632
627
|
@doc """
|
|
@@ -634,13 +629,13 @@ defmodule Ecto.Repo do
|
634
629
|
|
635
630
|
If the `c:init/2` callback is implemented in the repository,
|
636
631
|
it will be invoked with the first argument set to `:runtime`.
|
632
|
+ It does not consider the options given on `c:start_link/1`.
|
637
633
|
"""
|
638
|
- @doc group: "Runtime API"
|
634
|
+ @doc group: "Config API"
|
639
635
|
@callback config() :: Keyword.t()
|
640
636
|
|
641
637
|
@doc """
|
642
|
- Starts any connection pooling or supervision and return `{:ok, pid}`
|
643
|
- or just `:ok` if nothing needs to be done.
|
638
|
+ Starts the Repo supervision tree.
|
644
639
|
|
645
640
|
Returns `{:error, {:already_started, pid}}` if the repo is already
|
646
641
|
started or `{:error, term}` in case anything else goes wrong.
|
|
@@ -650,7 +645,7 @@ defmodule Ecto.Repo do
|
650
645
|
See the configuration in the moduledoc for options shared between adapters,
|
651
646
|
for adapter-specific configuration see the adapter's documentation.
|
652
647
|
"""
|
653
|
- @doc group: "Runtime API"
|
648
|
+ @doc group: "Process API"
|
654
649
|
@callback start_link(opts :: Keyword.t()) ::
|
655
650
|
{:ok, pid}
|
656
651
|
| {:error, {:already_started, pid}}
|
|
@@ -659,7 +654,7 @@ defmodule Ecto.Repo do
|
659
654
|
@doc """
|
660
655
|
Shuts down the repository.
|
661
656
|
"""
|
662
|
- @doc group: "Runtime API"
|
657
|
+ @doc group: "Process API"
|
663
658
|
@callback stop(timeout) :: :ok
|
664
659
|
|
665
660
|
@doc """
|
|
@@ -709,7 +704,7 @@ defmodule Ecto.Repo do
|
709
704
|
@doc """
|
710
705
|
Loads `data` into a schema or a map.
|
711
706
|
|
712
|
- The first argument can be a a schema module or a map (of types).
|
707
|
+ The first argument can be a schema module or a map (of types).
|
713
708
|
The first argument determines the return value: a struct or a map,
|
714
709
|
respectively.
|
715
710
|
|
|
@@ -759,7 +754,7 @@ defmodule Ecto.Repo do
|
759
754
|
|
760
755
|
See `c:put_dynamic_repo/1` for more information.
|
761
756
|
"""
|
762
|
- @doc group: "Runtime API"
|
757
|
+ @doc group: "Process API"
|
763
758
|
@callback get_dynamic_repo() :: atom() | pid()
|
764
759
|
|
765
760
|
@doc """
|
|
@@ -792,7 +787,7 @@ defmodule Ecto.Repo do
|
792
787
|
From this moment on, all future queries done by the current process will
|
793
788
|
run on `:tenant_foo`.
|
794
789
|
"""
|
795
|
- @doc group: "Runtime API"
|
790
|
+ @doc group: "Process API"
|
796
791
|
@callback put_dynamic_repo(name_or_pid :: atom() | pid()) :: atom() | pid()
|
797
792
|
|
798
793
|
## Ecto.Adapter.Queryable
|
|
@@ -827,7 +822,7 @@ defmodule Ecto.Repo do
|
827
822
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
828
823
|
and `join`s in the query that did not have a prefix previously given
|
829
824
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
830
|
- in the schema. For more information see the "Query Prefix" section of the
|
825
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
831
826
|
`Ecto.Query` documentation.
|
832
827
|
|
833
828
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -853,7 +848,7 @@ defmodule Ecto.Repo do
|
853
848
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
854
849
|
and `join`s in the query that did not have a prefix previously given
|
855
850
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
856
|
- in the schema. For more information see the "Query Prefix" section of the
|
851
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
857
852
|
`Ecto.Query` documentation.
|
858
853
|
|
859
854
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -881,7 +876,7 @@ defmodule Ecto.Repo do
|
881
876
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
882
877
|
and `join`s in the query that did not have a prefix previously given
|
883
878
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
884
|
- in the schema. For more information see the "Query Prefix" section of the
|
879
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
885
880
|
`Ecto.Query` documentation.
|
886
881
|
|
887
882
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -912,7 +907,7 @@ defmodule Ecto.Repo do
|
912
907
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
913
908
|
and `join`s in the query that did not have a prefix previously given
|
914
909
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
915
|
- in the schema. For more information see the "Query Prefix" section of the
|
910
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
916
911
|
`Ecto.Query` documentation.
|
917
912
|
|
918
913
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -991,7 +986,7 @@ defmodule Ecto.Repo do
|
991
986
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
992
987
|
and `join`s in the query that did not have a prefix previously given
|
993
988
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
994
|
- in the schema. For more information see the "Query Prefix" section of the
|
989
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
995
990
|
`Ecto.Query` documentation.
|
996
991
|
|
997
992
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -1051,7 +1046,7 @@ defmodule Ecto.Repo do
|
1051
1046
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
1052
1047
|
and `join`s in the query that did not have a prefix previously given
|
1053
1048
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
1054
|
- in the schema. For more information see the "Query Prefix" section of the
|
1049
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
1055
1050
|
`Ecto.Query` documentation.
|
1056
1051
|
|
1057
1052
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -1084,7 +1079,7 @@ defmodule Ecto.Repo do
|
1084
1079
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
1085
1080
|
and `join`s in the query that did not have a prefix previously given
|
1086
1081
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
1087
|
- in the schema. For more information see the "Query Prefix" section of the
|
1082
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
1088
1083
|
`Ecto.Query` documentation.
|
1089
1084
|
|
1090
1085
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -1112,7 +1107,7 @@ defmodule Ecto.Repo do
|
1112
1107
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
1113
1108
|
and `join`s in the query that did not have a prefix previously given
|
1114
1109
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
1115
|
- in the schema. For more information see the "Query Prefix" section of the
|
1110
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
1116
1111
|
`Ecto.Query` documentation.
|
1117
1112
|
|
1118
1113
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -1130,7 +1125,17 @@ defmodule Ecto.Repo do
|
1130
1125
|
database.
|
1131
1126
|
|
1132
1127
|
In case the association was already loaded, preload won't attempt
|
1133
|
- to reload it.
|
1128
|
+ to reload it. Preload assumes each association has the same nested
|
1129
|
+ associations already loaded. If this is not the case, it is
|
1130
|
+ possible to lose information. For example:
|
1131
|
+
|
1132
|
+ comment1 = TestRepo.preload(comment1, [author: [:permalink]])
|
1133
|
+ TestRepo.preload([comment1, comment2], :author)
|
1134
|
+
|
1135
|
+ If both comments are associated to the same author, the first comment
|
1136
|
+ will lose its nested `:permalink` association because the second comment
|
1137
|
+ does not have it preloaded. To avoid this, you must preload the nested
|
1138
|
+ associations as well.
|
1134
1139
|
|
1135
1140
|
If you want to reset the loaded fields, see `Ecto.reset_fields/2`.
|
1136
1141
|
|
|
@@ -1238,16 +1243,17 @@ defmodule Ecto.Repo do
|
1238
1243
|
@callback default_options(operation) :: Keyword.t()
|
1239
1244
|
when operation:
|
1240
1245
|
:all
|
1241
|
- | :insert_all
|
1242
|
- | :update_all
|
1246
|
+ | :delete
|
1243
1247
|
| :delete_all
|
1248
|
+ | :insert
|
1249
|
+ | :insert_all
|
1250
|
+ | :insert_or_update
|
1251
|
+ | :preload
|
1252
|
+ | :reload
|
1244
1253
|
| :stream
|
1245
1254
|
| :transaction
|
1246
|
- | :insert
|
1247
1255
|
| :update
|
1248
|
- | :delete
|
1249
|
- | :insert_or_update
|
1250
|
-
|
1256
|
+ | :update_all
|
1251
1257
|
@doc """
|
1252
1258
|
Fetches all entries from the data store matching the given query.
|
1253
1259
|
|
|
@@ -1259,7 +1265,7 @@ defmodule Ecto.Repo do
|
1259
1265
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
1260
1266
|
and `join`s in the query that did not have a prefix previously given
|
1261
1267
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
1262
|
- in the schema. For more information see the "Query Prefix" section of the
|
1268
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
1263
1269
|
`Ecto.Query` documentation.
|
1264
1270
|
|
1265
1271
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -1290,7 +1296,7 @@ defmodule Ecto.Repo do
|
1290
1296
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
1291
1297
|
and `join`s in the query that did not have a prefix previously given
|
1292
1298
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
1293
|
- in the schema. For more information see the "Query Prefix" section of the
|
1299
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
1294
1300
|
`Ecto.Query` documentation.
|
1295
1301
|
|
1296
1302
|
* `:max_rows` - The number of rows to load from the database as we stream.
|
|
@@ -1332,7 +1338,7 @@ defmodule Ecto.Repo do
|
1332
1338
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
1333
1339
|
and `join`s in the query that did not have a prefix previously given
|
1334
1340
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
1335
|
- in the schema. For more information see the "Query Prefix" section of the
|
1341
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
1336
1342
|
`Ecto.Query` documentation.
|
1337
1343
|
|
1338
1344
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -1381,7 +1387,7 @@ defmodule Ecto.Repo do
|
1381
1387
|
in Postgres or the database in MySQL). This will be applied to all `from`
|
1382
1388
|
and `join`s in the query that did not have a prefix previously given
|
1383
1389
|
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
|
1384
|
- in the schema. For more information see the "Query Prefix" section of the
|
1390
|
+ in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the
|
1385
1391
|
`Ecto.Query` documentation.
|
1386
1392
|
|
1387
1393
|
See the ["Shared options"](#module-shared-options) section at the module
|
|
@@ -1421,9 +1427,11 @@ defmodule Ecto.Repo do
|
1421
1427
|
both (`{"users", MyApp.User}`) as the first argument. The second
|
1422
1428
|
argument is a list of entries to be inserted, either as keyword
|
1423
1429
|
lists or as maps. The keys of the entries are the field names as
|
1424
|
- atoms and the value should be the respective value for the field
|
1425
|
- type or, optionally, an `Ecto.Query` that returns a single entry
|
1426
|
- with a single value.
|
1430
|
+ atoms, when a schema module is specified in the first argument.
|
1431
|
+ Otherwise, the keys can be either atoms or strings representing
|
1432
|
+ the names of the columns in the underlying datastore. The value
|
1433
|
+ should be the respective value for the field type or, optionally,
|
1434
|
+ an `Ecto.Query` that returns a single entry with a single value.
|
1427
1435
|
|
1428
1436
|
It returns a tuple containing the number of entries
|
1429
1437
|
and any returned result as second element. If the database
|
|
@@ -1462,7 +1470,7 @@ defmodule Ecto.Repo do
|
1462
1470
|
* `:on_conflict` - It may be one of `:raise` (the default), `:nothing`,
|
1463
1471
|
`:replace_all`, `{:replace_all_except, fields}`, `{:replace, fields}`,
|
1464
1472
|
a keyword list of update instructions or an `Ecto.Query`
|
1465
|
- query for updates. See the "Upserts" section for more information.
|
1473
|
+ query for updates. See the "[Upserts](#c:insert_all/3-upserts)" section for more information.
|
1466
1474
|
|
1467
1475
|
* `:conflict_target` - A list of column names to verify for conflicts.
|
1468
1476
|
It is expected those columns to have unique indexes on them that may conflict.
|
|
@@ -1470,8 +1478,8 @@ defmodule Ecto.Repo do
|
1470
1478
|
It may also be `{:unsafe_fragment, binary_fragment}` to pass any
|
1471
1479
|
expression to the database without any sanitization, this is useful
|
1472
1480
|
for partial index or index with expressions, such as
|
1473
|
- `{:unsafe_fragment, "(coalesce(firstname, ""), coalesce(lastname, "")) WHERE middlename IS NULL"}` for
|
1474
|
- `ON CONFLICT (coalesce(firstname, ""), coalesce(lastname, "")) WHERE middlename IS NULL` SQL query.
|
1481
|
+ `{:unsafe_fragment, "(coalesce(firstname, ''), coalesce(lastname, '')) WHERE middlename IS NULL"}` for
|
1482
|
+ `ON CONFLICT (coalesce(firstname, ''), coalesce(lastname, '')) WHERE middlename IS NULL` SQL query.
|
1475
1483
|
|
1476
1484
|
* `:placeholders` - A map with placeholders. This feature is not supported
|
1477
1485
|
by all databases. See the "Placeholders" section for more information.
|
|
@@ -1483,7 +1491,8 @@ defmodule Ecto.Repo do
|
1483
1491
|
|
1484
1492
|
A query can be given instead of a list with entries. This query needs to select
|
1485
1493
|
into a map containing only keys that are available as writeable columns in the
|
1486
|
- schema.
|
1494
|
+ schema. This will query and insert the values all inside one query, without
|
1495
|
+ another round trip to the application.
|
1487
1496
|
|
1488
1497
|
## Examples
|
1489
1498
|
|
|
@@ -1581,11 +1590,11 @@ defmodule Ecto.Repo do
|
1581
1590
|
"""
|
1582
1591
|
@doc group: "Schema API"
|
1583
1592
|
@callback insert_all(
|
1584
|
- schema_or_source :: binary | {binary, module} | module,
|
1585
|
- entries_or_query :: [%{atom => value} | Keyword.t(value)] | Ecto.Query.t(),
|
1593
|
+ schema_or_source :: binary() | {binary(), module()} | module(),
|
1594
|
+ entries_or_query :: [%{(atom() | String.t()) => value} | Keyword.t(value)] | Ecto.Query.t(),
|
1586
1595
|
opts :: Keyword.t()
|
1587
|
- ) :: {non_neg_integer, nil | [term]}
|
1588
|
- when value: term | Ecto.Query.t()
|
1596
|
+ ) :: {non_neg_integer(), nil | [term()]}
|
1597
|
+ when value: term() | Ecto.Query.t()
|
1589
1598
|
|
1590
1599
|
@doc """
|
1591
1600
|
Inserts a struct defined via `Ecto.Schema` or a changeset.
|
|
@@ -1612,8 +1621,8 @@ defmodule Ecto.Repo do
|
1612
1621
|
aware that the fields returned from the database overwrite what was
|
1613
1622
|
supplied by the user. Any field not returned by the database will be
|
1614
1623
|
present with the original value supplied by the user. Not all databases
|
1615
|
- support this option and it may not be available during upserts. See
|
1616
|
- the "Upserts" section for more information.
|
1624
|
+ support this option and it may not be available during upserts.
|
1625
|
+ See the ["Upserts"](`c:insert/2#upserts`) section for more information.
|
1617
1626
|
|
1618
1627
|
* `:prefix` - The prefix to run the query on (such as the schema path
|
1619
1628
|
in Postgres or the database in MySQL). This overrides the prefix set
|
|
@@ -1624,7 +1633,7 @@ defmodule Ecto.Repo do
|
1624
1633
|
* `:on_conflict` - It may be one of `:raise` (the default), `:nothing`,
|
1625
1634
|
`:replace_all`, `{:replace_all_except, fields}`, `{:replace, fields}`,
|
1626
1635
|
a keyword list of update instructions or an `Ecto.Query` query for updates.
|
1627
|
- See the "Upserts" section for more information.
|
1636
|
+ See the ["Upserts"](`c:insert/2#upserts`) section for more information.
|
1628
1637
|
|
1629
1638
|
* `:conflict_target` - A list of column names to verify for conflicts.
|
1630
1639
|
It is expected those columns to have unique indexes on them that may conflict.
|
|
@@ -1642,6 +1651,10 @@ defmodule Ecto.Repo do
|
1642
1651
|
* `:stale_error_message` - The message to add to the configured
|
1643
1652
|
`:stale_error_field` when stale errors happen, defaults to "is stale".
|
1644
1653
|
|
1654
|
+ * `:allow_stale` - Doesn't error if insert is stale. Defaults to `false`.
|
1655
|
+ This may happen if there are rules or triggers in the database that
|
1656
|
+ rejects the insert operation.
|
1657
|
+
|
1645
1658
|
See the ["Shared options"](#module-shared-options) section at the module
|
1646
1659
|
documentation for more options.
|
1647
1660
|
|
|
@@ -1758,6 +1771,32 @@ defmodule Ecto.Repo do
|
1758
1771
|
inserting a struct with associations and using the `:on_conflict` option
|
1759
1772
|
at the same time is not recommended, as Ecto will be unable to actually
|
1760
1773
|
track the proper status of the association.
|
1774
|
+
|
1775
|
+ ## Advanced Upserts
|
1776
|
+
|
1777
|
+ Using an `Ecto.Query` for `:on_conflict` can allow us to use more advanced
|
1778
|
+ database features. For example, PostgreSQL supports conditional upserts like
|
1779
|
+ `DO UPDATE SET title = EXCLUDED.title, version = EXCLUDED.version
|
1780
|
+ WHERE EXCLUDED.version > post.version`.
|
1781
|
+ This means that the title and version will be updated only if the proposed
|
1782
|
+ row has a greater version value than the existing row.
|
1783
|
+
|
1784
|
+ Ecto can support this as follows:
|
1785
|
+
|
1786
|
+ conflict_query =
|
1787
|
+ from(p in Post,
|
1788
|
+ update: [set: [
|
1789
|
+ title: fragment("EXCLUDED.title"),
|
1790
|
+ version: fragment("EXCLUDED.version")
|
1791
|
+ ]],
|
1792
|
+ where: fragment("EXCLUDED.version > ?", p.version)
|
1793
|
+ )
|
1794
|
+
|
1795
|
+ MyRepo.insert(
|
1796
|
+ %Post{id: 1, title: "Ecto Upserts (Dance Remix)", version: 2},
|
1797
|
+ conflict_target: [:id],
|
1798
|
+ on_conflict: conflict_query
|
1799
|
+ )
|
1761
1800
|
"""
|
1762
1801
|
@doc group: "Schema API"
|
1763
1802
|
@callback insert(
|
|
@@ -1812,6 +1851,11 @@ defmodule Ecto.Repo do
|
1812
1851
|
* `:stale_error_message` - The message to add to the configured
|
1813
1852
|
`:stale_error_field` when stale errors happen, defaults to "is stale".
|
1814
1853
|
|
1854
|
+ * `:allow_stale` - Doesn't error if update is stale. Defaults to `false`.
|
1855
|
+ This may happen if the struct has been deleted from the database before
|
1856
|
+ the update or if there is a rule or a trigger on the database that rejects
|
1857
|
+ the update operation.
|
1858
|
+
|
1815
1859
|
See the ["Shared options"](#module-shared-options) section at the module
|
1816
1860
|
documentation for more options.
|
1817
1861
|
|
|
@@ -1856,6 +1900,8 @@ defmodule Ecto.Repo do
|
1856
1900
|
* `:stale_error_message` - The message to add to the configured
|
1857
1901
|
`:stale_error_field` when stale errors happen, defaults to "is stale".
|
1858
1902
|
Only applies to updates.
|
1903
|
+ * `:allow_stale` - Doesn't error if delete is stale. Defaults to `false`.
|
1904
|
+ Only applies to updates.
|
1859
1905
|
|
1860
1906
|
See the ["Shared options"](#module-shared-options) section at the module
|
1861
1907
|
documentation for more options.
|
|
@@ -1916,6 +1962,11 @@ defmodule Ecto.Repo do
|
1916
1962
|
* `:stale_error_message` - The message to add to the configured
|
1917
1963
|
`:stale_error_field` when stale errors happen, defaults to "is stale".
|
1918
1964
|
|
1965
|
+ * `:allow_stale` - Doesn't error if delete is stale. Defaults to `false`.
|
1966
|
+ This may happen if the struct has been deleted from the database before
|
1967
|
+ this deletion or if there is a rule or a trigger on the database that rejects
|
1968
|
+ the delete operation.
|
1969
|
+
|
1919
1970
|
See the ["Shared options"](#module-shared-options) section at the module
|
1920
1971
|
documentation for more options.
|
1921
1972
|
|
|
@@ -2034,7 +2085,7 @@ defmodule Ecto.Repo do
|
2034
2085
|
# operation will raise an exception.
|
2035
2086
|
end)
|
2036
2087
|
|
2037
|
- See the "Aborted transactions" section for more examples of aborted
|
2088
|
+ See the ["Aborted transactions"](`c:transaction/2#aborted-transactions`) section for more examples of aborted
|
2038
2089
|
transactions and how to handle them.
|
2039
2090
|
|
2040
2091
|
In practice, managing nested transactions can become complex quickly.
|
|
@@ -2079,14 +2130,14 @@ defmodule Ecto.Repo do
|
2079
2130
|
end)
|
2080
2131
|
|
2081
2132
|
If the changeset is valid, but the insert operation fails due to a database constraint,
|
2082
|
- the subsequent `repo.insert(%Failure{})` operation will raise an exception because the
|
2083
|
- database has already aborted the transaction and thus making the operation invalid.
|
2133
|
+ the subsequent `repo.insert(%Status{value: "failure"})` operation will raise an exception
|
2134
|
+ because the database has already aborted the transaction and thus making the operation invalid.
|
2084
2135
|
In Postgres, the exception would look like this:
|
2085
2136
|
|
2086
2137
|
** (Postgrex.Error) ERROR 25P02 (in_failed_sql_transaction) current transaction is aborted, commands ignored until end of transaction block
|
2087
2138
|
|
2088
2139
|
If the changeset is invalid before it reaches the database due to a validation error,
|
2089
|
- no statement is sent to the database, an `:error` tuple is returned, and `repo.insert(%Failure{})`
|
2140
|
+ no statement is sent to the database, an `:error` tuple is returned, and `repo.insert(%Status{value: "failure"})`
|
2090
2141
|
operation will execute as usual.
|
2091
2142
|
|
2092
2143
|
We have two options to deal with such scenarios:
|
|
@@ -2098,7 +2149,7 @@ defmodule Ecto.Repo do
|
2098
2149
|
|
2099
2150
|
Another alternative is to handle this operation outside of the transaction.
|
2100
2151
|
For example, you can choose to perform an explicit `repo.rollback` call in the
|
2101
|
- `{:error, changeset}` clause and then perform the `repo.insert(%Failure{})` outside
|
2152
|
+ `{:error, changeset}` clause and then perform the `repo.insert(%Status{value: "failure"})` outside
|
2102
2153
|
of the transaction. You might also consider using `Ecto.Multi`, as they automatically
|
2103
2154
|
rollback whenever an operation fails.
|
changed
lib/ecto/repo/assoc.ex
|
@@ -96,12 +96,13 @@ defmodule Ecto.Repo.Assoc do
|
96
96
|
defp maybe_first(list, _), do: list
|
97
97
|
|
98
98
|
defp create_refls(idx, fields, dicts, sources) do
|
99
|
- {_source, schema, _prefix} = elem(sources, idx)
|
99
|
+ schema = get_assoc_schema(sources, idx)
|
100
100
|
|
101
101
|
Enum.map(:lists.zip(dicts, fields), fn
|
102
102
|
{{_primary_keys, _cache, dict, sub_dicts}, {field, {child_idx, child_fields}}} ->
|
103
|
+ refl = schema.__schema__(:association, field)
|
103
104
|
sub_refls = create_refls(child_idx, child_fields, sub_dicts, sources)
|
104
|
- {dict, schema.__schema__(:association, field), sub_refls}
|
105
|
+ {dict, refl, sub_refls}
|
105
106
|
end)
|
106
107
|
end
|
107
108
|
|
|
@@ -110,11 +111,21 @@ defmodule Ecto.Repo.Assoc do
|
110
111
|
create_accs(child_idx, child_fields, sources, %{})
|
111
112
|
end)
|
112
113
|
|
113
|
- {_source, schema, _prefix} = elem(sources, idx)
|
114
|
+ schema = get_assoc_schema(sources, idx)
|
114
115
|
|
115
116
|
case schema.__schema__(:primary_key) do
|
116
117
|
[] -> raise Ecto.NoPrimaryKeyFieldError, schema: schema
|
117
118
|
pk -> {pk, %{}, initial_dict, acc}
|
118
119
|
end
|
119
120
|
end
|
121
|
+
|
122
|
+ defp get_assoc_schema(sources, idx) do
|
123
|
+ case elem(sources, idx) do
|
124
|
+ {_, schema, _} ->
|
125
|
+ schema
|
126
|
+
|
127
|
+ %Ecto.SubQuery{select: {:source, {_, schema}, _, _}} ->
|
128
|
+ schema
|
129
|
+ end
|
130
|
+ end
|
120
131
|
end
|
changed
lib/ecto/repo/preloader.ex
|
@@ -12,14 +12,16 @@ defmodule Ecto.Repo.Preloader do
|
12
12
|
Transforms a result set based on query preloads, loading
|
13
13
|
the associations onto their parent schema.
|
14
14
|
"""
|
15
|
- @spec query([list], Ecto.Repo.t, list, Access.t, fun, {adapter_meta :: map, opts :: Keyword.t}) :: [list]
|
16
|
- def query([], _repo_name, _preloads, _take, _fun, _tuplet), do: []
|
17
|
- def query(rows, _repo_name, [], _take, fun, _tuplet), do: Enum.map(rows, fun)
|
15
|
+ @spec query([list], Ecto.Repo.t, list, Access.t, list, fun, {adapter_meta :: map, opts :: Keyword.t}) :: [list]
|
16
|
+ def query([], _repo_name, _preloads, _take, _assocs, _fun, _tuplet), do: []
|
17
|
+ def query(rows, _repo_name, [], _take, _assocs, fun, _tuplet), do: Enum.map(rows, fun)
|
18
|
+
|
19
|
+ def query(rows, repo_name, preloads, take, assocs, fun, tuplet) do
|
20
|
+ assocs = normalize_query_assocs(assocs)
|
18
21
|
|
19
|
- def query(rows, repo_name, preloads, take, fun, tuplet) do
|
20
22
|
rows
|
21
23
|
|> extract()
|
22
|
- |> normalize_and_preload_each(repo_name, preloads, take, tuplet)
|
24
|
+ |> normalize_and_preload_each(repo_name, preloads, take, assocs, tuplet)
|
23
25
|
|> unextract(rows, fun)
|
24
26
|
end
|
25
27
|
|
|
@@ -41,16 +43,16 @@ defmodule Ecto.Repo.Preloader do
|
41
43
|
end
|
42
44
|
|
43
45
|
def preload(structs, repo_name, preloads, {_adapter_meta, opts} = tuplet) when is_list(structs) do
|
44
|
- normalize_and_preload_each(structs, repo_name, preloads, opts[:take], tuplet)
|
46
|
+ normalize_and_preload_each(structs, repo_name, preloads, opts[:take], %{}, tuplet)
|
45
47
|
end
|
46
48
|
|
47
49
|
def preload(struct, repo_name, preloads, {_adapter_meta, opts} = tuplet) when is_map(struct) do
|
48
|
- normalize_and_preload_each([struct], repo_name, preloads, opts[:take], tuplet) |> hd()
|
50
|
+ normalize_and_preload_each([struct], repo_name, preloads, opts[:take], %{}, tuplet) |> hd()
|
49
51
|
end
|
50
52
|
|
51
|
- defp normalize_and_preload_each(structs, repo_name, preloads, take, tuplet) do
|
53
|
+ defp normalize_and_preload_each(structs, repo_name, preloads, take, query_assocs, tuplet) do
|
52
54
|
preloads = normalize(preloads, take, preloads)
|
53
|
- preload_each(structs, repo_name, preloads, tuplet)
|
55
|
+ preload_each(structs, repo_name, preloads, query_assocs, tuplet)
|
54
56
|
rescue
|
55
57
|
e ->
|
56
58
|
# Reraise errors so we ignore the preload inner stacktrace
|
|
@@ -59,21 +61,21 @@ defmodule Ecto.Repo.Preloader do
|
59
61
|
|
60
62
|
## Preloading
|
61
63
|
|
62
|
- defp preload_each(structs, _repo_name, [], _tuplet), do: structs
|
63
|
- defp preload_each([], _repo_name, _preloads, _tuplet), do: []
|
64
|
- defp preload_each(structs, repo_name, preloads, tuplet) do
|
64
|
+ defp preload_each(structs, _repo_name, [], _query_assocs, _tuplet), do: structs
|
65
|
+ defp preload_each([], _repo_name, _preloads, _query_assocs, _tuplet), do: []
|
66
|
+ defp preload_each(structs, repo_name, preloads, query_assocs, tuplet) do
|
65
67
|
if sample = Enum.find(structs, & &1) do
|
66
68
|
module = sample.__struct__
|
67
69
|
prefix = preload_prefix(tuplet, sample)
|
68
|
- {assocs, throughs, embeds} = expand(module, preloads, {%{}, %{}, []})
|
70
|
+ {assocs, throughs, embeds} = expand(module, preloads, query_assocs, {%{}, [], []})
|
69
71
|
structs = preload_embeds(structs, embeds, repo_name, tuplet)
|
72
|
+ structs = preload_throughs(structs, throughs, repo_name, query_assocs, tuplet)
|
70
73
|
|
71
74
|
{fetched_assocs, to_fetch_queries} =
|
72
75
|
prepare_queries(structs, module, assocs, prefix, repo_name, tuplet)
|
73
76
|
|
74
77
|
fetched_queries = maybe_pmap(to_fetch_queries, repo_name, tuplet)
|
75
|
- assocs = preload_assocs(fetched_assocs, fetched_queries, repo_name, tuplet)
|
76
|
- throughs = Map.values(throughs)
|
78
|
+ assocs = preload_assocs(fetched_assocs, fetched_queries, repo_name, query_assocs, tuplet)
|
77
79
|
|
78
80
|
for struct <- structs do
|
79
81
|
struct = Enum.reduce assocs, struct, &load_assoc/2
|
|
@@ -125,7 +127,7 @@ defmodule Ecto.Repo.Preloader do
|
125
127
|
|
126
128
|
# Then we execute queries in parallel
|
127
129
|
defp maybe_pmap(preloaders, _repo_name, {adapter_meta, opts}) do
|
128
|
- if match?([_,_|_], preloaders) and not adapter_meta.adapter.checked_out?(adapter_meta) and
|
130
|
+ if match?([_, _ | _] , preloaders) and not adapter_meta.adapter.checked_out?(adapter_meta) and
|
129
131
|
Keyword.get(opts, :in_parallel, true) do
|
130
132
|
# We pass caller: self() so the ownership pool knows where
|
131
133
|
# to fetch the connection from and set the proper timeouts.
|
|
@@ -151,23 +153,24 @@ defmodule Ecto.Repo.Preloader do
|
151
153
|
|
152
154
|
# Then we unpack the query results, merge them, and preload recursively
|
153
155
|
defp preload_assocs(
|
154
|
- [{assoc, query?, loaded_ids, loaded_structs, preloads} | assocs],
|
156
|
+ [{assoc, query?, loaded_ids, loaded_structs, sub_preloads} | assocs],
|
155
157
|
queries,
|
156
158
|
repo_name,
|
159
|
+ query_assocs,
|
157
160
|
tuplet
|
158
161
|
) do
|
159
162
|
{fetch_ids, fetch_structs, queries} = maybe_unpack_query(query?, queries)
|
160
|
- all = preload_each(Enum.reverse(loaded_structs, fetch_structs), repo_name, preloads, tuplet)
|
163
|
+ sub_query_assocs = Map.get(query_assocs, assoc.field, %{})
|
164
|
+ all = preload_each(Enum.reverse(loaded_structs, fetch_structs), repo_name, sub_preloads, sub_query_assocs, tuplet)
|
161
165
|
entry = {:assoc, assoc, assoc_map(assoc.cardinality, Enum.reverse(loaded_ids, fetch_ids), all)}
|
162
|
- [entry | preload_assocs(assocs, queries, repo_name, tuplet)]
|
166
|
+ [entry | preload_assocs(assocs, queries, repo_name, query_assocs, tuplet)]
|
163
167
|
end
|
164
168
|
|
165
|
- defp preload_assocs([], [], _repo_name, _tuplet), do: []
|
169
|
+ defp preload_assocs([] = _assocs, [] = _queries, _, _, _), do: []
|
166
170
|
|
167
|
- defp preload_embeds(structs, [], _repo_name, _tuplet), do: structs
|
171
|
+ defp preload_embeds(structs, [] = _embeds, _, _), do: structs
|
168
172
|
|
169
173
|
defp preload_embeds(structs, [embed | embeds], repo_name, tuplet) do
|
170
|
-
|
171
174
|
{%{field: field, cardinality: card}, sub_preloads} = embed
|
172
175
|
|
173
176
|
{embed_structs, counts} =
|
|
@@ -179,23 +182,59 @@ defmodule Ecto.Repo.Preloader do
|
179
182
|
struct, _counts -> raise ArgumentError, "expected #{inspect(struct)} to contain embed `#{field}`"
|
180
183
|
end)
|
181
184
|
|
182
|
- embed_structs = preload_each(embed_structs, repo_name, sub_preloads, tuplet)
|
183
|
- structs = load_embeds(card, field, structs, embed_structs, Enum.reverse(counts), [])
|
185
|
+ # It is not possible for an embed to be preloaded through Ecto.Query.preload
|
186
|
+ # Therefore, we don't consider associations coming from queries
|
187
|
+ embed_structs = preload_each(embed_structs, repo_name, sub_preloads, %{}, tuplet)
|
188
|
+ structs = put_through_or_embed(card, field, structs, embed_structs, Enum.reverse(counts), [])
|
184
189
|
preload_embeds(structs, embeds, repo_name, tuplet)
|
185
190
|
end
|
186
191
|
|
187
|
- defp load_embeds(_card, _field, [], [], [], acc), do: Enum.reverse(acc)
|
192
|
+ defp preload_throughs(structs, [] = _throughs, _, _, _), do: structs
|
188
193
|
|
189
|
- defp load_embeds(card, field, [struct | structs], embed_structs, [0 | counts], acc),
|
190
|
- do: load_embeds(card, field, structs, embed_structs, counts, [struct | acc])
|
194
|
+ defp preload_throughs(
|
195
|
+ structs,
|
196
|
+ [{_, _, false = _from_query?} | throughs],
|
197
|
+ repo_name,
|
198
|
+ query_assocs,
|
199
|
+ tuplet
|
200
|
+ ) do
|
201
|
+ # Through associations will not be preloaded directly unless they were
|
202
|
+ # loaded through a join using Ecto.Query.preload. When using Ecto.Repo.preload
|
203
|
+ # or Ecto.Query.preload where the through association is not part of a join,
|
204
|
+ # the chain of associations making up the through association are preloaded instead.
|
205
|
+ preload_throughs(structs, throughs, repo_name, query_assocs, tuplet)
|
206
|
+ end
|
191
207
|
|
192
|
- defp load_embeds(:one, field, [struct | structs], [embed_struct | embed_structs], [1 | counts], acc),
|
193
|
- do: load_embeds(:one, field, structs, embed_structs, counts, [Map.put(struct, field, embed_struct) | acc])
|
208
|
+ defp preload_throughs(structs, [through | throughs], repo_name, query_assocs, tuplet) do
|
209
|
+ {{_, %{field: field, cardinality: card}, _}, sub_preloads, true} = through
|
210
|
+ sub_query_assocs = Map.get(query_assocs, field, %{})
|
194
211
|
|
195
|
- defp load_embeds(:many, field, [struct | structs], embed_structs, [count | counts], acc) do
|
196
|
- {current_embeds, rest_embeds} = split_n(embed_structs, count, [])
|
197
|
- acc = [Map.put(struct, field, Enum.reverse(current_embeds)) | acc]
|
198
|
- load_embeds(:many, field, structs, rest_embeds, counts, acc)
|
212
|
+ {through_structs, counts} =
|
213
|
+ Enum.flat_map_reduce(structs, [], fn
|
214
|
+ %{^field => throughs}, counts when is_list(throughs) -> {throughs, [length(throughs) | counts]}
|
215
|
+ %{^field => nil}, counts -> {[], [0 | counts]}
|
216
|
+ %{^field => through}, counts -> {[through], [1 | counts]}
|
217
|
+ nil, counts -> {[], [0 | counts]}
|
218
|
+ struct, _counts -> raise ArgumentError, "expected #{inspect(struct)} to contain through association `#{field}`"
|
219
|
+ end)
|
220
|
+
|
221
|
+ through_structs = preload_each(through_structs, repo_name, sub_preloads, sub_query_assocs, tuplet)
|
222
|
+ structs = put_through_or_embed(card, field, structs, through_structs, Enum.reverse(counts), [])
|
223
|
+ preload_throughs(structs, throughs, repo_name, query_assocs, tuplet)
|
224
|
+ end
|
225
|
+
|
226
|
+ defp put_through_or_embed(_card, _field, [], [], [], acc), do: Enum.reverse(acc)
|
227
|
+
|
228
|
+ defp put_through_or_embed(card, field, [struct | structs], loaded_structs, [0 | counts], acc),
|
229
|
+ do: put_through_or_embed(card, field, structs, loaded_structs, counts, [struct | acc])
|
230
|
+
|
231
|
+ defp put_through_or_embed(:one, field, [struct | structs], [loaded | loaded_structs], [1 | counts], acc),
|
232
|
+ do: put_through_or_embed(:one, field, structs, loaded_structs, counts, [Map.put(struct, field, loaded) | acc])
|
233
|
+
|
234
|
+ defp put_through_or_embed(:many, field, [struct | structs], loaded_structs, [count | counts], acc) do
|
235
|
+ {current_loaded, rest_loaded} = split_n(loaded_structs, count, [])
|
236
|
+ acc = [Map.put(struct, field, Enum.reverse(current_loaded)) | acc]
|
237
|
+ put_through_or_embed(:many, field, structs, rest_loaded, counts, acc)
|
199
238
|
end
|
200
239
|
|
201
240
|
defp maybe_unpack_query(false, queries), do: {[], [], queries}
|
|
@@ -238,12 +277,13 @@ defmodule Ecto.Repo.Preloader do
|
238
277
|
end
|
239
278
|
end
|
240
279
|
|
241
|
- defp fetch_query(ids, assoc, _repo_name, query, _prefix, related_key, _take, _tuplet) when is_function(query, 1) do
|
280
|
+ defp fetch_query(ids, assoc, _repo_name, query, _prefix, related_key, _take, _tuplet)
|
281
|
+ when is_function(query, 1) or is_function(query, 2) do
|
242
282
|
# Note we use an explicit sort because we don't want
|
243
283
|
# to reorder based on the struct. Only the ID.
|
244
284
|
ids
|
245
|
- |> Enum.uniq
|
246
|
- |> query.()
|
285
|
+ |> Enum.uniq()
|
286
|
+ |> preload_function(assoc, query)
|
247
287
|
|> fetched_records_to_tuple_ids(assoc, related_key)
|
248
288
|
|> Enum.sort(fn {id1, _}, {id2, _} -> id1 <= id2 end)
|
249
289
|
|> unzip_ids([], [])
|
|
@@ -273,7 +313,7 @@ defmodule Ecto.Repo.Preloader do
|
273
313
|
query = add_preload_order(assoc.preload_order, query)
|
274
314
|
|
275
315
|
update_in query.order_bys, fn order_bys ->
|
276
|
- [%Ecto.Query.QueryExpr{expr: [asc: related_field_ast], params: [],
|
316
|
+ [%Ecto.Query.ByExpr{expr: [asc: related_field_ast], params: [],
|
277
317
|
file: __ENV__.file, line: __ENV__.line}|order_bys]
|
278
318
|
end
|
279
319
|
|
|
@@ -284,6 +324,9 @@ defmodule Ecto.Repo.Preloader do
|
284
324
|
unzip_ids Ecto.Repo.Queryable.all(repo_name, query, tuplet), [], []
|
285
325
|
end
|
286
326
|
|
327
|
+ defp preload_function(ids, _assoc, query) when is_function(query, 1), do: query.(ids)
|
328
|
+ defp preload_function(ids, assoc, query) when is_function(query, 2), do: query.(ids, assoc)
|
329
|
+
|
287
330
|
defp fetched_records_to_tuple_ids([], _assoc, _related_key),
|
288
331
|
do: []
|
289
332
|
|
|
@@ -443,11 +486,10 @@ defmodule Ecto.Repo.Preloader do
|
443
486
|
Map.put(struct, field, loaded)
|
444
487
|
end
|
445
488
|
|
446
|
- defp load_through({:through, _assoc, _throughs}, nil) do
|
447
|
- nil
|
448
|
- end
|
489
|
+ defp load_through({_, _, _}, nil), do: nil
|
490
|
+ defp load_through({_, _, true = _from_query?}, struct), do: struct
|
449
491
|
|
450
|
- defp load_through({:through, assoc, throughs}, struct) do
|
492
|
+ defp load_through({{:through, assoc, throughs}, _, false = _from_query?}, struct) do
|
451
493
|
%{cardinality: cardinality, field: field, owner: owner} = assoc
|
452
494
|
{loaded, _} = Enum.reduce(throughs, {[struct], owner}, &recur_through/2)
|
453
495
|
Map.put(struct, field, maybe_first(loaded, cardinality))
|
|
@@ -535,13 +577,13 @@ defmodule Ecto.Repo.Preloader do
|
535
577
|
end
|
536
578
|
|
537
579
|
defp normalize_each({atom, {query, list}}, acc, take, original)
|
538
|
- when is_atom(atom) and (is_map(query) or is_function(query, 1)) do
|
580
|
+ when is_atom(atom) and (is_map(query) or is_function(query, 1) or is_function(query, 2)) do
|
539
581
|
fields = take(take, atom)
|
540
582
|
[{atom, {fields, query!(query), normalize_each(wrap(list, original), [], fields, original)}}|acc]
|
541
583
|
end
|
542
584
|
|
543
585
|
defp normalize_each({atom, query}, acc, take, _original)
|
544
|
- when is_atom(atom) and (is_map(query) or is_function(query, 1)) do
|
586
|
+ when is_atom(atom) and (is_map(query) or is_function(query, 1) or is_function(query, 2)) do
|
545
587
|
[{atom, {take(take, atom), query!(query), []}}|acc]
|
546
588
|
end
|
547
589
|
|
|
@@ -559,6 +601,7 @@ defmodule Ecto.Repo.Preloader do
|
559
601
|
end
|
560
602
|
|
561
603
|
defp query!(query) when is_function(query, 1), do: query
|
604
|
+ defp query!(query) when is_function(query, 2), do: query
|
562
605
|
defp query!(%Ecto.Query{} = query), do: query
|
563
606
|
|
564
607
|
defp take(take, field) do
|
|
@@ -577,9 +620,19 @@ defmodule Ecto.Repo.Preloader do
|
577
620
|
"preload expects an atom, a (nested) keyword or a (nested) list of atoms"
|
578
621
|
end
|
579
622
|
|
623
|
+ defp normalize_query_assocs([]), do: %{}
|
624
|
+
|
625
|
+ defp normalize_query_assocs(assocs) when is_list(assocs) do
|
626
|
+ Enum.reduce(assocs, %{}, &normalize_each_query_assoc(&1, &2))
|
627
|
+ end
|
628
|
+
|
629
|
+ defp normalize_each_query_assoc({field, {_idx, sub_assocs}}, acc) do
|
630
|
+ Map.put(acc, field, normalize_query_assocs(sub_assocs))
|
631
|
+ end
|
632
|
+
|
580
633
|
## Expand
|
581
634
|
|
582
|
- def expand(schema, preloads, acc) do
|
635
|
+ def expand(schema, preloads, query_assocs, acc) do
|
583
636
|
Enum.reduce(preloads, acc, fn {preload, {fields, query, sub_preloads}},
|
584
637
|
{assocs, throughs, embeds} ->
|
585
638
|
assoc_or_embed = association_or_embed!(schema, preload)
|
|
@@ -593,13 +646,18 @@ defmodule Ecto.Repo.Preloader do
|
593
646
|
{assocs, throughs, embeds}
|
594
647
|
|
595
648
|
{:through, _, through} ->
|
596
|
- through =
|
597
|
- through
|
598
|
- |> Enum.reverse()
|
599
|
- |> Enum.reduce({fields, query, sub_preloads}, &{nil, nil, [{&1, &2}]})
|
600
|
- |> elem(2)
|
649
|
+ case query_assocs do
|
650
|
+ %{^preload => _} ->
|
651
|
+ {assocs, [{info, sub_preloads, true} | throughs], embeds}
|
601
652
|
|
602
|
- expand(schema, through, {assocs, Map.put(throughs, preload, info), embeds})
|
653
|
+ _ ->
|
654
|
+ {_, _, through} =
|
655
|
+ through
|
656
|
+ |> Enum.reverse()
|
657
|
+ |> Enum.reduce({fields, query, sub_preloads}, &{nil, nil, [{&1, &2}]})
|
658
|
+
|
659
|
+ expand(schema, through, query_assocs, {assocs, [{info, sub_preloads, false} | throughs], embeds})
|
660
|
+ end
|
603
661
|
|
604
662
|
:embed ->
|
605
663
|
if sub_preloads == [] do
|
changed
lib/ecto/repo/queryable.ex
|
@@ -235,7 +235,7 @@ defmodule Ecto.Repo.Queryable do
|
235
235
|
{count,
|
236
236
|
rows
|
237
237
|
|> Ecto.Repo.Assoc.query(assocs, sources, preprocessor)
|
238
|
- |> Ecto.Repo.Preloader.query(name, preloads, take, postprocessor, tuplet)}
|
238
|
+ |> Ecto.Repo.Preloader.query(name, preloads, take, assocs, postprocessor, tuplet)}
|
239
239
|
end
|
240
240
|
end
|
241
241
|
|
|
@@ -576,7 +576,7 @@ defmodule Ecto.Repo.Queryable do
|
576
576
|
end
|
577
577
|
|
578
578
|
unless Enum.all?(structs, &(&1.__struct__ == head.__struct__)) do
|
579
|
- raise ArgumentError, "expected an homogenous list, received different struct types"
|
579
|
+ raise ArgumentError, "expected an homogeneous list, received different struct types"
|
580
580
|
end
|
581
581
|
|
582
582
|
:ok
|
changed
lib/ecto/repo/schema.ex
|
@@ -52,7 +52,7 @@ defmodule Ecto.Repo.Schema do
|
52
52
|
on_conflict = Keyword.get(opts, :on_conflict, :raise)
|
53
53
|
conflict_target = Keyword.get(opts, :conflict_target, [])
|
54
54
|
conflict_target = conflict_target(conflict_target, dumper)
|
55
|
- {on_conflict, conflict_cast_params} = on_conflict(on_conflict, conflict_target, schema_meta, counter, adapter)
|
55
|
+ {on_conflict, conflict_cast_params} = on_conflict(on_conflict, conflict_target, schema_meta, counter, dumper, adapter)
|
56
56
|
opts = Keyword.put(opts, :cast_params, placeholder_cast_params ++ row_cast_params ++ conflict_cast_params)
|
57
57
|
|
58
58
|
{count, rows_or_query} =
|
|
@@ -104,43 +104,64 @@ defmodule Ecto.Repo.Schema do
|
104
104
|
{rows, header, row_cast_params, placeholder_cast_params, placeholder_dump_params, fn -> counter end}
|
105
105
|
end
|
106
106
|
|
107
|
- defp extract_header_and_fields(repo, %Ecto.Query{} = query, _schema, _dumper, _autogen_id, _placeholder_map, adapter, opts) do
|
107
|
+ defp extract_header_and_fields(repo, %Ecto.Query{} = query, _schema, dumper, _autogen_id, _placeholder_map, adapter, opts) do
|
108
108
|
{query, opts} = repo.prepare_query(:insert_all, query, opts)
|
109
109
|
query = attach_prefix(query, opts)
|
110
110
|
|
111
|
- {query, cast_params, dump_params} = Ecto.Adapter.Queryable.plan_query(:insert_all, adapter, query)
|
111
|
+ {query, cast_params, dump_params} =
|
112
|
+ Ecto.Adapter.Queryable.plan_query(:insert_all, adapter, query)
|
112
113
|
|
113
|
- ix = case query.select do
|
114
|
- %Ecto.Query.SelectExpr{expr: {:&, _, [ix]}} -> ix
|
115
|
- _ -> nil
|
116
|
- end
|
114
|
+ ix =
|
115
|
+ case query.select do
|
116
|
+ %Ecto.Query.SelectExpr{expr: {:&, _, [ix]}} -> ix
|
117
|
+ _ -> nil
|
118
|
+ end
|
117
119
|
|
118
|
- header = case query.select do
|
119
|
- %Ecto.Query.SelectExpr{expr: {:%{}, _ctx, args}} ->
|
120
|
- Enum.map(args, &elem(&1, 0))
|
120
|
+ header =
|
121
|
+ case query.select do
|
122
|
+ %Ecto.Query.SelectExpr{expr: {:%{}, [], [{:|, _, [{:&, _, [ix]}, args]}]}, fields: fields} ->
|
123
|
+ {updated_fields, updated_set} =
|
124
|
+ Enum.map_reduce(args, MapSet.new(), fn {field, _}, set ->
|
125
|
+ dumped_field = insert_all_select_dump!(field, dumper)
|
126
|
+ {dumped_field, MapSet.put(set, dumped_field)}
|
127
|
+ end)
|
121
128
|
|
122
|
- %Ecto.Query.SelectExpr{take: %{^ix => {_fun, fields}}} ->
|
123
|
- fields
|
129
|
+ unchanged_fields =
|
130
|
+ for {{:., _, [{:&, _, [^ix]}, field]}, [], []} = expr <- fields,
|
131
|
+ not MapSet.member?(updated_set, field),
|
132
|
+ do: insert_all_select_dump!(expr)
|
124
133
|
|
125
|
- _ ->
|
126
|
- raise ArgumentError, """
|
127
|
- cannot generate a fields list for insert_all from the given source query
|
128
|
- because it does not have a select clause that uses a map:
|
134
|
+ unchanged_fields ++ updated_fields
|
129
135
|
|
130
|
- #{inspect query}
|
136
|
+ %Ecto.Query.SelectExpr{expr: {:%{}, _ctx, args}} ->
|
137
|
+ Enum.map(args, fn {field, _} -> insert_all_select_dump!(field, dumper) end)
|
131
138
|
|
132
|
- Please add a select clause that selects into a map, like this:
|
139
|
+ %Ecto.Query.SelectExpr{take: %{^ix => {_fun, fields}}} ->
|
140
|
+ Enum.map(fields, &insert_all_select_dump!(&1, dumper))
|
133
141
|
|
134
|
- from x in Source,
|
135
|
- ...,
|
136
|
- select: %{
|
137
|
- field_a: x.bar,
|
138
|
- field_b: x.foo
|
139
|
- }
|
142
|
+ %Ecto.Query.SelectExpr{expr: {:&, _, [_ix]}, fields: fields} ->
|
143
|
+ Enum.map(fields, &insert_all_select_dump!(&1))
|
140
144
|
|
141
|
- All keys must exist in the schema that is being inserted into
|
142
|
- """
|
143
|
- end
|
145
|
+ _ ->
|
146
|
+ raise ArgumentError, """
|
147
|
+ cannot generate a fields list for insert_all from the given source query:
|
148
|
+
|
149
|
+ #{inspect(query)}
|
150
|
+
|
151
|
+ The select clause must be one of the following:
|
152
|
+
|
153
|
+ * A single `map/2` or several `map/2` expressions combined with `select_merge`
|
154
|
+ * A single `struct/2` or several `struct/2` expressions combined with `select_merge`
|
155
|
+ * A source such as `p` in the query `from p in Post`
|
156
|
+ * A single literal map or several literal maps combined with `select_merge`. If
|
157
|
+ combining several literal maps, there cannot be any query interpolations
|
158
|
+ except in the last `select_merge`. Consider using `Ecto.Query.exclude/2`
|
159
|
+ to rebuild the select expression from scratch if you need multiple `select_merge`
|
160
|
+ statements with interpolations
|
161
|
+
|
162
|
+ All keys must exist in the schema that is being inserted into
|
163
|
+ """
|
164
|
+ end
|
144
165
|
|
145
166
|
counter = fn -> length(dump_params) end
|
146
167
|
|
|
@@ -160,7 +181,7 @@ defmodule Ecto.Repo.Schema do
|
160
181
|
defp init_mapper(schema, dumper, adapter, placeholder_map) do
|
161
182
|
fn {field, value}, acc ->
|
162
183
|
case dumper do
|
163
|
- %{^field => {source, type}} ->
|
184
|
+ %{^field => {source, type, writable}} when writable != :never ->
|
164
185
|
extract_value(source, value, type, placeholder_map, acc, fn val ->
|
165
186
|
dump_field!(:insert_all, schema, field, type, val, adapter)
|
166
187
|
end)
|
|
@@ -168,7 +189,8 @@ defmodule Ecto.Repo.Schema do
|
168
189
|
%{} ->
|
169
190
|
raise ArgumentError,
|
170
191
|
"unknown field `#{inspect(field)}` in schema #{inspect(schema)} given to " <>
|
171
|
- "insert_all. Note virtual fields and associations are not supported"
|
192
|
+ "insert_all. Unwritable fields, such as virtual and read only fields " <>
|
193
|
+ "are not supported. Associations are also not supported"
|
172
194
|
end
|
173
195
|
end
|
174
196
|
end
|
|
@@ -247,6 +269,22 @@ defmodule Ecto.Repo.Schema do
|
247
269
|
{rows, Enum.reverse(cast_params), counter}
|
248
270
|
end
|
249
271
|
|
272
|
+ defp insert_all_select_dump!({{:., dot_meta, [{:&, _, [_]}, field]}, [], []}) do
|
273
|
+ if dot_meta[:writable] == :never do
|
274
|
+ raise ArgumentError, "cannot select unwritable field `#{inspect(field)}` for insert_all"
|
275
|
+ else
|
276
|
+ field
|
277
|
+ end
|
278
|
+ end
|
279
|
+
|
280
|
+ defp insert_all_select_dump!(field, dumper) when is_atom(field) do
|
281
|
+ case dumper do
|
282
|
+ %{^field => {source, _, writable}} when writable != :never -> source
|
283
|
+ %{} -> raise ArgumentError, "cannot select unwritable field `#{inspect(field)}` for insert_all"
|
284
|
+ nil -> field
|
285
|
+ end
|
286
|
+ end
|
287
|
+
|
250
288
|
defp autogenerate_id(nil, fields, header, _adapter) do
|
251
289
|
{fields, header}
|
252
290
|
end
|
|
@@ -324,7 +362,7 @@ defmodule Ecto.Repo.Schema do
|
324
362
|
struct = struct_from_changeset!(:insert, changeset)
|
325
363
|
schema = struct.__struct__
|
326
364
|
dumper = schema.__schema__(:dump)
|
327
|
- fields = schema.__schema__(:fields)
|
365
|
+ insertable_fields = schema.__schema__(:insertable_fields)
|
328
366
|
assocs = schema.__schema__(:associations)
|
329
367
|
embeds = schema.__schema__(:embeds)
|
330
368
|
|
|
@@ -341,7 +379,7 @@ defmodule Ecto.Repo.Schema do
|
341
379
|
# On insert, we always merge the whole struct into the
|
342
380
|
# changeset as changes, except the primary key if it is nil.
|
343
381
|
changeset = put_repo_and_action(changeset, :insert, repo, tuplet)
|
344
|
- changeset = Relation.surface_changes(changeset, struct, fields ++ assocs)
|
382
|
+ changeset = Relation.surface_changes(changeset, struct, insertable_fields ++ assocs)
|
345
383
|
|
346
384
|
wrap_in_transaction(adapter, adapter_meta, opts, changeset, assocs, embeds, prepare, fn ->
|
347
385
|
assoc_opts = assoc_opts(assocs, opts)
|
|
@@ -360,14 +398,14 @@ defmodule Ecto.Repo.Schema do
|
360
398
|
{changes, cast_extra, dump_extra, return_types, return_sources} =
|
361
399
|
autogenerate_id(autogen_id, changes, return_types, return_sources, adapter)
|
362
400
|
|
363
|
- changes = Map.take(changes, fields)
|
401
|
+ changes = Map.take(changes, insertable_fields)
|
364
402
|
autogen = autogenerate_changes(schema, :insert, changes)
|
365
403
|
|
366
404
|
dump_changes =
|
367
405
|
dump_changes!(:insert, changes, autogen, schema, dump_extra, dumper, adapter)
|
368
406
|
|
369
407
|
{on_conflict, conflict_cast_params} =
|
370
|
- on_conflict(on_conflict, conflict_target, schema_meta, fn -> length(dump_changes) end, adapter)
|
408
|
+ on_conflict(on_conflict, conflict_target, schema_meta, fn -> length(dump_changes) end, dumper, adapter)
|
371
409
|
|
372
410
|
change_values = Enum.map(changes, &elem(&1, 1))
|
373
411
|
autogen_values = Enum.map(autogen, &elem(&1, 1))
|
|
@@ -416,7 +454,7 @@ defmodule Ecto.Repo.Schema do
|
416
454
|
struct = struct_from_changeset!(:update, changeset)
|
417
455
|
schema = struct.__struct__
|
418
456
|
dumper = schema.__schema__(:dump)
|
419
|
- fields = schema.__schema__(:fields)
|
457
|
+ updatable_fields = schema.__schema__(:updatable_fields)
|
420
458
|
assocs = schema.__schema__(:associations)
|
421
459
|
embeds = schema.__schema__(:embeds)
|
422
460
|
|
|
@@ -445,7 +483,7 @@ defmodule Ecto.Repo.Schema do
|
445
483
|
if changeset.valid? do
|
446
484
|
embeds = Ecto.Embedded.prepare(changeset, embeds, adapter, :update)
|
447
485
|
|
448
|
- changes = changeset.changes |> Map.merge(embeds) |> Map.take(fields)
|
486
|
+ changes = changeset.changes |> Map.merge(embeds) |> Map.take(updatable_fields)
|
449
487
|
autogen = autogenerate_changes(schema, :update, changes)
|
450
488
|
dump_changes = dump_changes!(:update, changes, autogen, schema, [], dumper, adapter)
|
451
489
|
|
|
@@ -627,7 +665,7 @@ defmodule Ecto.Repo.Schema do
|
627
665
|
end
|
628
666
|
defp fields_to_sources(fields, dumper) do
|
629
667
|
Enum.reduce(fields, {[], []}, fn field, {types, sources} ->
|
630
|
- {source, type} = Map.fetch!(dumper, field)
|
668
|
+ {source, type, _writable} = Map.fetch!(dumper, field)
|
631
669
|
{[{field, type} | types], [source | sources]}
|
632
670
|
end)
|
633
671
|
end
|
|
@@ -687,7 +725,7 @@ defmodule Ecto.Repo.Schema do
|
687
725
|
defp conflict_target(conflict_target, dumper) do
|
688
726
|
for target <- List.wrap(conflict_target) do
|
689
727
|
case dumper do
|
690
|
- %{^target => {alias, _}} ->
|
728
|
+ %{^target => {alias, _, _}} ->
|
691
729
|
alias
|
692
730
|
%{} when is_atom(target) ->
|
693
731
|
raise ArgumentError, "unknown field `#{inspect(target)}` in conflict_target"
|
|
@@ -697,7 +735,7 @@ defmodule Ecto.Repo.Schema do
|
697
735
|
end
|
698
736
|
end
|
699
737
|
|
700
|
- defp on_conflict(on_conflict, conflict_target, schema_meta, counter_fun, adapter) do
|
738
|
+ defp on_conflict(on_conflict, conflict_target, schema_meta, counter_fun, dumper, adapter) do
|
701
739
|
%{source: source, schema: schema, prefix: prefix} = schema_meta
|
702
740
|
|
703
741
|
case on_conflict do
|
|
@@ -710,12 +748,18 @@ defmodule Ecto.Repo.Schema do
|
710
748
|
:nothing ->
|
711
749
|
{{:nothing, [], conflict_target}, []}
|
712
750
|
|
751
|
+ {:replace, []} ->
|
752
|
+ raise ArgumentError, ":on_conflict option with `{:replace, fields}` requires a non-empty list of fields"
|
753
|
+
|
713
754
|
{:replace, keys} when is_list(keys) ->
|
714
|
- fields = Enum.map(keys, &field_source!(schema, &1))
|
715
|
- {{fields, [], conflict_target}, []}
|
755
|
+ {{replace_fields!(dumper, keys), [], conflict_target}, []}
|
716
756
|
|
717
757
|
:replace_all ->
|
718
|
- {{replace_all_fields!(:replace_all, schema, []), [], conflict_target}, []}
|
758
|
+ # Remove the conflict targets from the replacing fields
|
759
|
+ # since the values don't change and this allows postgres to
|
760
|
+ # possibly perform a HOT optimization: https://www.postgresql.org/docs/current/storage-hot.html
|
761
|
+ to_remove = List.wrap(conflict_target)
|
762
|
+ {{replace_all_fields!(:replace_all, schema, to_remove), [], conflict_target}, []}
|
719
763
|
|
720
764
|
{:replace_all_except, fields} ->
|
721
765
|
{{replace_all_fields!(:replace_all_except, schema, fields), [], conflict_target}, []}
|
|
@@ -733,12 +777,27 @@ defmodule Ecto.Repo.Schema do
|
733
777
|
end
|
734
778
|
end
|
735
779
|
|
780
|
+ defp replace_fields!(nil, fields), do: fields
|
781
|
+
|
782
|
+ defp replace_fields!(dumper, fields) do
|
783
|
+ Enum.map(fields, fn field ->
|
784
|
+ case dumper do
|
785
|
+ %{^field => {source, _type, :always}} ->
|
786
|
+ source
|
787
|
+
|
788
|
+ _ ->
|
789
|
+ raise ArgumentError,
|
790
|
+ "cannot replace non-updatable field `#{inspect(field)}` in :on_conflict option"
|
791
|
+ end
|
792
|
+ end)
|
793
|
+ end
|
794
|
+
|
736
795
|
defp replace_all_fields!(kind, nil, _to_remove) do
|
737
796
|
raise ArgumentError, "cannot use #{inspect(kind)} on operations without a schema"
|
738
797
|
end
|
739
798
|
|
740
799
|
defp replace_all_fields!(_kind, schema, to_remove) do
|
741
|
- Enum.map(schema.__schema__(:fields) -- to_remove, &field_source!(schema, &1))
|
800
|
+ Enum.map(schema.__schema__(:updatable_fields) -- to_remove, &field_source!(schema, &1))
|
742
801
|
end
|
743
802
|
|
744
803
|
defp field_source!(nil, field) do
|
|
@@ -782,14 +841,18 @@ defmodule Ecto.Repo.Schema do
|
782
841
|
{:error, :stale} ->
|
783
842
|
opts = List.last(args)
|
784
843
|
|
785
|
- case Keyword.fetch(opts, :stale_error_field) do
|
786
|
- {:ok, stale_error_field} when is_atom(stale_error_field) ->
|
787
|
- stale_message = Keyword.get(opts, :stale_error_message, "is stale")
|
788
|
- user_changeset = Changeset.add_error(user_changeset, stale_error_field, stale_message, [stale: true])
|
789
|
- {:error, user_changeset}
|
844
|
+ if Keyword.get(opts, :allow_stale, false) do
|
845
|
+ {:ok, []}
|
846
|
+ else
|
847
|
+ case Keyword.fetch(opts, :stale_error_field) do
|
848
|
+ {:ok, stale_error_field} when is_atom(stale_error_field) ->
|
849
|
+ stale_message = Keyword.get(opts, :stale_error_message, "is stale")
|
850
|
+ user_changeset = Changeset.add_error(user_changeset, stale_error_field, stale_message, [stale: true])
|
851
|
+ {:error, user_changeset}
|
790
852
|
|
791
|
- _other ->
|
792
|
- raise Ecto.StaleEntryError, changeset: user_changeset, action: action
|
853
|
+ _other ->
|
854
|
+ raise Ecto.StaleEntryError, changeset: user_changeset, action: action
|
855
|
+ end
|
793
856
|
end
|
794
857
|
end
|
795
858
|
end
|
|
@@ -1054,7 +1117,7 @@ defmodule Ecto.Repo.Schema do
|
1054
1117
|
|
1055
1118
|
defp dump_fields!(action, schema, kw, dumper, adapter) do
|
1056
1119
|
for {field, value} <- kw do
|
1057
|
- {alias, type} = Map.fetch!(dumper, field)
|
1120
|
+ {alias, type, _writable} = Map.fetch!(dumper, field)
|
1058
1121
|
{alias, dump_field!(action, schema, field, type, value, adapter)}
|
1059
1122
|
end
|
1060
1123
|
end
|
changed
lib/ecto/repo/supervisor.ex
|
@@ -17,7 +17,7 @@ defmodule Ecto.Repo.Supervisor do
|
17
17
|
@doc """
|
18
18
|
Retrieves the runtime configuration.
|
19
19
|
"""
|
20
|
- def runtime_config(type, repo, otp_app, opts) do
|
20
|
+ def init_config(type, repo, otp_app, opts) do
|
21
21
|
config = Application.get_env(otp_app, repo, [])
|
22
22
|
config = [otp_app: otp_app] ++ (@defaults |> Keyword.merge(config) |> Keyword.merge(opts))
|
23
23
|
config = Keyword.put_new_lazy(config, :telemetry_prefix, fn -> telemetry_prefix(repo) end)
|
|
@@ -35,7 +35,7 @@ defmodule Ecto.Repo.Supervisor do
|
35
35
|
defp telemetry_prefix(repo) do
|
36
36
|
repo
|
37
37
|
|> Module.split()
|
38
|
- |> Enum.map(& &1 |> Macro.underscore() |> String.to_atom())
|
38
|
+ |> Enum.map(&(&1 |> Macro.underscore() |> String.to_atom()))
|
39
39
|
end
|
40
40
|
|
41
41
|
defp repo_init(type, repo, config) do
|
|
@@ -58,8 +58,9 @@ defmodule Ecto.Repo.Supervisor do
|
58
58
|
end
|
59
59
|
|
60
60
|
if Code.ensure_compiled(adapter) != {:module, adapter} do
|
61
|
- raise ArgumentError, "adapter #{inspect adapter} was not compiled, " <>
|
62
|
- "ensure it is correct and it is included as a project dependency"
|
61
|
+ raise ArgumentError,
|
62
|
+ "adapter #{inspect(adapter)} was not compiled, " <>
|
63
|
+ "ensure it is correct and it is included as a project dependency"
|
63
64
|
end
|
64
65
|
|
65
66
|
behaviours =
|
|
@@ -125,6 +126,7 @@ defmodule Ecto.Repo.Supervisor do
|
125
126
|
|
126
127
|
defp parse_uri_query(%URI{query: nil}),
|
127
128
|
do: []
|
129
|
+
|
128
130
|
defp parse_uri_query(%URI{query: query} = url) do
|
129
131
|
query
|
130
132
|
|> URI.query_decoder()
|
|
@@ -150,8 +152,8 @@ defmodule Ecto.Repo.Supervisor do
|
150
152
|
|
151
153
|
_ ->
|
152
154
|
raise Ecto.InvalidURLError,
|
153
|
- url: url,
|
154
|
- message: "can not parse value `#{value}` for parameter `#{key}` as an integer"
|
155
|
+ url: url,
|
156
|
+ message: "cannot parse value `#{value}` for parameter `#{key}` as an integer"
|
155
157
|
end
|
156
158
|
end
|
157
159
|
|
|
@@ -174,7 +176,7 @@ defmodule Ecto.Repo.Supervisor do
|
174
176
|
# Normalize name to atom, ignore via/global names
|
175
177
|
name = if is_atom(name), do: name, else: nil
|
176
178
|
|
177
|
- case runtime_config(:supervisor, repo, otp_app, opts) do
|
179
|
+ case init_config(:supervisor, repo, otp_app, opts) do
|
178
180
|
{:ok, opts} ->
|
179
181
|
:telemetry.execute(
|
180
182
|
[:ecto, :repo, :init],
|
changed
lib/ecto/schema.ex
|
@@ -47,9 +47,10 @@ defmodule Ecto.Schema do
|
47
47
|
However, most commonly, structs are cast, validated and manipulated with the
|
48
48
|
`Ecto.Changeset` module.
|
49
49
|
|
50
|
- Note that the name of the database table does not need to correlate to your
|
51
|
- module name. For example, if you are working with a legacy database, you can
|
52
|
- reference the table name when you define your schema:
|
50
|
+ The first argument of `schema/2` is the name of database's table, which does
|
51
|
+ not need to correlate to your module name (commonly referred to as the schema/schema name).
|
52
|
+ For example, if you are working with a legacy database, you can reference the table name
|
53
|
+ (`legacy_users`) when you define your schema (`User`):
|
53
54
|
|
54
55
|
defmodule User do
|
55
56
|
use Ecto.Schema
|
|
@@ -255,6 +256,7 @@ defmodule Ecto.Schema do
|
255
256
|
`:boolean` | `boolean` | true, false
|
256
257
|
`:string` | UTF-8 encoded `string` | "hello"
|
257
258
|
`:binary` | `binary` | `<<int, int, int, ...>>`
|
259
|
+ `:bitstring` | `bitstring` | `<<_::size>>
|
258
260
|
`{:array, inner_type}` | `list` | `[value, value, value, ...]`
|
259
261
|
`:map` | `map` |
|
260
262
|
`{:map, inner_type}` | `map` |
|
|
@@ -266,6 +268,7 @@ defmodule Ecto.Schema do
|
266
268
|
`:naive_datetime_usec` | `NaiveDateTime` |
|
267
269
|
`:utc_datetime` | `DateTime` |
|
268
270
|
`:utc_datetime_usec` | `DateTime` |
|
271
|
+ `:duration` | `Duration` |
|
269
272
|
|
270
273
|
**Notes:**
|
271
274
|
|
|
@@ -292,6 +295,10 @@ defmodule Ecto.Schema do
|
292
295
|
where casting has to be done explicitly and is never performed
|
293
296
|
implicitly when loading from or dumping to the database.
|
294
297
|
|
298
|
+ * For `the `:duration` type, you may need to enable `Duration` support in
|
299
|
+ your adapter. For information on how to enable it in Postgrex, see their
|
300
|
+ [HexDocs page](https://hexdocs.pm/postgrex/readme.html#data-representation).
|
301
|
+
|
295
302
|
### Custom types
|
296
303
|
|
297
304
|
Besides providing primitive types, Ecto allows custom types to be
|
|
@@ -437,7 +444,7 @@ defmodule Ecto.Schema do
|
437
444
|
* `__schema__(:embed, embed)` - Returns the embedding reflection of the given embed;
|
438
445
|
|
439
446
|
* `__schema__(:read_after_writes)` - Non-virtual fields that must be read back
|
440
|
- from the database after every write (insert or update);
|
447
|
+ from the database after every write (insert, update, and delete);
|
441
448
|
|
442
449
|
* `__schema__(:autogenerate_id)` - Primary key that is auto generated on insert;
|
443
450
|
* `__schema__(:autogenerate_fields)` - Returns a list of fields names that are auto
|
|
@@ -525,7 +532,8 @@ defmodule Ecto.Schema do
|
525
532
|
:type,
|
526
533
|
:where,
|
527
534
|
:references,
|
528
|
- :skip_default_validation
|
535
|
+ :skip_default_validation,
|
536
|
+ :writable
|
529
537
|
]
|
530
538
|
|
531
539
|
@doc """
|
|
@@ -539,6 +547,16 @@ defmodule Ecto.Schema do
|
539
547
|
Embedded schemas by default set the primary key type
|
540
548
|
to `:binary_id` but such can be configured with the
|
541
549
|
`@primary_key` attribute.
|
550
|
+
|
551
|
+ `belongs_to/3` associations may be defined inside of
|
552
|
+ embedded schemas. However, they are essentially read-only.
|
553
|
+ This means you may preload the associations but you may
|
554
|
+ not modify them by using `Ecto.Changeset.cast_assoc/3`
|
555
|
+ or `Ecto.Changeset.put_assoc/4`. If you would like to
|
556
|
+ modify the associations of an embedded schema, you must
|
557
|
+ change them independently. Associations nested inside of
|
558
|
+ embedded schemas will also not be persisted to the database
|
559
|
+ when calling `c:Ecto.Repo.insert/2` or `c:Ecto.Repo.update/2`.
|
542
560
|
"""
|
543
561
|
defmacro embedded_schema(do: block) do
|
544
562
|
schema(__CALLER__, nil, false, :binary_id, block)
|
|
@@ -658,23 +676,39 @@ defmodule Ecto.Schema do
|
658
676
|
def __schema__(:redact_fields), do: unquote(redacted_fields)
|
659
677
|
def __schema__(:virtual_fields), do: unquote(Enum.map(virtual_fields, &elem(&1, 0)))
|
660
678
|
|
679
|
+ def __schema__(:updatable_fields),
|
680
|
+ do: unquote(for {name, {_, :always}} <- fields, do: name)
|
681
|
+
|
682
|
+ def __schema__(:insertable_fields),
|
683
|
+ do: unquote(for {name, {_, writable}} when writable != :never <- fields, do: name)
|
684
|
+
|
661
685
|
def __schema__(:autogenerate_fields),
|
662
686
|
do: unquote(Enum.flat_map(autogenerate, &elem(&1, 0)))
|
663
687
|
|
664
|
- def __schema__(:query) do
|
665
|
- %Ecto.Query{
|
666
|
- from: %Ecto.Query.FromExpr{
|
667
|
- source: {unquote(source), __MODULE__},
|
668
|
- prefix: unquote(prefix)
|
688
|
+ if meta? do
|
689
|
+ def __schema__(:query) do
|
690
|
+ %Ecto.Query{
|
691
|
+ from: %Ecto.Query.FromExpr{
|
692
|
+ source: {unquote(source), __MODULE__},
|
693
|
+ prefix: unquote(prefix)
|
694
|
+ }
|
669
695
|
}
|
670
|
- }
|
696
|
+ end
|
671
697
|
end
|
672
698
|
|
673
699
|
for clauses <-
|
674
|
- Ecto.Schema.__schema__(fields, field_sources, assocs, embeds, virtual_fields),
|
700
|
+ Ecto.Schema.__schema__(
|
701
|
+ fields,
|
702
|
+ field_sources,
|
703
|
+ assocs,
|
704
|
+ embeds,
|
705
|
+ virtual_fields
|
706
|
+ ),
|
675
707
|
{args, body} <- clauses do
|
676
708
|
def __schema__(unquote_splicing(args)), do: unquote(body)
|
677
709
|
end
|
710
|
+
|
711
|
+ :ok
|
678
712
|
end
|
679
713
|
|
680
714
|
quote do
|
|
@@ -703,7 +737,7 @@ defmodule Ecto.Schema do
|
703
737
|
|
704
738
|
The default value is validated against the field's type at compilation time
|
705
739
|
and it will raise an ArgumentError if there is a type mismatch. If you cannot
|
706
|
- infer the field's type at compilation time, you can use the
|
740
|
+ infer the field's type at compilation time, you can use the
|
707
741
|
`:skip_default_validation` option on the field to skip validations.
|
708
742
|
|
709
743
|
Once a default value is set, if you send changes to the changeset that
|
|
@@ -721,7 +755,7 @@ defmodule Ecto.Schema do
|
721
755
|
A shorthand value of `true` is equivalent to `{type, :autogenerate, []}`.
|
722
756
|
|
723
757
|
* `:read_after_writes` - When true, the field is always read back
|
724
|
- from the database after insert and updates.
|
758
|
+ from the database after inserts, updates, and deletes.
|
725
759
|
|
726
760
|
For relational databases, this means the RETURNING option of those
|
727
761
|
statements is used. For this reason, MySQL does not support this
|
|
@@ -746,6 +780,12 @@ defmodule Ecto.Schema do
|
746
780
|
* `:skip_default_validation` - When true, it will skip the type validation
|
747
781
|
step at compile time.
|
748
782
|
|
783
|
+ * `:writable` - Defines when a field is allowed to be modified. Must be one of
|
784
|
+ `:always`, `:insert`, or `:never`. If set to `:always`, the field can be modified
|
785
|
+ by any repo operation. If set to `:insert`, the field can be inserted but cannot
|
786
|
+ be further modified, even in an upsert. If set to `:never`, the field becomes
|
787
|
+ read only. Defaults to `:always`.
|
788
|
+
|
749
789
|
"""
|
750
790
|
defmacro field(name, type \\ :string, opts \\ []) do
|
751
791
|
quote do
|
|
@@ -793,7 +833,8 @@ defmodule Ecto.Schema do
|
793
833
|
association, defaults to the primary key on the schema
|
794
834
|
|
795
835
|
* `:through` - Allow this association to be defined in terms of existing
|
796
|
- associations. Read the section on `:through` associations for more info
|
836
|
+ associations. Read the [section on `:through` associations](#has_many/3-has_many-has_one-through)
|
837
|
+ for more info
|
797
838
|
|
798
839
|
* `:on_delete` - The action taken on associations when parent record
|
799
840
|
is deleted. May be `:nothing` (default), `:nilify_all` and `:delete_all`.
|
|
@@ -826,12 +867,13 @@ defmodule Ecto.Schema do
|
826
867
|
* `:where` - A filter for the association. See "Filtering associations" below.
|
827
868
|
It does not apply to `:through` associations.
|
828
869
|
|
829
|
- * `:preload_order` - Sets the default `order_by` of the association.
|
830
|
- It is used when the association is preloaded.
|
870
|
+ * `:preload_order` - Sets the default `order_by` when preloading the association.
|
871
|
+ It may be a keyword list/list of fields or an MFA tuple, such as `{Mod, fun, []}`.
|
872
|
+ Both cases must resolve to a valid `order_by` expression.
|
831
873
|
For example, if you set `Post.has_many :comments, preload_order: [asc: :content]`,
|
832
874
|
whenever the `:comments` associations is preloaded,
|
833
|
- the comments will be order by the `:content` field.
|
834
|
- See `Ecto.Query.order_by/3` for more examples.
|
875
|
+ the comments will be ordered by the `:content` field.
|
876
|
+ See `Ecto.Query.order_by/3` to learn more about ordering expressions.
|
835
877
|
|
836
878
|
## Examples
|
837
879
|
|
|
@@ -1003,11 +1045,12 @@ defmodule Ecto.Schema do
|
1003
1045
|
Note `:through` associations are read-only. For example, you cannot use
|
1004
1046
|
`Ecto.Changeset.cast_assoc/3` to modify through associations.
|
1005
1047
|
"""
|
1006
|
- defmacro has_many(name, queryable, opts \\ []) do
|
1007
|
- queryable = expand_literals(queryable, __CALLER__)
|
1048
|
+ defmacro has_many(name, schema, opts \\ []) do
|
1049
|
+ schema = expand_literals(schema, __CALLER__)
|
1050
|
+ opts = expand_literals(opts, __CALLER__)
|
1008
1051
|
|
1009
1052
|
quote do
|
1010
|
- Ecto.Schema.__has_many__(__MODULE__, unquote(name), unquote(queryable), unquote(opts))
|
1053
|
+ Ecto.Schema.__has_many__(__MODULE__, unquote(name), unquote(schema), unquote(opts))
|
1011
1054
|
end
|
1012
1055
|
end
|
1013
1056
|
|
|
@@ -1079,11 +1122,11 @@ defmodule Ecto.Schema do
|
1079
1122
|
[post] = Repo.all(from(p in Post, where: p.id == 42, preload: :permalink))
|
1080
1123
|
post.permalink #=> %Permalink{...}
|
1081
1124
|
"""
|
1082
|
- defmacro has_one(name, queryable, opts \\ []) do
|
1083
|
- queryable = expand_literals(queryable, __CALLER__)
|
1125
|
+ defmacro has_one(name, schema, opts \\ []) do
|
1126
|
+ schema = expand_literals(schema, __CALLER__)
|
1084
1127
|
|
1085
1128
|
quote do
|
1086
|
- Ecto.Schema.__has_one__(__MODULE__, unquote(name), unquote(queryable), unquote(opts))
|
1129
|
+ Ecto.Schema.__has_one__(__MODULE__, unquote(name), unquote(schema), unquote(opts))
|
1087
1130
|
end
|
1088
1131
|
end
|
1089
1132
|
|
|
@@ -1286,11 +1329,11 @@ defmodule Ecto.Schema do
|
1286
1329
|
|
1287
1330
|
See `many_to_many/3` for more information on this particular approach.
|
1288
1331
|
"""
|
1289
|
- defmacro belongs_to(name, queryable, opts \\ []) do
|
1290
|
- queryable = expand_literals(queryable, __CALLER__)
|
1332
|
+ defmacro belongs_to(name, schema, opts \\ []) do
|
1333
|
+ schema = expand_literals(schema, __CALLER__)
|
1291
1334
|
|
1292
1335
|
quote do
|
1293
|
- Ecto.Schema.__belongs_to__(__MODULE__, unquote(name), unquote(queryable), unquote(opts))
|
1336
|
+ Ecto.Schema.__belongs_to__(__MODULE__, unquote(name), unquote(schema), unquote(opts))
|
1294
1337
|
end
|
1295
1338
|
end
|
1296
1339
|
|
|
@@ -1375,7 +1418,7 @@ defmodule Ecto.Schema do
|
1375
1418
|
* `:preload_order` - Sets the default `order_by` when preloading the association.
|
1376
1419
|
It may be a keyword list/list of fields or an MFA tuple, such as `{Mod, fun, []}`.
|
1377
1420
|
Both cases must resolve to a valid `order_by` expression. See `Ecto.Query.order_by/3`
|
1378
|
- to learn more about about ordering expressions.
|
1421
|
+ to learn more about ordering expressions.
|
1379
1422
|
See the [preload order](#many_to_many/3-preload-order) section below to learn how
|
1380
1423
|
this option can be utilized
|
1381
1424
|
|
|
@@ -1582,12 +1625,12 @@ defmodule Ecto.Schema do
|
1582
1625
|
{:error, changeset} -> # Handle the error
|
1583
1626
|
end
|
1584
1627
|
"""
|
1585
|
- defmacro many_to_many(name, queryable, opts \\ []) do
|
1586
|
- queryable = expand_literals(queryable, __CALLER__)
|
1628
|
+ defmacro many_to_many(name, schema, opts \\ []) do
|
1629
|
+ schema = expand_literals(schema, __CALLER__)
|
1587
1630
|
opts = expand_literals(opts, __CALLER__)
|
1588
1631
|
|
1589
1632
|
quote do
|
1590
|
- Ecto.Schema.__many_to_many__(__MODULE__, unquote(name), unquote(queryable), unquote(opts))
|
1633
|
+ Ecto.Schema.__many_to_many__(__MODULE__, unquote(name), unquote(schema), unquote(opts))
|
1591
1634
|
end
|
1592
1635
|
end
|
1593
1636
|
|
|
@@ -1612,8 +1655,8 @@ defmodule Ecto.Schema do
|
1612
1655
|
|
1613
1656
|
* `:primary_key` - The `:primary_key` option can be used with the same arguments
|
1614
1657
|
as `@primary_key` (see the [Schema attributes](https://hexdocs.pm/ecto/Ecto.Schema.html#module-schema-attributes)
|
1615
|
- section for more info). Primary keys are automatically set up for embedded schemas as well,
|
1616
|
- defaulting to `{:id, :binary_id, autogenerate: true}`.
|
1658
|
+ section for more info). Primary keys are automatically set up for embedded schemas as well,
|
1659
|
+ defaulting to `{:id, :binary_id, autogenerate: true}`.
|
1617
1660
|
|
1618
1661
|
* `:on_replace` - The action taken on associations when the embed is
|
1619
1662
|
replaced when casting or manipulating parent changeset. May be
|
|
@@ -1628,6 +1671,12 @@ defmodule Ecto.Schema do
|
1628
1671
|
selecting the whole struct in a query, such as `from p in Post, select: p`.
|
1629
1672
|
Defaults to `true`.
|
1630
1673
|
|
1674
|
+ * `:defaults_to_struct` - When true, the field will default to the initialized
|
1675
|
+ struct instead of nil, the same you would get from something like `%Order.Item{}`.
|
1676
|
+ One important thing is that if the underlying data is explicitly nil when loading
|
1677
|
+ the schema, it will still be loaded as nil, similar to how `:default` works in fields.
|
1678
|
+ Defaults to `false`.
|
1679
|
+
|
1631
1680
|
## Examples
|
1632
1681
|
|
1633
1682
|
defmodule Order do
|
|
@@ -1725,7 +1774,7 @@ defmodule Ecto.Schema do
|
1725
1774
|
Ecto provides this guarantee for all built-in types.
|
1726
1775
|
|
1727
1776
|
When decoding, if a key exists in the database not defined in the
|
1728
|
- schema, it'll be ignored. If a field exists in the schema thats not
|
1777
|
+ schema, it'll be ignored. If a field exists in the schema that's not
|
1729
1778
|
in the database, it's value will be `nil`.
|
1730
1779
|
"""
|
1731
1780
|
defmacro embeds_one(name, schema, opts \\ [])
|
|
@@ -1777,10 +1826,10 @@ defmodule Ecto.Schema do
|
1777
1826
|
maps are represented as JSON which allows Ecto to choose what works best).
|
1778
1827
|
|
1779
1828
|
The embedded may or may not have a primary key. Ecto uses the primary keys
|
1780
|
- to detect if an embed is being updated or not. If a primary is not present
|
1781
|
- and you still want the list of embeds to be updated, `:on_replace` must be
|
1782
|
- set to `:delete`, forcing all current embeds to be deleted and replaced by
|
1783
|
- new ones whenever a new list of embeds is set.
|
1829
|
+ to detect if an embed is being updated or not. If a primary key is not
|
1830
|
+ present and you still want the list of embeds to be updated, `:on_replace`
|
1831
|
+ must be set to `:delete`, forcing all current embeds to be deleted and
|
1832
|
+ replaced by new ones whenever a new list of embeds is set.
|
1784
1833
|
|
1785
1834
|
For encoding and decoding of embeds, please read the docs for
|
1786
1835
|
`embeds_one/3`.
|
|
@@ -1885,8 +1934,8 @@ defmodule Ecto.Schema do
|
1885
1934
|
end
|
1886
1935
|
end
|
1887
1936
|
|
1888
|
- Primary keys are automatically set up for embedded schemas as well,
|
1889
|
- defaulting to `{:id, :binary_id, autogenerate: true}`. You can
|
1937
|
+ Primary keys are automatically set up for embedded schemas as well,
|
1938
|
+ defaulting to `{:id, :binary_id, autogenerate: true}`. You can
|
1890
1939
|
customize it by passing a `:primary_key` option with the same arguments
|
1891
1940
|
as `@primary_key` (see the [Schema attributes](https://hexdocs.pm/ecto/Ecto.Schema.html#module-schema-attributes)
|
1892
1941
|
section for more info).
|
|
@@ -2023,6 +2072,7 @@ defmodule Ecto.Schema do
|
2023
2072
|
defp define_field(mod, name, type, opts) do
|
2024
2073
|
virtual? = opts[:virtual] || false
|
2025
2074
|
pk? = opts[:primary_key] || false
|
2075
|
+ writable = opts[:writable] || :always
|
2026
2076
|
put_struct_field(mod, name, Keyword.get(opts, :default))
|
2027
2077
|
|
2028
2078
|
if Keyword.get(opts, :redact, false) do
|
|
@@ -2062,6 +2112,10 @@ defmodule Ecto.Schema do
|
2062
2112
|
raise ArgumentError, "cannot mark the same field as autogenerate and read_after_writes"
|
2063
2113
|
end
|
2064
2114
|
|
2115
|
+ if writable != :always && gen do
|
2116
|
+ raise ArgumentError, "autogenerated fields must always be writable"
|
2117
|
+ end
|
2118
|
+
|
2065
2119
|
if pk? do
|
2066
2120
|
Module.put_attribute(mod, :ecto_primary_keys, name)
|
2067
2121
|
end
|
|
@@ -2070,7 +2124,7 @@ defmodule Ecto.Schema do
|
2070
2124
|
Module.put_attribute(mod, :ecto_query_fields, {name, type})
|
2071
2125
|
end
|
2072
2126
|
|
2073
|
- Module.put_attribute(mod, :ecto_fields, {name, type})
|
2127
|
+ Module.put_attribute(mod, :ecto_fields, {name, {type, writable}})
|
2074
2128
|
end
|
2075
2129
|
end
|
2076
2130
|
|
|
@@ -2197,11 +2251,19 @@ defmodule Ecto.Schema do
|
2197
2251
|
Module.put_attribute(mod, :ecto_changeset_fields, {name, {:assoc, struct}})
|
2198
2252
|
end
|
2199
2253
|
|
2200
|
- @valid_embeds_one_options [:on_replace, :source, :load_in_query]
|
2254
|
+ @valid_embeds_one_options [:on_replace, :source, :load_in_query, :defaults_to_struct]
|
2201
2255
|
|
2202
2256
|
@doc false
|
2203
2257
|
def __embeds_one__(mod, name, schema, opts) when is_atom(schema) do
|
2204
2258
|
check_options!(opts, @valid_embeds_one_options, "embeds_one/3")
|
2259
|
+
|
2260
|
+ opts =
|
2261
|
+ if Keyword.get(opts, :defaults_to_struct) do
|
2262
|
+ Keyword.put(opts, :default, schema.__schema__(:loaded))
|
2263
|
+ else
|
2264
|
+ opts
|
2265
|
+ end
|
2266
|
+
|
2205
2267
|
embed(mod, :one, name, schema, opts)
|
2206
2268
|
end
|
2207
2269
|
|
|
@@ -2271,7 +2333,7 @@ defmodule Ecto.Schema do
|
2271
2333
|
@doc false
|
2272
2334
|
def __schema__(fields, field_sources, assocs, embeds, virtual_fields) do
|
2273
2335
|
load =
|
2274
|
- for {name, type} <- fields do
|
2336
|
+ for {name, {type, _writable}} <- fields do
|
2275
2337
|
if alias = field_sources[name] do
|
2276
2338
|
{name, {:source, alias, type}}
|
2277
2339
|
else
|
|
@@ -2280,17 +2342,17 @@ defmodule Ecto.Schema do
|
2280
2342
|
end
|
2281
2343
|
|
2282
2344
|
dump =
|
2283
|
- for {name, type} <- fields do
|
2284
|
- {name, {field_sources[name] || name, type}}
|
2345
|
+ for {name, {type, writable}} <- fields do
|
2346
|
+ {name, {field_sources[name] || name, type, writable}}
|
2285
2347
|
end
|
2286
2348
|
|
2287
2349
|
field_sources_quoted =
|
2288
|
- for {name, _type} <- fields do
|
2350
|
+ for {name, {_type, _writable}} <- fields do
|
2289
2351
|
{[:field_source, name], field_sources[name] || name}
|
2290
2352
|
end
|
2291
2353
|
|
2292
2354
|
types_quoted =
|
2293
|
- for {name, type} <- fields do
|
2355
|
+ for {name, {type, _writable}} <- fields do
|
2294
2356
|
{[:type, name], Macro.escape(type)}
|
2295
2357
|
end
|
2296
2358
|
|
|
@@ -2347,7 +2409,7 @@ defmodule Ecto.Schema do
|
2347
2409
|
|
2348
2410
|
Module.put_attribute(mod, :ecto_changeset_fields, {name, {:embed, struct}})
|
2349
2411
|
Module.put_attribute(mod, :ecto_embeds, {name, struct})
|
2350
|
- define_field(mod, name, {:parameterized, Ecto.Embedded, struct}, opts)
|
2412
|
+ define_field(mod, name, {:parameterized, {Ecto.Embedded, struct}}, opts)
|
2351
2413
|
end
|
2352
2414
|
|
2353
2415
|
defp put_struct_field(mod, name, assoc) do
|
|
@@ -2381,7 +2443,7 @@ defmodule Ecto.Schema do
|
2381
2443
|
end
|
2382
2444
|
end
|
2383
2445
|
|
2384
|
- defp check_options!({:parameterized, _, _}, _opts, _valid, _fun_arity) do
|
2446
|
+ defp check_options!({:parameterized, _}, _opts, _valid, _fun_arity) do
|
2385
2447
|
:ok
|
2386
2448
|
end
|
2387
2449
|
|
|
@@ -2455,7 +2517,9 @@ defmodule Ecto.Schema do
|
2455
2517
|
Module.put_attribute(mod, :ecto_autogenerate, {[name], mfa})
|
2456
2518
|
end
|
2457
2519
|
|
2458
|
- defp store_type_autogenerate!(mod, name, source, {:parameterized, typemod, params} = type, pk?) do
|
2520
|
+ defp store_type_autogenerate!(mod, name, source, {:parameterized, typemod_params} = type, pk?) do
|
2521
|
+ {typemod, params} = typemod_params
|
2522
|
+
|
2459
2523
|
cond do
|
2460
2524
|
store_autogenerate_id!(mod, name, source, type, pk?) ->
|
2461
2525
|
:ok
|
changed
lib/ecto/schema/loader.ex
|
@@ -24,7 +24,7 @@ defmodule Ecto.Schema.Loader do
|
24
24
|
@doc """
|
25
25
|
Loads data coming from the user/embeds into schema.
|
26
26
|
|
27
|
- Assumes data does not all belongs to schema/struct
|
27
|
+ Assumes data does not all belong to schema/struct
|
28
28
|
and that it may also require source-based renaming.
|
29
29
|
"""
|
30
30
|
def unsafe_load(schema, data, loader) do
|
|
@@ -36,7 +36,7 @@ defmodule Ecto.Schema.Loader do
|
36
36
|
@doc """
|
37
37
|
Loads data coming from the user/embeds into struct and types.
|
38
38
|
|
39
|
- Assumes data does not all belongs to schema/struct
|
39
|
+ Assumes data does not all belong to schema/struct
|
40
40
|
and that it may also require source-based renaming.
|
41
41
|
"""
|
42
42
|
def unsafe_load(struct, types, map, loader) when is_map(map) do
|
|
@@ -91,7 +91,7 @@ defmodule Ecto.Schema.Loader do
|
91
91
|
Dumps the given data.
|
92
92
|
"""
|
93
93
|
def safe_dump(struct, types, dumper) do
|
94
|
- Enum.reduce(types, %{}, fn {field, {source, type}}, acc ->
|
94
|
+ Enum.reduce(types, %{}, fn {field, {source, type, _writable}}, acc ->
|
95
95
|
value = Map.get(struct, field)
|
96
96
|
|
97
97
|
case dumper.(type, value) do
|
changed
lib/ecto/type.ex
|
@@ -15,6 +15,27 @@ defmodule Ecto.Type do
|
15
15
|
basic custom types and rely on parameterized types if you need
|
16
16
|
the extra functionality.
|
17
17
|
|
18
|
+ ## External vs internal vs database representation
|
19
|
+
|
20
|
+ The core functionality of a custom type is the mapping between
|
21
|
+ external, internal and database representations of a value belonging
|
22
|
+ to the type.
|
23
|
+
|
24
|
+ For a definition of external and internal data take a look at the
|
25
|
+ [related section](`Ecto.Changeset#module-external-vs-internal-data`)
|
26
|
+ in the changeset documentation.
|
27
|
+
|
28
|
+ ```mermaid
|
29
|
+ stateDiagram-v2
|
30
|
+ external: External Data
|
31
|
+ internal: Internal Data
|
32
|
+ database: Database Data
|
33
|
+ external --> internal: cast/1
|
34
|
+ external --> database: dump/1
|
35
|
+ internal --> database: dump/1
|
36
|
+ database --> internal: load/1
|
37
|
+ ```
|
38
|
+
|
18
39
|
## Example
|
19
40
|
|
20
41
|
Imagine you want to store a URI struct as part of a schema in a
|
|
@@ -180,13 +201,14 @@ defmodule Ecto.Type do
|
180
201
|
@type primitive :: base | composite
|
181
202
|
|
182
203
|
@typedoc "Custom types are represented by user-defined modules."
|
183
|
- @type custom :: module | {:parameterized, module, term}
|
204
|
+ @type custom :: module | {:parameterized, {module, term}}
|
184
205
|
|
185
206
|
@type base ::
|
186
207
|
:integer
|
187
208
|
| :float
|
188
209
|
| :boolean
|
189
210
|
| :string
|
211
|
+ | :bitstring
|
190
212
|
| :map
|
191
213
|
| :binary
|
192
214
|
| :decimal
|
|
@@ -200,17 +222,19 @@ defmodule Ecto.Type do
|
200
222
|
| :utc_datetime_usec
|
201
223
|
| :naive_datetime_usec
|
202
224
|
| :time_usec
|
225
|
+ | :duration
|
203
226
|
|
204
227
|
@type composite :: {:array, t} | {:map, t} | private_composite
|
205
228
|
|
206
|
- @typep private_composite :: {:maybe, t} | {:in, t} | {:param, :any_datetime}
|
229
|
+ @typep private_composite :: {:try, t} | {:in, t} | {:supertype, :datetime}
|
207
230
|
|
208
231
|
@base ~w(
|
209
|
- integer float decimal boolean string map binary id binary_id any
|
232
|
+ integer float decimal boolean string bitstring map binary id binary_id any
|
210
233
|
utc_datetime naive_datetime date time
|
211
234
|
utc_datetime_usec naive_datetime_usec time_usec
|
235
|
+ duration
|
212
236
|
)a
|
213
|
- @composite ~w(array map maybe in param)a
|
237
|
+ @composite ~w(array map try in param)a
|
214
238
|
@variadic ~w(in splice)a
|
215
239
|
|
216
240
|
@doc """
|
|
@@ -314,7 +338,7 @@ defmodule Ecto.Type do
|
314
338
|
|
315
339
|
"""
|
316
340
|
@spec primitive?(t) :: boolean
|
317
|
- def primitive?({:parameterized, _, _}), do: true
|
341
|
+ def primitive?({:parameterized, _}), do: true
|
318
342
|
def primitive?({composite, _}) when composite in @composite, do: true
|
319
343
|
def primitive?(base) when base in @base, do: true
|
320
344
|
def primitive?(_), do: false
|
|
@@ -350,7 +374,7 @@ defmodule Ecto.Type do
|
350
374
|
|
351
375
|
See `c:embed_as/1`.
|
352
376
|
"""
|
353
|
- def embed_as({:parameterized, module, params}, format), do: module.embed_as(format, params)
|
377
|
+ def embed_as({:parameterized, {module, params}}, format), do: module.embed_as(format, params)
|
354
378
|
def embed_as({composite, type}, format) when composite in @composite, do: embed_as(type, format)
|
355
379
|
def embed_as(base, _format) when base in @base, do: :self
|
356
380
|
def embed_as(mod, format), do: mod.embed_as(format)
|
|
@@ -412,10 +436,10 @@ defmodule Ecto.Type do
|
412
436
|
"""
|
413
437
|
@spec type(t) :: t
|
414
438
|
def type(type)
|
415
|
- def type({:parameterized, type, params}), do: type.type(params)
|
439
|
+ def type({:parameterized, {type, params}}), do: type.type(params)
|
416
440
|
def type({:array, type}), do: {:array, type(type)}
|
417
441
|
def type({:map, type}), do: {:map, type(type)}
|
418
|
- def type({:maybe, type}), do: type(type)
|
442
|
+ def type({:try, type}), do: type(type)
|
419
443
|
def type(type) when type in @base, do: type
|
420
444
|
def type(type) when is_atom(type), do: type.type()
|
421
445
|
def type(type), do: type
|
|
@@ -456,10 +480,10 @@ defmodule Ecto.Type do
|
456
480
|
defp do_match?(:binary_id, :binary), do: true
|
457
481
|
defp do_match?(:id, :integer), do: true
|
458
482
|
defp do_match?(type, type), do: true
|
459
|
- defp do_match?(:naive_datetime, {:param, :any_datetime}), do: true
|
460
|
- defp do_match?(:naive_datetime_usec, {:param, :any_datetime}), do: true
|
461
|
- defp do_match?(:utc_datetime, {:param, :any_datetime}), do: true
|
462
|
- defp do_match?(:utc_datetime_usec, {:param, :any_datetime}), do: true
|
483
|
+ defp do_match?(:naive_datetime, {:supertype, :datetime}), do: true
|
484
|
+ defp do_match?(:naive_datetime_usec, {:supertype, :datetime}), do: true
|
485
|
+ defp do_match?(:utc_datetime, {:supertype, :datetime}), do: true
|
486
|
+ defp do_match?(:utc_datetime_usec, {:supertype, :datetime}), do: true
|
463
487
|
defp do_match?(_, _), do: false
|
464
488
|
|
465
489
|
@doc """
|
|
@@ -496,7 +520,7 @@ defmodule Ecto.Type do
|
496
520
|
@spec dump(t, term, (t, term -> {:ok, term} | :error)) :: {:ok, term} | :error
|
497
521
|
def dump(type, value, dumper \\ &dump/2)
|
498
522
|
|
499
|
- def dump({:parameterized, module, params}, value, dumper) do
|
523
|
+ def dump({:parameterized, {module, params}}, value, dumper) do
|
500
524
|
module.dump(value, dumper, params)
|
501
525
|
end
|
502
526
|
|
|
@@ -504,7 +528,7 @@ defmodule Ecto.Type do
|
504
528
|
{:ok, nil}
|
505
529
|
end
|
506
530
|
|
507
|
- def dump({:maybe, type}, value, dumper) do
|
531
|
+ def dump({:try, type}, value, dumper) do
|
508
532
|
case dump(type, value, dumper) do
|
509
533
|
{:ok, _} = ok -> ok
|
510
534
|
:error -> {:ok, value}
|
|
@@ -518,8 +542,10 @@ defmodule Ecto.Type do
|
518
542
|
end
|
519
543
|
end
|
520
544
|
|
521
|
- def dump({:array, {_, _, _} = type}, value, dumper), do: array(value, type, dumper, false, [])
|
522
|
- def dump({:array, type}, value, dumper), do: array(value, type, dumper, true, [])
|
545
|
+ def dump({:array, {:parameterized, _} = type}, value, dumper),
|
546
|
+ do: array_with_type(value, type, dumper, false, [])
|
547
|
+
|
548
|
+ def dump({:array, type}, value, dumper), do: array_with_type(value, type, dumper, true, [])
|
523
549
|
def dump({:map, type}, value, dumper), do: map(value, type, dumper, false, %{})
|
524
550
|
|
525
551
|
def dump(:any, value, _dumper), do: {:ok, value}
|
|
@@ -529,6 +555,7 @@ defmodule Ecto.Type do
|
529
555
|
def dump(:map, value, _dumper), do: same_map(value)
|
530
556
|
def dump(:string, value, _dumper), do: same_binary(value)
|
531
557
|
def dump(:binary, value, _dumper), do: same_binary(value)
|
558
|
+ def dump(:bitstring, value, _dumper), do: same_bitstring(value)
|
532
559
|
def dump(:id, value, _dumper), do: same_integer(value)
|
533
560
|
def dump(:binary_id, value, _dumper), do: same_binary(value)
|
534
561
|
def dump(:decimal, value, _dumper), do: same_decimal(value)
|
|
@@ -539,7 +566,8 @@ defmodule Ecto.Type do
|
539
566
|
def dump(:naive_datetime_usec, value, _dumper), do: dump_naive_datetime_usec(value)
|
540
567
|
def dump(:utc_datetime, value, _dumper), do: dump_utc_datetime(value)
|
541
568
|
def dump(:utc_datetime_usec, value, _dumper), do: dump_utc_datetime_usec(value)
|
542
|
- def dump({:param, :any_datetime}, value, _dumper), do: dump_any_datetime(value)
|
569
|
+ def dump(:duration, value, _dumper), do: same_duration(value)
|
570
|
+ def dump({:supertype, :datetime}, value, _dumper), do: dump_any_datetime(value)
|
543
571
|
def dump(mod, value, _dumper) when is_atom(mod), do: mod.dump(value)
|
544
572
|
|
545
573
|
defp dump_float(term) when is_float(term), do: {:ok, term}
|
|
@@ -597,7 +625,7 @@ defmodule Ecto.Type do
|
597
625
|
@spec load(t, term, (t, term -> {:ok, term} | :error)) :: {:ok, term} | :error
|
598
626
|
def load(type, value, loader \\ &load/2)
|
599
627
|
|
600
|
- def load({:parameterized, module, params}, value, loader) do
|
628
|
+ def load({:parameterized, {module, params}}, value, loader) do
|
601
629
|
module.load(value, loader, params)
|
602
630
|
end
|
603
631
|
|
|
@@ -605,15 +633,17 @@ defmodule Ecto.Type do
|
605
633
|
{:ok, nil}
|
606
634
|
end
|
607
635
|
|
608
|
- def load({:maybe, type}, value, loader) do
|
636
|
+ def load({:try, type}, value, loader) do
|
609
637
|
case load(type, value, loader) do
|
610
638
|
{:ok, _} = ok -> ok
|
611
639
|
:error -> {:ok, value}
|
612
640
|
end
|
613
641
|
end
|
614
642
|
|
615
|
- def load({:array, {_, _, _} = type}, value, loader), do: array(value, type, loader, false, [])
|
616
|
- def load({:array, type}, value, loader), do: array(value, type, loader, true, [])
|
643
|
+ def load({:array, {:parameterized, _} = type}, value, loader),
|
644
|
+ do: array_with_type(value, type, loader, false, [])
|
645
|
+
|
646
|
+ def load({:array, type}, value, loader), do: array_with_type(value, type, loader, true, [])
|
617
647
|
def load({:map, type}, value, loader), do: map(value, type, loader, false, %{})
|
618
648
|
|
619
649
|
def load(:any, value, _loader), do: {:ok, value}
|
|
@@ -623,6 +653,7 @@ defmodule Ecto.Type do
|
623
653
|
def load(:map, value, _loader), do: same_map(value)
|
624
654
|
def load(:string, value, _loader), do: same_binary(value)
|
625
655
|
def load(:binary, value, _loader), do: same_binary(value)
|
656
|
+ def load(:bitstring, value, _loader), do: same_bitstring(value)
|
626
657
|
def load(:id, value, _loader), do: same_integer(value)
|
627
658
|
def load(:binary_id, value, _loader), do: same_binary(value)
|
628
659
|
def load(:decimal, value, _loader), do: same_decimal(value)
|
|
@@ -633,6 +664,7 @@ defmodule Ecto.Type do
|
633
664
|
def load(:naive_datetime_usec, value, _loader), do: load_naive_datetime_usec(value)
|
634
665
|
def load(:utc_datetime, value, _loader), do: load_utc_datetime(value)
|
635
666
|
def load(:utc_datetime_usec, value, _loader), do: load_utc_datetime_usec(value)
|
667
|
+ def load(:duration, value, _loader), do: same_duration(value)
|
636
668
|
def load(mod, value, _loader), do: mod.load(value)
|
637
669
|
|
638
670
|
defp load_float(term) when is_float(term), do: {:ok, term}
|
|
@@ -772,11 +804,11 @@ defmodule Ecto.Type do
|
772
804
|
|
773
805
|
"""
|
774
806
|
@spec cast(t, term) :: {:ok, term} | {:error, keyword()} | :error
|
775
|
- def cast({:parameterized, type, params}, value), do: type.cast(value, params)
|
807
|
+ def cast({:parameterized, {type, params}}, value), do: type.cast(value, params)
|
776
808
|
def cast({:in, _type}, nil), do: :error
|
777
809
|
def cast(_type, nil), do: {:ok, nil}
|
778
810
|
|
779
|
- def cast({:maybe, type}, value) do
|
811
|
+ def cast({:try, type}, value) do
|
780
812
|
case cast(type, value) do
|
781
813
|
{:ok, _} = ok -> ok
|
782
814
|
_ -> {:ok, value}
|
|
@@ -793,6 +825,7 @@ defmodule Ecto.Type do
|
793
825
|
defp cast_fun(:map), do: &cast_map/1
|
794
826
|
defp cast_fun(:string), do: &cast_binary/1
|
795
827
|
defp cast_fun(:binary), do: &cast_binary/1
|
828
|
+ defp cast_fun(:bitstring), do: &cast_bitstring/1
|
796
829
|
defp cast_fun(:id), do: &cast_integer/1
|
797
830
|
defp cast_fun(:binary_id), do: &cast_binary/1
|
798
831
|
defp cast_fun(:any), do: &{:ok, &1}
|
|
@@ -804,21 +837,22 @@ defmodule Ecto.Type do
|
804
837
|
defp cast_fun(:naive_datetime_usec), do: &maybe_pad_usec(cast_naive_datetime(&1))
|
805
838
|
defp cast_fun(:utc_datetime), do: &maybe_truncate_usec(cast_utc_datetime(&1))
|
806
839
|
defp cast_fun(:utc_datetime_usec), do: &maybe_pad_usec(cast_utc_datetime(&1))
|
807
|
- defp cast_fun({:param, :any_datetime}), do: &cast_any_datetime(&1)
|
808
|
- defp cast_fun({:parameterized, mod, params}), do: &mod.cast(&1, params)
|
840
|
+ defp cast_fun(:duration), do: &cast_duration/1
|
841
|
+ defp cast_fun({:supertype, :datetime}), do: &cast_any_datetime(&1)
|
842
|
+ defp cast_fun({:parameterized, {mod, params}}), do: &mod.cast(&1, params)
|
809
843
|
defp cast_fun({qual, type}) when qual in @variadic, do: cast_fun({:array, type})
|
810
844
|
|
811
|
- defp cast_fun({:array, {:parameterized, _, _} = type}) do
|
845
|
+ defp cast_fun({:array, {:parameterized, _} = type}) do
|
812
846
|
fun = cast_fun(type)
|
813
|
- &array(&1, fun, false, [])
|
847
|
+ &array_with_index(&1, fun, false, 0, [])
|
814
848
|
end
|
815
849
|
|
816
850
|
defp cast_fun({:array, type}) do
|
817
851
|
fun = cast_fun(type)
|
818
|
- &array(&1, fun, true, [])
|
852
|
+ &array_with_index(&1, fun, true, 0, [])
|
819
853
|
end
|
820
854
|
|
821
|
- defp cast_fun({:map, {:parameterized, _, _} = type}) do
|
855
|
+ defp cast_fun({:map, {:parameterized, _} = type}) do
|
822
856
|
fun = cast_fun(type)
|
823
857
|
&map(&1, fun, false, %{})
|
824
858
|
end
|
|
@@ -876,9 +910,48 @@ defmodule Ecto.Type do
|
876
910
|
defp cast_binary(term) when is_binary(term), do: {:ok, term}
|
877
911
|
defp cast_binary(_), do: :error
|
878
912
|
|
913
|
+ defp cast_bitstring(term) when is_bitstring(term), do: {:ok, term}
|
914
|
+ defp cast_bitstring(_), do: :error
|
915
|
+
|
879
916
|
defp cast_map(term) when is_map(term), do: {:ok, term}
|
880
917
|
defp cast_map(_), do: :error
|
881
918
|
|
919
|
+ if Code.ensure_loaded?(Duration) do
|
920
|
+ defp cast_duration(%Duration{} = term), do: {:ok, term}
|
921
|
+ end
|
922
|
+
|
923
|
+ defp cast_duration(_), do: :error
|
924
|
+
|
925
|
+ @doc """
|
926
|
+ Casts a value to the given type or raises an error.
|
927
|
+
|
928
|
+ See `cast/2` for more information.
|
929
|
+
|
930
|
+ ## Examples
|
931
|
+
|
932
|
+ iex> Ecto.Type.cast!(:integer, "1")
|
933
|
+ 1
|
934
|
+ iex> Ecto.Type.cast!(:integer, 1)
|
935
|
+ 1
|
936
|
+ iex> Ecto.Type.cast!(:integer, nil)
|
937
|
+ nil
|
938
|
+
|
939
|
+ iex> Ecto.Type.cast!(:integer, 1.0)
|
940
|
+ ** (Ecto.CastError) cannot cast 1.0 to :integer
|
941
|
+ """
|
942
|
+ def cast!(type, value) do
|
943
|
+ case Ecto.Type.cast(type, value) do
|
944
|
+ {:ok, value} ->
|
945
|
+ value
|
946
|
+
|
947
|
+ :error ->
|
948
|
+ raise Ecto.CastError, type: type, value: value
|
949
|
+
|
950
|
+ {:error, metadata} ->
|
951
|
+ raise Ecto.CastError, [type: type, value: value] ++ Keyword.take(metadata, [:message])
|
952
|
+ end
|
953
|
+ end
|
954
|
+
|
882
955
|
## Shared helpers
|
883
956
|
|
884
957
|
@compile {:inline, same_integer: 1, same_boolean: 1, same_map: 1, same_decimal: 1, same_date: 1}
|
|
@@ -891,6 +964,9 @@ defmodule Ecto.Type do
|
891
964
|
defp same_binary(term) when is_binary(term), do: {:ok, term}
|
892
965
|
defp same_binary(_), do: :error
|
893
966
|
|
967
|
+ defp same_bitstring(term) when is_bitstring(term), do: {:ok, term}
|
968
|
+ defp same_bitstring(_), do: :error
|
969
|
+
|
894
970
|
defp same_map(term) when is_map(term), do: {:ok, term}
|
895
971
|
defp same_map(_), do: :error
|
896
972
|
|
|
@@ -902,6 +978,12 @@ defmodule Ecto.Type do
|
902
978
|
defp same_date(%Date{} = term), do: {:ok, term}
|
903
979
|
defp same_date(_), do: :error
|
904
980
|
|
981
|
+ if Code.ensure_loaded?(Duration) do
|
982
|
+ defp same_duration(%Duration{} = term), do: {:ok, term}
|
983
|
+ end
|
984
|
+
|
985
|
+ defp same_duration(_), do: :error
|
986
|
+
|
905
987
|
@doc false
|
906
988
|
def empty_trimmed_string?(value) do
|
907
989
|
is_binary(value) and String.trim_leading(value) == ""
|
|
@@ -1243,7 +1325,7 @@ defmodule Ecto.Type do
|
1243
1325
|
end
|
1244
1326
|
end
|
1245
1327
|
|
1246
|
- defp equal_fun({:parameterized, mod, params}) do
|
1328
|
+ defp equal_fun({:parameterized, {mod, params}}) do
|
1247
1329
|
&mod.equal?(&1, &2, params)
|
1248
1330
|
end
|
1249
1331
|
|
|
@@ -1302,11 +1384,7 @@ defmodule Ecto.Type do
|
1302
1384
|
@doc """
|
1303
1385
|
Format type for error messaging and logs.
|
1304
1386
|
"""
|
1305
|
- def format({composite, type}) when composite in [:array, :map] do
|
1306
|
- "{#{inspect(composite)}, #{format(type)}}"
|
1307
|
- end
|
1308
|
-
|
1309
|
- def format({:parameterized, type, params}) do
|
1387
|
+ def format({:parameterized, {type, params}}) do
|
1310
1388
|
if function_exported?(type, :format, 1) do
|
1311
1389
|
apply(type, :format, [params])
|
1312
1390
|
else
|
|
@@ -1314,6 +1392,10 @@ defmodule Ecto.Type do
|
1314
1392
|
end
|
1315
1393
|
end
|
1316
1394
|
|
1395
|
+ def format({composite, type}) when composite in [:array, :map] do
|
1396
|
+ "{#{inspect(composite)}, #{format(type)}}"
|
1397
|
+ end
|
1398
|
+
|
1317
1399
|
def format(type), do: inspect(type)
|
1318
1400
|
|
1319
1401
|
## Helpers
|
|
@@ -1331,23 +1413,35 @@ defmodule Ecto.Type do
|
1331
1413
|
defp of_base_type?(:date, value), do: Kernel.match?(%Date{}, value)
|
1332
1414
|
defp of_base_type?(_, _), do: false
|
1333
1415
|
|
1334
|
- defp array([nil | t], fun, true, acc) do
|
1335
|
- array(t, fun, true, [nil | acc])
|
1416
|
+ defp array_with_index([nil | t], fun, true, index, acc) do
|
1417
|
+ array_with_index(t, fun, true, index + 1, [nil | acc])
|
1336
1418
|
end
|
1337
1419
|
|
1338
|
- defp array([h | t], fun, skip_nil?, acc) do
|
1420
|
+ defp array_with_index([h | t], fun, skip_nil?, index, acc) do
|
1339
1421
|
case fun.(h) do
|
1340
|
- {:ok, h} -> array(t, fun, skip_nil?, [h | acc])
|
1341
|
- :error -> :error
|
1342
|
- {:error, _custom_errors} -> :error
|
1422
|
+ {:ok, h} ->
|
1423
|
+ array_with_index(t, fun, skip_nil?, index + 1, [h | acc])
|
1424
|
+
|
1425
|
+ :error ->
|
1426
|
+ :error
|
1427
|
+
|
1428
|
+ {:error, custom_errors} ->
|
1429
|
+ {:error, Keyword.update(custom_errors, :source, [index], &[index | &1])}
|
1343
1430
|
end
|
1344
1431
|
end
|
1345
1432
|
|
1346
|
- defp array([], _fun, _skip_nil?, acc) do
|
1433
|
+ defp array_with_index([], _fun, _skip_nil?, _index, acc) do
|
1347
1434
|
{:ok, Enum.reverse(acc)}
|
1348
1435
|
end
|
1349
1436
|
|
1350
|
- defp array(_, _, _, _) do
|
1437
|
+ defp array_with_index(%_{} = struct, fun, skip_nil?, index, acc) do
|
1438
|
+ case Enumerable.impl_for(struct) do
|
1439
|
+ nil -> :error
|
1440
|
+ _ -> struct |> Enum.to_list() |> array_with_index(fun, skip_nil?, index, acc)
|
1441
|
+ end
|
1442
|
+ end
|
1443
|
+
|
1444
|
+ defp array_with_index(_, _, _, _, _) do
|
1351
1445
|
:error
|
1352
1446
|
end
|
1353
1447
|
|
|
@@ -1365,9 +1459,14 @@ defmodule Ecto.Type do
|
1365
1459
|
|
1366
1460
|
defp map_each([{key, value} | t], fun, skip_nil?, acc) do
|
1367
1461
|
case fun.(value) do
|
1368
|
- {:ok, value} -> map_each(t, fun, skip_nil?, Map.put(acc, key, value))
|
1369
|
- :error -> :error
|
1370
|
- {:error, _custom_errors} -> :error
|
1462
|
+ {:ok, value} ->
|
1463
|
+ map_each(t, fun, skip_nil?, Map.put(acc, key, value))
|
1464
|
+
|
1465
|
+ :error ->
|
1466
|
+ :error
|
1467
|
+
|
1468
|
+ {:error, custom_errors} ->
|
1469
|
+ {:error, Keyword.update(custom_errors, :source, [key], &[key | &1])}
|
1371
1470
|
end
|
1372
1471
|
end
|
1373
1472
|
|
|
@@ -1375,22 +1474,22 @@ defmodule Ecto.Type do
|
1375
1474
|
{:ok, acc}
|
1376
1475
|
end
|
1377
1476
|
|
1378
|
- defp array([nil | t], type, fun, true, acc) do
|
1379
|
- array(t, type, fun, true, [nil | acc])
|
1477
|
+ defp array_with_type([nil | t], type, fun, true, acc) do
|
1478
|
+ array_with_type(t, type, fun, true, [nil | acc])
|
1380
1479
|
end
|
1381
1480
|
|
1382
|
- defp array([h | t], type, fun, skip_nil?, acc) do
|
1481
|
+ defp array_with_type([h | t], type, fun, skip_nil?, acc) do
|
1383
1482
|
case fun.(type, h) do
|
1384
|
- {:ok, h} -> array(t, type, fun, skip_nil?, [h | acc])
|
1483
|
+ {:ok, h} -> array_with_type(t, type, fun, skip_nil?, [h | acc])
|
1385
1484
|
:error -> :error
|
1386
1485
|
end
|
1387
1486
|
end
|
1388
1487
|
|
1389
|
- defp array([], _type, _fun, _skip_nil?, acc) do
|
1488
|
+ defp array_with_type([], _type, _fun, _skip_nil?, acc) do
|
1390
1489
|
{:ok, Enum.reverse(acc)}
|
1391
1490
|
end
|
1392
1491
|
|
1393
|
- defp array(_, _, _, _, _) do
|
1492
|
+ defp array_with_type(_, _, _, _, _) do
|
1394
1493
|
:error
|
1395
1494
|
end
|
changed
lib/ecto/uuid.ex
|
@@ -24,7 +24,7 @@ defmodule Ecto.UUID do
|
24
24
|
|
25
25
|
If `uuid` is neither of these, `:error` will be returned.
|
26
26
|
|
27
|
- Since both binaries and strings are represent as binaries, this means some
|
27
|
+ Since both binaries and strings are represented as binaries, this means some
|
28
28
|
strings you may not expect are actually also valid UUIDs in their binary form
|
29
29
|
and so will be casted into their string form.
|
changed
mix.exs
|
@@ -2,7 +2,7 @@ defmodule Ecto.MixProject do
|
2
2
|
use Mix.Project
|
3
3
|
|
4
4
|
@source_url "https://github.com/elixir-ecto/ecto"
|
5
|
- @version "3.11.2"
|
5
|
+ @version "3.12.0"
|
6
6
|
|
7
7
|
def project do
|
8
8
|
[
|
|
@@ -60,11 +60,12 @@ defmodule Ecto.MixProject do
|
60
60
|
skip_undefined_reference_warnings_on: ["CHANGELOG.md"],
|
61
61
|
extras: extras(),
|
62
62
|
groups_for_extras: groups_for_extras(),
|
63
|
- groups_for_functions: [
|
63
|
+ groups_for_docs: [
|
64
64
|
group_for_function("Query API"),
|
65
65
|
group_for_function("Schema API"),
|
66
66
|
group_for_function("Transaction API"),
|
67
|
- group_for_function("Runtime API"),
|
67
|
+ group_for_function("Process API"),
|
68
|
+ group_for_function("Config API"),
|
68
69
|
group_for_function("User callbacks")
|
69
70
|
],
|
70
71
|
groups_for_modules: [
|
|
@@ -104,7 +105,37 @@ defmodule Ecto.MixProject do
|
104
105
|
Ecto.Association.NotLoaded,
|
105
106
|
Ecto.Embedded
|
106
107
|
]
|
107
|
- ]
|
108
|
+ ],
|
109
|
+ before_closing_body_tag: fn
|
110
|
+ :html ->
|
111
|
+ """
|
112
|
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/mermaid.min.js"></script>
|
113
|
+ <script>
|
114
|
+ document.addEventListener("DOMContentLoaded", function () {
|
115
|
+ mermaid.initialize({
|
116
|
+ startOnLoad: false,
|
117
|
+ theme: document.body.className.includes("dark") ? "dark" : "default"
|
118
|
+ });
|
119
|
+ let id = 0;
|
120
|
+ for (const codeEl of document.querySelectorAll("pre code.mermaid")) {
|
121
|
+ const preEl = codeEl.parentElement;
|
122
|
+ const graphDefinition = codeEl.textContent;
|
123
|
+ const graphEl = document.createElement("div");
|
124
|
+ const graphId = "mermaid-graph-" + id++;
|
125
|
+ mermaid.render(graphId, graphDefinition).then(({svg, bindFunctions}) => {
|
126
|
+ graphEl.innerHTML = svg;
|
127
|
+ bindFunctions?.(graphEl);
|
128
|
+ preEl.insertAdjacentElement("afterend", graphEl);
|
129
|
+ preEl.remove();
|
130
|
+ });
|
131
|
+ }
|
132
|
+ });
|
133
|
+ </script>
|
134
|
+ """
|
135
|
+
|
136
|
+ _ ->
|
137
|
+ ""
|
138
|
+ end
|
108
139
|
]
|
109
140
|
end
|