Skip to content

Commit b02e921

Browse files
Revert "Allow numbers in literal/1 (#4562)" and add identifier/1 and constant/1 instead (#4563)
1 parent 3ab8279 commit b02e921

File tree

3 files changed

+101
-34
lines changed

3 files changed

+101
-34
lines changed

lib/ecto/query/api.ex

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -480,20 +480,36 @@ defmodule Ecto.Query.API do
480480
def fragment(fragments), do: doc!([fragments])
481481

482482
@doc """
483-
Allows a literal identifier or number to be injected into a fragment:
483+
Allows a dynamic identifier to be injected into a fragment:
484484
485485
collation = "es_ES"
486-
fragment("? COLLATE ?", ^name, literal(^collation))
486+
select("posts", [p], fragment("? COLLATE ?", p.title, identifier(^"es_ES")))
487+
488+
The example above will inject the value of `collation` directly
489+
into the query instead of treating it as a query parameter. It will
490+
generate a query such as `SELECT p0.title COLLATE "es_ES" FROM "posts" AS p0`
491+
as opposed to `SELECT p0.title COLLATE $1 FROM "posts" AS p0`.
492+
493+
Note that each different value of `collation` will emit a different query,
494+
which will be independently prepared and cached.
495+
"""
496+
def identifier(binary), do: doc!([binary])
497+
498+
@doc """
499+
Allows a dynamic string or number to be injected into a fragment:
487500
488501
limit = 10
489-
limit(query, fragment("?", literal(^limit)))
502+
"posts" |> select([p], p.title) |> limit(fragment("?", constant(^limit)))
503+
504+
The example above will inject the value of `limit` directly
505+
into the query instead of treating it as a query parameter. It will
506+
generate a query such as `SELECT p0.title FROM "posts" AS p0 LIMIT 1`
507+
as opposed to `SELECT p0.title FROM "posts" AS p0` LIMIT $1`.
490508
491-
The example above will inject `collation` and `limit` into the queries as
492-
literals instead of query parameters. Note that each different value passed
493-
to `literal/1` will emit a different query, which will be independently prepared
494-
and cached.
509+
Note that each different value of `limit` will emit a different query,
510+
which will be independently prepared and cached.
495511
"""
496-
def literal(literal), do: doc!([literal])
512+
def constant(value), do: doc!([value])
497513

498514
@doc """
499515
Allows a list argument to be spliced into a fragment.
@@ -507,7 +523,7 @@ defmodule Ecto.Query.API do
507523
You may only splice runtime values. For example, this would not work because
508524
query bindings are compile-time constructs:
509525
510-
from p in Post, where: fragment("concat(?)", splice(^[p.count, " ", "count"])
526+
from p in Post, where: fragment("concat(?)", splice(^[p.count, " ", "count"]))
511527
"""
512528
def splice(list), do: doc!([list])
513529

lib/ecto/query/builder.ex

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -751,16 +751,45 @@ defmodule Ecto.Query.Builder do
751751
)
752752
end
753753

754-
defp escape_fragment({:literal, _meta, [expr]}, params_acc, _vars, _env) do
754+
defp escape_fragment({:literal, meta, [expr]}, params_acc, vars, env) do
755+
env =
756+
case env do
757+
{env, _fun} -> env
758+
env -> env
759+
end
760+
761+
IO.warn(
762+
"`literal/1` is deprecated. Please use `identifier/1` instead.",
763+
Macro.Env.stacktrace(env)
764+
)
765+
766+
escape_fragment({:identifier, meta, [expr]}, params_acc, vars, env)
767+
end
768+
769+
defp escape_fragment({:identifier, _meta, [expr]}, params_acc, _vars, _env) do
770+
case expr do
771+
{:^, _, [expr]} ->
772+
checked = quote do: Ecto.Query.Builder.identifier!(unquote(expr))
773+
escaped = {:{}, [], [:identifier, [], [checked]]}
774+
{escaped, params_acc}
775+
776+
_ ->
777+
error!(
778+
"identifier/1 in fragment expects an interpolated value, such as identifier(^value), got `#{Macro.to_string(expr)}`"
779+
)
780+
end
781+
end
782+
783+
defp escape_fragment({:constant, _meta, [expr]}, params_acc, _vars, _env) do
755784
case expr do
756785
{:^, _, [expr]} ->
757-
checked = quote do: Ecto.Query.Builder.literal!(unquote(expr))
758-
escaped = {:{}, [], [:literal, [], [checked]]}
786+
checked = quote do: Ecto.Query.Builder.constant!(unquote(expr))
787+
escaped = {:{}, [], [:constant, [], [checked]]}
759788
{escaped, params_acc}
760789

