Skip to content

Commit 61ca67a

Browse files
Support non-string prefix (#4497)
1 parent 07cdcc2 commit 61ca67a

File tree

9 files changed

+118
-36
lines changed

9 files changed

+118
-36
lines changed

lib/ecto/query.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ defmodule Ecto.Query do
882882
subquery = wrap_in_subquery(query)
883883

884884
case Keyword.fetch(opts, :prefix) do
885-
{:ok, prefix} when is_binary(prefix) or is_nil(prefix) ->
885+
{:ok, prefix} ->
886886
put_in(subquery.query.prefix, prefix)
887887

888888
:error ->
@@ -909,12 +909,13 @@ defmodule Ecto.Query do
909909
@doc """
910910
Puts the given prefix in a query.
911911
"""
912-
def put_query_prefix(%Ecto.Query{} = query, prefix) when is_binary(prefix) do
912+
def put_query_prefix(%Ecto.Query{} = query, prefix) do
913913
%{query | prefix: prefix}
914914
end
915915

916-
def put_query_prefix(other, prefix) when is_binary(prefix) do
917-
other |> Ecto.Queryable.to_query() |> put_query_prefix(prefix)
916+
def put_query_prefix(other, prefix) do
917+
query = %Ecto.Query{} = Ecto.Queryable.to_query(other)
918+
put_query_prefix(query, prefix)
918919
end
919920

920921
@doc """

lib/ecto/query/builder/from.ex

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,15 @@ defmodule Ecto.Query.Builder.From do
7777
If possible, it does all calculations at compile time to avoid
7878
runtime work.
7979
"""
80-
@spec build(Macro.t(), Macro.Env.t(), atom, {:ok, String.t | nil} | nil, hints) ::
80+
@spec build(Macro.t(), Macro.Env.t(), atom, {:ok, Ecto.Schema.prefix | nil} | nil, hints) ::
8181
{Macro.t(), Keyword.t(), non_neg_integer | nil}
8282
def build(query, env, as, prefix, hints) do
8383
hints = Enum.map(hints, &hint!(&1))
8484

8585
prefix = case prefix do
8686
nil -> nil
8787
{:ok, prefix} when is_binary(prefix) or is_nil(prefix) -> {:ok, prefix}
88-
{:ok, {:^, _, [prefix]}} -> {:ok, quote(do: Ecto.Query.Builder.From.prefix!(unquote(prefix)))}
88+
{:ok, {:^, _, [prefix]}} -> {:ok, prefix}
8989
{:ok, prefix} -> Builder.error!("`prefix` must be a compile time string or an interpolated value using ^, got: #{Macro.to_string(prefix)}")
9090
end
9191

@@ -152,13 +152,6 @@ defmodule Ecto.Query.Builder.From do
152152
{:%, [], [Ecto.Query, {:%{}, [], query_fields}]}
153153
end
154154

155-
@doc """
156-
Validates a prefix at runtime.
157-
"""
158-
@spec prefix!(any) :: nil | String.t()
159-
def prefix!(prefix) when is_binary(prefix) or is_nil(prefix), do: prefix
160-
def prefix!(prefix), do: raise("`prefix` must be a string, got: #{inspect(prefix)}")
161-
162155
@doc """
163156
Validates hints at compile time and runtime
164157
"""
@@ -187,7 +180,7 @@ defmodule Ecto.Query.Builder.From do
187180
@doc """
188181
The callback applied by `build/2` to build the query.
189182
"""
190-
@spec apply(Ecto.Queryable.t(), non_neg_integer, Macro.t(), {:ok, String.t} | nil, hints) :: Ecto.Query.t()
183+
@spec apply(Ecto.Queryable.t(), non_neg_integer, Macro.t(), {:ok, Ecto.Schema.prefix} | nil, hints) :: Ecto.Query.t()
191184
def apply(query, binds, as, prefix, hints) do
192185
query =
193186
query

lib/ecto/query/builder/join.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ defmodule Ecto.Query.Builder.Join do
128128
If possible, it does all calculations at compile time to avoid
129129
runtime work.
130130
"""
131-
@spec build(Macro.t, atom, [Macro.t], Macro.t, Macro.t, Macro.t, atom, nil | {:ok, String.t | nil}, nil | String.t | [String.t], Macro.Env.t) ::
131+
@spec build(Macro.t, atom, [Macro.t], Macro.t, Macro.t, Macro.t, atom, nil | {:ok, Ecto.Schema.prefix}, nil | String.t | [String.t], Macro.Env.t) ::
132132
{Macro.t, Keyword.t, non_neg_integer | nil}
133133
def build(query, qual, binding, expr, count_bind, on, as, prefix, maybe_hints, env) do
134134
{:ok, prefix} = prefix || {:ok, nil}
@@ -144,7 +144,7 @@ defmodule Ecto.Query.Builder.Join do
144144
prefix = case prefix do
145145
nil -> nil
146146
prefix when is_binary(prefix) -> prefix
147-
{:^, _, [prefix]} -> quote(do: Ecto.Query.Builder.From.prefix!(unquote(prefix)))
147+
{:^, _, [prefix]} -> prefix
148148
prefix -> Builder.error!("`prefix` must be a compile time string or an interpolated value using ^, got: #{Macro.to_string(prefix)}")
149149
end
150150

lib/ecto/query/planner.ex

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2311,11 +2311,7 @@ defmodule Ecto.Query.Planner do
23112311
"""
23122312
def attach_prefix(%{prefix: nil} = query, opts) when is_list(opts) do
23132313
case Keyword.fetch(opts, :prefix) do
2314-
{:ok, prefix} when is_binary(prefix) or is_nil(prefix) ->
2315-
%{query | prefix: prefix}
2316-
2317-
{:ok, prefix} when is_atom(prefix) ->
2318-
IO.warn("atom prefixes are deprecated. Please use a string instead.")
2314+
{:ok, prefix} ->
23192315
%{query | prefix: prefix}
23202316

23212317
:error ->

lib/ecto/schema.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ defmodule Ecto.Schema do
478478
alias Ecto.Schema.Metadata
479479

480480
@type source :: String.t()
481-
@type prefix :: String.t() | nil
481+
@type prefix :: any()
482482
@type schema :: %{optional(atom) => any, __struct__: atom, __meta__: Metadata.t()}
483483
@type embedded_schema :: %{optional(atom) => any, __struct__: atom}
484484
@type t :: schema | embedded_schema
@@ -662,7 +662,7 @@ defmodule Ecto.Schema do
662662
%{unquote_splicing(Macro.escape(@ecto_changeset_fields))}
663663
end
664664

665-
def __schema__(:prefix), do: unquote(prefix)
665+
def __schema__(:prefix), do: unquote(Macro.escape(prefix))
666666
def __schema__(:source), do: unquote(source)
667667
def __schema__(:fields), do: unquote(Enum.map(fields, &elem(&1, 0)))
668668
def __schema__(:query_fields), do: unquote(Enum.map(query_fields, &elem(&1, 0)))
@@ -690,7 +690,7 @@ defmodule Ecto.Schema do
690690
%Ecto.Query{
691691
from: %Ecto.Query.FromExpr{
692692
source: {unquote(source), __MODULE__},
693-
prefix: unquote(prefix)
693+
prefix: unquote(Macro.escape(prefix))
694694
}
695695
}
696696
end

test/ecto/query/planner_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,12 @@ defmodule Ecto.Query.PlannerTest do
817817
|> plan()
818818

819819
assert query.sources == {{"comments", Comment, "global"}, {"comments", Comment, "local"}}
820+
821+
# Non-string schema prefix is supported
822+
{query, _, _, _} =
823+
from(c in Comment, join: Post, on: true) |> Map.put(:prefix, %{key: :global}) |> plan()
824+
825+
assert query.sources == {{"comments", Comment, %{key: :global}}, {"posts", Post, "my_prefix"}}
820826
end
821827

822828
test "plan: combination queries" do

test/ecto/query_test.exs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ defmodule Ecto.QueryTest do
242242
assert put_query_prefix(from("posts"), "hello").prefix == "hello"
243243
assert put_query_prefix(Schema, "hello").prefix == "hello"
244244
end
245+
246+
test "stores non-string prefix in query" do
247+
assert put_query_prefix(from("posts"), %{key: :hello}).prefix == %{key: :hello}
248+
assert put_query_prefix(Schema, %{key: :hello}).prefix == %{key: :hello}
249+
end
245250
end
246251

247252
describe "trailing bindings (...)" do
@@ -500,25 +505,16 @@ defmodule Ecto.QueryTest do
500505
assert hd(query.joins).prefix == join_prefix
501506
end
502507

503-
test "variables are validated at runtime" do
504-
prefix = 123
505-
506-
assert_raise RuntimeError, ~r/`prefix` must be a string/, fn ->
507-
from p in "posts", prefix: ^prefix
508-
end
509-
510-
assert_raise RuntimeError, ~r/`prefix` must be a string/, fn ->
511-
from p in "posts", join: "comments", on: true, prefix: ^prefix
512-
end
513-
end
514-
515508
test "are supported and overridden from schemas" do
516509
query = from(Post)
517510
assert query.from.prefix == "another"
518511

519512
query = from(Post, prefix: "hello")
520513
assert query.from.prefix == "hello"
521514

515+
query = from(Post, prefix: ^123)
516+
assert query.from.prefix == 123
517+
522518
query = from(Post, prefix: nil)
523519
assert query.from.prefix == nil
524520
end

test/ecto/repo_test.exs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ defmodule Ecto.RepoTest do
8181
end
8282
end
8383

84+
defmodule MySchemaWithNonStringPrefix do
85+
use Ecto.Schema
86+
87+
@schema_prefix %{key: :private}
88+
89+
schema "my_schema" do
90+
field :x, :string
91+
end
92+
end
93+
8494
defmodule MySchemaWithAssoc do
8595
use Ecto.Schema
8696

@@ -318,6 +328,12 @@ defmodule Ecto.RepoTest do
318328
TestRepo.reload(struct_with_prefix)
319329
assert_received {:all, %{prefix: "another"}}
320330
end
331+
332+
test "supports non-string prefix" do
333+
struct_with_prefix = put_meta(%MySchema{id: 2}, prefix: %{key: :another})
334+
TestRepo.reload(struct_with_prefix)
335+
assert_received {:all, %{prefix: %{key: :another}}}
336+
end
321337
end
322338

323339
defmodule DefaultOptionRepo do
@@ -1116,6 +1132,12 @@ defmodule Ecto.RepoTest do
11161132

11171133
assert {:ok, schema} = TestRepo.delete(valid, prefix: "public")
11181134
assert schema.__meta__.prefix == "public"
1135+
1136+
assert {:ok, schema} = TestRepo.insert(valid, prefix: %{key: :public})
1137+
assert schema.__meta__.prefix == %{key: :public}
1138+
1139+
assert {:ok, schema} = TestRepo.delete(valid, prefix: %{key: :public})
1140+
assert schema.__meta__.prefix == %{key: :public}
11191141
end
11201142

11211143
test "insert and delete prefix overrides schema_prefix with struct" do
@@ -1128,6 +1150,16 @@ defmodule Ecto.RepoTest do
11281150
assert schema.__meta__.prefix == "public"
11291151
end
11301152

1153+
test "insert and delete prefix overrides schema_prefix with struct when prefix is not a string" do
1154+
valid = %MySchemaWithNonStringPrefix{id: 1}
1155+
1156+
assert {:ok, schema} = TestRepo.insert(valid, prefix: %{key: :public})
1157+
assert schema.__meta__.prefix == %{key: :public}
1158+
1159+
assert {:ok, schema} = TestRepo.delete(valid, prefix: %{key: :public})
1160+
assert schema.__meta__.prefix == %{key: :public}
1161+
end
1162+
11311163
test "insert, update, insert_or_update and delete sets schema prefix with changeset" do
11321164
valid = Ecto.Changeset.cast(%MySchema{id: 1}, %{x: "foo"}, [:x])
11331165

@@ -1474,6 +1506,18 @@ defmodule Ecto.RepoTest do
14741506

14751507
assert [schema] = TestRepo.all(MySchema, prefix: "public")
14761508
assert schema.__meta__.prefix == "public"
1509+
1510+
assert schema = TestRepo.get(MySchema, 123, prefix: %{key: :public})
1511+
assert schema.__meta__.prefix == %{key: :public}
1512+
1513+
assert schema = TestRepo.get_by(MySchema, [id: 123], prefix: %{key: :public})
1514+
assert schema.__meta__.prefix == %{key: :public}
1515+
1516+
assert schema = TestRepo.one(MySchema, prefix: %{key: :public})
1517+
assert schema.__meta__.prefix == %{key: :public}
1518+
1519+
assert [schema] = TestRepo.all(MySchema, prefix: %{key: :public})
1520+
assert schema.__meta__.prefix == %{key: :public}
14771521
end
14781522

14791523
test "get, get_by, one and all ignores prefix if schema_prefix set" do
@@ -1488,6 +1532,18 @@ defmodule Ecto.RepoTest do
14881532

14891533
assert [schema] = TestRepo.all(MySchemaWithPrefix, prefix: "public")
14901534
assert schema.__meta__.prefix == "private"
1535+
1536+
assert schema = TestRepo.get(MySchemaWithNonStringPrefix, 123, prefix: %{key: :public})
1537+
assert schema.__meta__.prefix == %{key: :private}
1538+
1539+
assert schema = TestRepo.get_by(MySchemaWithNonStringPrefix, [id: 123], prefix: %{key: :public})
1540+
assert schema.__meta__.prefix == %{key: :private}
1541+
1542+
assert schema = TestRepo.one(MySchemaWithNonStringPrefix, prefix: %{key: :public})
1543+
assert schema.__meta__.prefix == %{key: :private}
1544+
1545+
assert [schema] = TestRepo.all(MySchemaWithNonStringPrefix, prefix: %{key: :public})
1546+
assert schema.__meta__.prefix == %{key: :private}
14911547
end
14921548

14931549
describe "changeset prepare" do

test/ecto/schema_test.exs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,15 @@ defmodule Ecto.SchemaTest do
341341
end
342342
end
343343

344+
defmodule SchemaWithNonStringPrefix do
345+
use Ecto.Schema
346+
347+
@schema_prefix %{key: :tenant}
348+
schema "company" do
349+
field :name
350+
end
351+
end
352+
344353
test "schema prefix metadata" do
345354
assert SchemaWithPrefix.__schema__(:source) == "company"
346355
assert SchemaWithPrefix.__schema__(:prefix) == "tenant"
@@ -366,6 +375,31 @@ defmodule Ecto.SchemaTest do
366375
assert query.from.prefix == "tenant"
367376
end
368377

378+
test "schema non-string prefix metadata" do
379+
assert SchemaWithNonStringPrefix.__schema__(:source) == "company"
380+
assert SchemaWithNonStringPrefix.__schema__(:prefix) == %{key: :tenant}
381+
assert %SchemaWithNonStringPrefix{}.__meta__.source == "company"
382+
assert %SchemaWithNonStringPrefix{}.__meta__.prefix == %{key: :tenant}
383+
end
384+
385+
test "schema non-string prefix in queries from" do
386+
import Ecto.Query
387+
388+
query = from(SchemaWithNonStringPrefix, select: 1)
389+
assert query.from.prefix == %{key: :tenant}
390+
391+
query = from({"another_company", SchemaWithNonStringPrefix}, select: 1)
392+
assert query.from.prefix == %{key: :tenant}
393+
394+
from = SchemaWithNonStringPrefix
395+
query = from(from, select: 1)
396+
assert query.from.prefix == %{key: :tenant}
397+
398+
from = {"another_company", SchemaWithNonStringPrefix}
399+
query = from(from, select: 1)
400+
assert query.from.prefix == %{key: :tenant}
401+
end
402+
369403
## Schema context
370404
defmodule SchemaWithContext do
371405
use Ecto.Schema

0 commit comments

Comments
 (0)