761790
_ ->
762791
error!(
763-
"literal/1 in fragment expects an interpolated value, such as literal(^value), got `#{Macro.to_string(expr)}`"
792+
"constant/1 in fragment expects an interpolated value, such as constant(^value), got `#{Macro.to_string(expr)}`"
764793
)
765794
end
766795
end
@@ -1254,14 +1283,27 @@ defmodule Ecto.Query.Builder do
12541283
end
12551284

12561285
@doc """
1257-
Called by escaper at runtime to verify literal in fragments.
1286+
Called by escaper at runtime to verify identifier in fragments.
12581287
"""
1259-
def literal!(literal) when is_binary(literal), do: literal
1260-
def literal!(literal) when is_number(literal), do: literal
1288+
def identifier!(identifier) do
1289+
if is_binary(identifier) do
1290+
identifier
1291+
else
1292+
raise ArgumentError,
1293+
"identifier(^value) expects `value` to be a string, got `#{inspect(identifier)}`"
1294+
end
1295+
end
12611296

1262-
def literal!(literal) do
1263-
raise ArgumentError,
1264-
"literal(^value) expects `value` to be a string or a number, got `#{inspect(literal)}`"
1297+
@doc """
1298+
Called by escaper at runtime to verify constant in fragments.
1299+
"""
1300+
def constant!(constant) do
1301+
if is_binary(constant) or is_number(constant) do
1302+
constant
1303+
else
1304+
raise ArgumentError,
1305+
"constant(^value) expects `value` to be a string or a number, got `#{inspect(constant)}`"
1306+
end
12651307
end
12661308

12671309
@doc """

test/ecto/query_test.exs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -976,32 +976,41 @@ defmodule Ecto.QueryTest do
976976
end
977977
end
978978

979-
test "supports literals" do
980-
query = from p in "posts", select: fragment("? COLLATE ?", p.name, literal(^"es_ES"))
979+
test "supports identifiers" do
980+
query = from p in "posts", select: fragment("? COLLATE ?", p.name, identifier(^"es_ES"))
981981
assert {:fragment, _, parts} = query.select.expr
982982

983983
assert [
984984
raw: "",
985985
expr: {{:., _, [{:&, _, [0]}, :name]}, _, _},
986986
raw: " COLLATE ",
987-
expr: {:literal, _, ["es_ES"]},
987+
expr: {:identifier, _, ["es_ES"]},
988988
raw: ""
989989
] = parts
990990

991-
query = from p in "posts", limit: fragment("?", literal(^1))
992-
assert {:fragment, _, parts} = query.limit.expr
991+
msg = "identifier(^value) expects `value` to be a string, got `123`"
993992

994-
assert [
995-
raw: "",
996-
expr: {:literal, _, [1]},
997-
raw: ""
998-
] = parts
993+
assert_raise ArgumentError, msg, fn ->
994+
from p in "posts", select: fragment("? COLLATE ?", p.name, identifier(^123))
995+
end
996+
end
999997

1000-
assert_raise ArgumentError,
1001-
"literal(^value) expects `value` to be a string or a number, got `%{}`",
1002-
fn ->
1003-
from p in "posts", select: fragment("? COLLATE ?", p.name, literal(^%{}))
1004-
end
998+
test "supports constants" do
999+
query =
1000+
from p in "posts",
1001+
select: fragment("?", constant(^"hi")),
1002+
limit: fragment("?", constant(^1))
1003+
1004+
assert {:fragment, _, select_parts} = query.select.expr
1005+
assert {:fragment, _, limit_parts} = query.limit.expr
1006+
assert [raw: "", expr: {:constant, _, ["hi"]}, raw: ""] = select_parts
1007+
assert [raw: "", expr: {:constant, _, [1]}, raw: ""] = limit_parts
1008+
1009+
msg = "constant(^value) expects `value` to be a string or a number, got `%{}`"
1010+
1011+
assert_raise ArgumentError, msg, fn ->
1012+
from p in "posts", limit: fragment("?", constant(^%{}))
1013+
end
10051014
end
10061015

10071016
test "supports list splicing" do

0 commit comments

Comments
 (0)